diff options
-rw-r--r-- | client/components/boards/boardHeader.jade | 18 | ||||
-rw-r--r-- | client/components/boards/boardHeader.js | 102 | ||||
-rw-r--r-- | client/components/lists/listHeader.jade | 4 | ||||
-rw-r--r-- | client/components/lists/listHeader.js | 18 | ||||
-rw-r--r-- | client/components/sidebar/sidebarFilters.jade | 3 | ||||
-rw-r--r-- | client/components/sidebar/sidebarFilters.js | 4 | ||||
-rw-r--r-- | client/components/swimlanes/swimlanes.jade | 2 | ||||
-rw-r--r-- | client/components/swimlanes/swimlanes.js | 5 | ||||
-rw-r--r-- | client/lib/filter.js | 20 | ||||
-rw-r--r-- | i18n/en.i18n.json | 12 | ||||
-rw-r--r-- | models/boards.js | 8 | ||||
-rw-r--r-- | models/cards.js | 6 | ||||
-rw-r--r-- | models/lists.js | 22 | ||||
-rw-r--r-- | models/swimlanes.js | 6 | ||||
-rw-r--r-- | models/users.js | 58 |
15 files changed, 273 insertions, 15 deletions
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index fe533f95..175cc2c2 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -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 diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index cb84c233..e14b1444 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -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'); diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 23ae6282..3b3a0242 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -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 diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index 5b7232cd..b524d4e0 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -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( diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 55ab213a..0a68719e 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/client/components/sidebar/sidebarFilters.jade @@ -5,6 +5,9 @@ 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 span.sidebar-list-item-description diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 3483d00c..ee76ef37 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/client/components/sidebar/sidebarFilters.js @@ -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); diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 8f07a01c..98af6d54 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -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'}}" diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index 1bfc0f79..8faad870 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -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() diff --git a/client/lib/filter.js b/client/lib/filter.js index 1ca3a280..b0695625 100644 --- a/client/lib/filter.js +++ b/client/lib/filter.js @@ -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(), - - _fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'], + lists: new AdvancedFilter(), // we need the ability to filter list by name as well + + _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 diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 58fda954..ab2ea537 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -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", diff --git a/models/boards.js b/models/boards.js index c7f93022..85a7558c 100644 --- a/models/boards.js +++ b/models/boards.js @@ -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() { diff --git a/models/cards.js b/models/cards.js index 35d596d6..27dda0ee 100644 --- a/models/cards.js +++ b/models/cards.js @@ -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( { diff --git a/models/lists.js b/models/lists.js index 9136c337..16ad434c 100644 --- a/models/lists.js +++ b/models/lists.js @@ -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()) { diff --git a/models/swimlanes.js b/models/swimlanes.js index 4cd35574..831f1eff 100644 --- a/models/swimlanes.js +++ b/models/swimlanes.js @@ -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() { diff --git a/models/users.js b/models/users.js index 93fb409e..83a224ba 100644 --- a/models/users.js +++ b/models/users.js @@ -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( { |