Add Feature: allow user to sort Lists in Board by his own preference, boardadmin can star list

This commit is contained in:
Sam X. Chen 2019-10-18 16:44:09 -04:00
parent 2737d6b23f
commit bc2a20f04e
15 changed files with 272 additions and 14 deletions

View file

@ -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

View file

@ -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');

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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);

View file

@ -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'}}"

View file

@ -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()

View file

@ -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

View file

@ -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",

View file

@ -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() {

View file

@ -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(
{

View file

@ -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()) {

View file

@ -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() {

View file

@ -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(
{