mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Add Feature: allow user to sort Lists in Board by his own preference, boardadmin can star list
This commit is contained in:
parent
2737d6b23f
commit
bc2a20f04e
15 changed files with 272 additions and 14 deletions
|
|
@ -77,6 +77,10 @@ template(name="boardHeaderBar")
|
|||
i.fa.fa-archive
|
||||
span {{_ 'archives'}}
|
||||
|
||||
if showSort
|
||||
a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
|
||||
i.fa(class="{{directionClass}}")
|
||||
span {{_ 'sort'}}{{_ listSortShortDesc}}
|
||||
a.board-header-btn.js-open-filter-view(
|
||||
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
|
||||
class="{{#if Filter.isActive}}emphasis{{/if}}")
|
||||
|
|
@ -194,6 +198,20 @@ template(name="createBoard")
|
|||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="listsortPopup")
|
||||
h2
|
||||
| {{_ 'list-sort-by'}}
|
||||
hr
|
||||
ul.pop-over-list
|
||||
each value in allowedSortValues
|
||||
li
|
||||
a.js-sort-by(name="{{value.name}}")
|
||||
if $eq sortby value.name
|
||||
i(class="fa {{Direction}}")
|
||||
| {{_ value.label }}{{_ value.shortLabel}}
|
||||
if $eq sortby value.name
|
||||
i(class="fa fa-check")
|
||||
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
label
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const DOWNCLS = 'fa-sort-down';
|
||||
const UPCLS = 'fa-sort-up';
|
||||
Template.boardMenuPopup.events({
|
||||
'click .js-rename-board': Popup.open('boardChangeTitle'),
|
||||
'click .js-custom-fields'() {
|
||||
|
|
@ -80,7 +82,25 @@ BlazeComponent.extendComponent({
|
|||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
return currentBoard && currentBoard.stars >= 2;
|
||||
},
|
||||
|
||||
showSort() {
|
||||
return Meteor.user().hasSortBy();
|
||||
},
|
||||
directionClass() {
|
||||
return this.currentDirection() === -1 ? DOWNCLS : UPCLS;
|
||||
},
|
||||
changeDirection() {
|
||||
const direction = 0 - this.currentDirection() === -1 ? '-' : '';
|
||||
Meteor.call('setListSortBy', direction + this.currentListSortBy());
|
||||
},
|
||||
currentDirection() {
|
||||
return Meteor.user().getListSortByDirection();
|
||||
},
|
||||
currentListSortBy() {
|
||||
return Meteor.user().getListSortBy();
|
||||
},
|
||||
listSortShortDesc() {
|
||||
return `list-label-short-${this.currentListSortBy()}`;
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -118,6 +138,16 @@ BlazeComponent.extendComponent({
|
|||
'click .js-open-filter-view'() {
|
||||
Sidebar.setView('filter');
|
||||
},
|
||||
'click .js-open-sort-view'(evt) {
|
||||
const target = evt.target;
|
||||
if (target.tagName === 'I') {
|
||||
// click on the text, popup choices
|
||||
this.changeDirection();
|
||||
} else {
|
||||
// change the sort order
|
||||
Popup.open('listsort')(evt);
|
||||
}
|
||||
},
|
||||
'click .js-filter-reset'(event) {
|
||||
event.stopPropagation();
|
||||
Sidebar.setView();
|
||||
|
|
@ -277,3 +307,73 @@ BlazeComponent.extendComponent({
|
|||
];
|
||||
},
|
||||
}).register('boardChangeWatchPopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
//this.sortBy = new ReactiveVar();
|
||||
////this.sortDirection = new ReactiveVar();
|
||||
//this.setSortBy();
|
||||
this.downClass = DOWNCLS;
|
||||
this.upClass = UPCLS;
|
||||
},
|
||||
allowedSortValues() {
|
||||
const types = [];
|
||||
const pushed = {};
|
||||
Meteor.user()
|
||||
.getListSortTypes()
|
||||
.forEach(type => {
|
||||
const key = type.replace(/^-/, '');
|
||||
if (pushed[key] === undefined) {
|
||||
types.push({
|
||||
name: key,
|
||||
label: `list-label-${key}`,
|
||||
shortLabel: `list-label-short-${key}`,
|
||||
});
|
||||
pushed[key] = 1;
|
||||
}
|
||||
});
|
||||
return types;
|
||||
},
|
||||
Direction() {
|
||||
return Meteor.user().getListSortByDirection() === -1
|
||||
? this.downClass
|
||||
: this.upClass;
|
||||
},
|
||||
sortby() {
|
||||
return Meteor.user().getListSortBy();
|
||||
},
|
||||
|
||||
setSortBy(type = null) {
|
||||
const user = Meteor.user();
|
||||
if (type === null) {
|
||||
type = user._getListSortBy();
|
||||
} else {
|
||||
let value = '';
|
||||
if (type.map) {
|
||||
// is an array
|
||||
value = (type[1] === -1 ? '-' : '') + type[0];
|
||||
}
|
||||
Meteor.call('setListSortBy', value);
|
||||
}
|
||||
//this.sortBy.set(type[0]);
|
||||
//this.sortDirection.set(type[1]);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-sort-by'(evt) {
|
||||
evt.preventDefault();
|
||||
const target = evt.target;
|
||||
const sortby = target.getAttribute('name');
|
||||
const down = !!target.querySelector(`.${this.upClass}`);
|
||||
const direction = down ? -1 : 1;
|
||||
this.setSortBy([sortby, direction]);
|
||||
if (Utils.isMiniScreen) {
|
||||
Popup.close();
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('listsortPopup');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ template(name="listHeader")
|
|||
if currentList
|
||||
a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
|
||||
h2.list-header-name(
|
||||
title="{{ moment updatedAt 'LLL' }}"
|
||||
title="{{ moment modifiedAt 'LLL' }}"
|
||||
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}js-open-inlined-form is-editable{{/unless}}{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
|
|
@ -39,6 +39,8 @@ template(name="listHeader")
|
|||
i.list-header-watch-icon.fa.fa-eye
|
||||
div.list-header-menu
|
||||
unless currentUser.isCommentOnly
|
||||
if isBoardAdmin
|
||||
a.fa.js-list-star.list-header-plus-icon(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if canSeeAddCard
|
||||
a.js-add-card.fa.fa-plus.list-header-plus-icon
|
||||
a.fa.fa-navicon.js-open-list-menu
|
||||
|
|
|
|||
|
|
@ -13,6 +13,20 @@ BlazeComponent.extendComponent({
|
|||
);
|
||||
},
|
||||
|
||||
isBoardAdmin() {
|
||||
return Meteor.user().isBoardAdmin();
|
||||
},
|
||||
starred(check = undefined) {
|
||||
const list = Template.currentData();
|
||||
const status = list.isStarred();
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
list.star(!status);
|
||||
return !status;
|
||||
}
|
||||
},
|
||||
editTitle(event) {
|
||||
event.preventDefault();
|
||||
const newTitle = this.childComponents('inlinedForm')[0]
|
||||
|
|
@ -61,6 +75,10 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-list-star'(event) {
|
||||
event.preventDefault();
|
||||
this.starred(!this.starred());
|
||||
},
|
||||
'click .js-open-list-menu': Popup.open('listAction'),
|
||||
'click .js-add-card'(event) {
|
||||
const listDom = $(event.target).parents(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
and #each x in y constructors to fix this.
|
||||
|
||||
template(name="filterSidebar")
|
||||
ul.sidebar-list
|
||||
span {{_ 'list-filter-label'}}
|
||||
input.js-list-filter(type="text")
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.labelIds.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-label-filter
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'change .js-list-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.lists.set(this.find('.js-list-filter').value.trim());
|
||||
},
|
||||
'click .js-toggle-label-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.labelIds.toggle(this.currentData()._id);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ template(name="listsGroup")
|
|||
+cardDetails(currentCard)
|
||||
|
||||
template(name="addListForm")
|
||||
.list.list-composer.js-list-composer
|
||||
.list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
|
||||
.list-header-add
|
||||
+inlinedForm(autoclose=false)
|
||||
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
|
||||
|
|
|
|||
|
|
@ -267,6 +267,11 @@ BlazeComponent.extendComponent({
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (Filter.lists._isActive()) {
|
||||
if (!list.title.match(Filter.lists.getRegexSelector())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (Filter.hideEmpty.isSelected()) {
|
||||
const swimlaneId = this.parentComponent()
|
||||
.parentComponent()
|
||||
|
|
|
|||
|
|
@ -439,6 +439,14 @@ class AdvancedFilter {
|
|||
const commands = this._filterToCommands();
|
||||
return this._arrayToSelector(commands);
|
||||
}
|
||||
getRegexSelector() {
|
||||
// generate a regex for filter list
|
||||
this._dep.depend();
|
||||
return new RegExp(
|
||||
`^.*${this._filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$`,
|
||||
'i',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The global Filter object.
|
||||
|
|
@ -455,8 +463,16 @@ Filter = {
|
|||
hideEmpty: new SetFilter(),
|
||||
customFields: new SetFilter('_id'),
|
||||
advanced: new AdvancedFilter(),
|
||||
lists: new AdvancedFilter(), // we need the ability to filter list by name as well
|
||||
|
||||
_fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'],
|
||||
_fields: [
|
||||
'labelIds',
|
||||
'members',
|
||||
'archive',
|
||||
'hideEmpty',
|
||||
'customFields',
|
||||
'lists',
|
||||
],
|
||||
|
||||
// We don't filter cards that have been added after the last filter change. To
|
||||
// implement this we keep the id of these cards in this `_exceptions` fields
|
||||
|
|
|
|||
|
|
@ -300,8 +300,18 @@
|
|||
"error-username-taken": "This username is already taken",
|
||||
"error-email-taken": "Email has already been taken",
|
||||
"export-board": "Export board",
|
||||
"sort": "Sort",
|
||||
"sort-desc": "Click to Sort List",
|
||||
"list-sort-by": "Sort the List By:",
|
||||
"list-label-modifiedAt": "Last Access Time",
|
||||
"list-label-title": "Name of the List",
|
||||
"list-label-sort": "Your Manual Order",
|
||||
"list-label-short-modifiedAt": "(L)",
|
||||
"list-label-short-title": "(N)",
|
||||
"list-label-short-sort": "(M)",
|
||||
"filter": "Filter",
|
||||
"filter-cards": "Filter Cards",
|
||||
"filter-cards": "Filter Cards or Lists",
|
||||
"list-filter-label": "Filter List by Title",
|
||||
"filter-clear": "Clear filter",
|
||||
"filter-no-label": "No label",
|
||||
"filter-no-member": "No member",
|
||||
|
|
|
|||
|
|
@ -409,18 +409,20 @@ Boards.helpers({
|
|||
},
|
||||
|
||||
lists() {
|
||||
const enabled = Meteor.user().hasShowDesktopDragHandles();
|
||||
return enabled ? this.draggableLists() : this.newestLists();
|
||||
const enabled = Meteor.user().hasSortBy();
|
||||
return enabled ? this.newestLists() : this.draggableLists();
|
||||
},
|
||||
|
||||
newestLists() {
|
||||
// sorted lists from newest to the oldest, by its creation date or its cards' last modification date
|
||||
const value = Meteor.user()._getListSortBy();
|
||||
const sortKey = { starred: -1, [value[0]]: value[1] }; // [["starred",-1],value];
|
||||
return Lists.find(
|
||||
{
|
||||
boardId: this._id,
|
||||
archived: false,
|
||||
},
|
||||
{ sort: { updatedAt: -1 } },
|
||||
{ sort: sortKey },
|
||||
);
|
||||
},
|
||||
draggableLists() {
|
||||
|
|
|
|||
|
|
@ -1696,9 +1696,11 @@ if (Meteor.isServer) {
|
|||
const activityType = `a-${action}`;
|
||||
const card = Cards.findOne(doc._id);
|
||||
const list = card.list();
|
||||
if (list) {
|
||||
if (list && action === 'endAt') {
|
||||
// change list modifiedAt
|
||||
const modifiedAt = new Date();
|
||||
const modifiedAt = new Date(
|
||||
new Date(value).getTime() - 365 * 24 * 3600 * 1e3,
|
||||
); // set it as 1 year before
|
||||
const boardId = list.boardId;
|
||||
Lists.direct.update(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ Lists.attachSchema(
|
|||
*/
|
||||
type: String,
|
||||
},
|
||||
starred: {
|
||||
/**
|
||||
* if a list is stared
|
||||
* then we put it on the top
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
archived: {
|
||||
/**
|
||||
* is the list archived
|
||||
|
|
@ -81,10 +90,14 @@ Lists.attachSchema(
|
|||
denyUpdate: false,
|
||||
// eslint-disable-next-line consistent-return
|
||||
autoValue() {
|
||||
if (this.isInsert || this.isUpsert || this.isUpdate) {
|
||||
// this is redundant with updatedAt
|
||||
/*if (this.isInsert || this.isUpsert || this.isUpdate) {
|
||||
return new Date();
|
||||
} else {
|
||||
this.unset();
|
||||
}*/
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -252,6 +265,10 @@ Lists.helpers({
|
|||
return this.type === 'template-list';
|
||||
},
|
||||
|
||||
isStarred() {
|
||||
return this.starred === true;
|
||||
},
|
||||
|
||||
remove() {
|
||||
Lists.remove({ _id: this._id });
|
||||
},
|
||||
|
|
@ -261,6 +278,9 @@ Lists.mutations({
|
|||
rename(title) {
|
||||
return { $set: { title } };
|
||||
},
|
||||
star(enable = true) {
|
||||
return { $set: { starred: !!enable } };
|
||||
},
|
||||
|
||||
archive() {
|
||||
if (this.isTemplateList()) {
|
||||
|
|
|
|||
|
|
@ -174,8 +174,8 @@ Swimlanes.helpers({
|
|||
},
|
||||
|
||||
lists() {
|
||||
const enabled = Meteor.user().hasShowDesktopDragHandles();
|
||||
return enabled ? this.draggableLists() : this.newestLists();
|
||||
const enabled = Meteor.user().hasSortBy();
|
||||
return enabled ? this.newestLists() : this.draggableLists();
|
||||
},
|
||||
newestLists() {
|
||||
// sorted lists from newest to the oldest, by its creation date or its cards' last modification date
|
||||
|
|
@ -185,7 +185,7 @@ Swimlanes.helpers({
|
|||
swimlaneId: { $in: [this._id, ''] },
|
||||
archived: false,
|
||||
},
|
||||
{ sort: { updatedAt: -1 } },
|
||||
{ sort: { modifiedAt: -1 } },
|
||||
);
|
||||
},
|
||||
draggableLists() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,16 @@ const isSandstorm =
|
|||
Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
|
||||
Users = Meteor.users;
|
||||
|
||||
const allowedSortValues = [
|
||||
'-modifiedAt',
|
||||
'modifiedAt',
|
||||
'-title',
|
||||
'title',
|
||||
'-sort',
|
||||
'sort',
|
||||
];
|
||||
const defaultSortBy = allowedSortValues[0];
|
||||
|
||||
/**
|
||||
* A User in wekan
|
||||
*/
|
||||
|
|
@ -191,6 +201,15 @@ Users.attachSchema(
|
|||
'board-view-cal',
|
||||
],
|
||||
},
|
||||
'profile.listSortBy': {
|
||||
/**
|
||||
* default sort list for user
|
||||
*/
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: defaultSortBy,
|
||||
allowedValues: allowedSortValues,
|
||||
},
|
||||
'profile.templatesBoardId': {
|
||||
/**
|
||||
* Reference to the templates board
|
||||
|
|
@ -365,6 +384,31 @@ Users.helpers({
|
|||
return _.contains(invitedBoards, boardId);
|
||||
},
|
||||
|
||||
_getListSortBy() {
|
||||
const profile = this.profile || {};
|
||||
const sortBy = profile.listSortBy || defaultSortBy;
|
||||
const keyPattern = /^(-{0,1})(.*$)/;
|
||||
const ret = [];
|
||||
if (keyPattern.exec(sortBy)) {
|
||||
ret[0] = RegExp.$2;
|
||||
ret[1] = RegExp.$1 ? -1 : 1;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
hasSortBy() {
|
||||
// if use doesn't have dragHandle, then we can let user to choose sort list by different order
|
||||
return !this.hasShowDesktopDragHandles();
|
||||
},
|
||||
getListSortBy() {
|
||||
return this._getListSortBy()[0];
|
||||
},
|
||||
getListSortTypes() {
|
||||
return allowedSortValues;
|
||||
},
|
||||
getListSortByDirection() {
|
||||
return this._getListSortBy()[1];
|
||||
},
|
||||
|
||||
hasTag(tag) {
|
||||
const { tags = [] } = this.profile || {};
|
||||
return _.contains(tags, tag);
|
||||
|
|
@ -485,6 +529,13 @@ Users.mutations({
|
|||
else this.addTag(tag);
|
||||
},
|
||||
|
||||
setListSortBy(value) {
|
||||
return {
|
||||
$set: {
|
||||
'profile.listSortBy': value,
|
||||
},
|
||||
};
|
||||
},
|
||||
toggleDesktopHandles(value = false) {
|
||||
return {
|
||||
$set: {
|
||||
|
|
@ -569,6 +620,10 @@ Meteor.methods({
|
|||
Users.update(userId, { $set: { username } });
|
||||
}
|
||||
},
|
||||
setListSortBy(value) {
|
||||
check(value, String);
|
||||
Meteor.user().setListSortBy(value);
|
||||
},
|
||||
toggleDesktopDragHandles() {
|
||||
const user = Meteor.user();
|
||||
user.toggleDesktopHandles(user.hasShowDesktopDragHandles());
|
||||
|
|
@ -800,6 +855,9 @@ if (Meteor.isServer) {
|
|||
if (Meteor.isServer) {
|
||||
// Let mongoDB ensure username unicity
|
||||
Meteor.startup(() => {
|
||||
allowedSortValues.forEach(value => {
|
||||
Lists._collection._ensureIndex(value);
|
||||
});
|
||||
Users._collection._ensureIndex({ modifiedAt: -1 });
|
||||
Users._collection._ensureIndex(
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue