From 2400c910135dbcdddd82954951fc3a970748af55 Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 10:48:44 +0300 Subject: [PATCH 1/9] Add sort field to boards model --- models/boards.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models/boards.js b/models/boards.js index 35ee1a36c..fba690a7c 100644 --- a/models/boards.js +++ b/models/boards.js @@ -493,6 +493,14 @@ Boards.attachSchema( type: String, defaultValue: 'board', }, + sort: { + /** + * Sort value + */ + type: Number, + decimal: true, + defaultValue: -1, + }, }), ); From 9f396e9038712e0223cbd47b7bc14253610f9af9 Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 10:51:58 +0300 Subject: [PATCH 2/9] Add a migration to add a sort field to the boards model --- server/migrations.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/migrations.js b/server/migrations.js index b44899878..21b54bdac 100644 --- a/server/migrations.js +++ b/server/migrations.js @@ -1033,3 +1033,15 @@ Migrations.add('add-description-text-allowed', () => { noValidateMulti, ); }); + +Migrations.add('add-sort-field-to-boards', () => { + Boards.find().forEach((board, index) => { + if (!board.hasOwnProperty('sort')) { + Boards.direct.update( + board._id, + { $set: { sort: index } }, + noValidate + ); + } + }); +}); From 10fcc19b7f9307e71f01b6abca055806d69f7d4e Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 12:30:21 +0300 Subject: [PATCH 3/9] Add sortDefault helper for sorting boards --- client/components/boards/boardArchive.js | 2 +- client/components/boards/boardsList.js | 16 ++++------ client/components/cards/cardDetails.js | 6 ++-- client/components/lists/listBody.js | 4 +-- .../components/rules/actions/boardActions.js | 2 +- client/components/settings/settingBody.js | 2 +- client/components/sidebar/sidebar.js | 6 ++-- models/boards.js | 9 ++++-- models/users.js | 30 ++++++++++++++----- server/publications/boards.js | 4 ++- 10 files changed, 50 insertions(+), 31 deletions(-) diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js index d3e65bd8d..9f4d60a18 100644 --- a/client/components/boards/boardArchive.js +++ b/client/components/boards/boardArchive.js @@ -7,7 +7,7 @@ BlazeComponent.extendComponent({ return Boards.find( { archived: true }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ } }, ); }, diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index 65bed16ad..aabc98e8a 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -7,8 +7,8 @@ Template.boardListHeaderBar.events({ }); Template.boardListHeaderBar.helpers({ - title(){ - return FlowRouter.getRouteName() == 'home' ? 'my-boards' :'public'; + title() { + return FlowRouter.getRouteName() == 'home' ? 'my-boards' : 'public'; }, templatesBoardId() { return Meteor.user() && Meteor.user().getTemplatesBoardId(); @@ -27,16 +27,12 @@ BlazeComponent.extendComponent({ let query = { archived: false, type: 'board', - } + }; if (FlowRouter.getRouteName() == 'home') - query['members.userId'] = Meteor.userId() - else - query.permission = 'public' + query['members.userId'] = Meteor.userId(); + else query.permission = 'public'; - return Boards.find( - query, - { sort: ['title'] }, - ); + return Boards.find(query, { sort: { sort: 1 /* boards default sorting */ } }); }, isStarred() { const user = Meteor.user(); diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 9d31fc60d..ce504146e 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -727,7 +727,7 @@ BlazeComponent.extendComponent({ _id: { $ne: Meteor.user().getTemplatesBoardId() }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -903,7 +903,7 @@ BlazeComponent.extendComponent({ }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -974,7 +974,7 @@ BlazeComponent.extendComponent({ } } }, - 'click .js-delete': Popup.afterConfirm('cardDelete', function () { + 'click .js-delete': Popup.afterConfirm('cardDelete', function() { Popup.close(); Cards.remove(this._id); Utils.goBoardId(this.boardId); diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 03f88f638..88f88db0c 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -411,7 +411,7 @@ BlazeComponent.extendComponent({ type: 'board', }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; @@ -597,7 +597,7 @@ BlazeComponent.extendComponent({ type: 'board', }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; diff --git a/client/components/rules/actions/boardActions.js b/client/components/rules/actions/boardActions.js index c2f2375a5..02910cc10 100644 --- a/client/components/rules/actions/boardActions.js +++ b/client/components/rules/actions/boardActions.js @@ -11,7 +11,7 @@ BlazeComponent.extendComponent({ }, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); return boards; diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index 319c066b2..62752084b 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -48,7 +48,7 @@ BlazeComponent.extendComponent({ 'members.isAdmin': true, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 78b47a483..11471c2f3 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -510,7 +510,7 @@ BlazeComponent.extendComponent({ 'members.userId': Meteor.userId(), }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, @@ -589,7 +589,7 @@ BlazeComponent.extendComponent({ 'subtext-with-parent', 'no-parent', ]; - options.forEach(function (element) { + options.forEach(function(element) { if (element !== value) { $(`#${element} ${MCB}`).toggleClass(CKCLS, false); $(`#${element}`).toggleClass(CKCLS, false); @@ -688,7 +688,7 @@ BlazeComponent.extendComponent({ 'members.userId': Meteor.userId(), }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ); }, diff --git a/models/boards.js b/models/boards.js index fba690a7c..fdb07f894 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1474,7 +1474,7 @@ if (Meteor.isServer) { 'members.userId': paramUserId, }, { - sort: ['title'], + sort: { sort: 1 /* boards default sorting */ }, }, ).map(function(board) { return { @@ -1504,7 +1504,12 @@ if (Meteor.isServer) { Authentication.checkUserId(req.userId); JsonRoutes.sendResult(res, { code: 200, - data: Boards.find({ permission: 'public' }).map(function(doc) { + data: Boards.find( + { permission: 'public' }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ).map(function(doc) { return { _id: doc._id, title: doc.title, diff --git a/models/users.js b/models/users.js index a9eeb38b6..f4b7329a8 100644 --- a/models/users.js +++ b/models/users.js @@ -386,12 +386,20 @@ if (Meteor.isClient) { Users.helpers({ boards() { - return Boards.find({ 'members.userId': this._id }); + return Boards.find( + { 'members.userId': this._id }, + { sort: { sort: 1 /* boards default sorting */ } }, + ); }, starredBoards() { const { starredBoards = [] } = this.profile || {}; - return Boards.find({ archived: false, _id: { $in: starredBoards } }); + return Boards.find( + { archived: false, _id: { $in: starredBoards } }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ); }, hasStarred(boardId) { @@ -401,7 +409,12 @@ Users.helpers({ invitedBoards() { const { invitedBoards = [] } = this.profile || {}; - return Boards.find({ archived: false, _id: { $in: invitedBoards } }); + return Boards.find( + { archived: false, _id: { $in: invitedBoards } }, + { + sort: { sort: 1 /* boards default sorting */ }, + }, + ); }, isInvitedTo(boardId) { @@ -1292,10 +1305,13 @@ if (Meteor.isServer) { let data = Meteor.users.findOne({ _id: id }); if (data !== undefined) { if (action === 'takeOwnership') { - data = Boards.find({ - 'members.userId': id, - 'members.isAdmin': true, - }).map(function(board) { + data = Boards.find( + { + 'members.userId': id, + 'members.isAdmin': true, + }, + { sort: { sort: 1 /* boards default sorting */ } }, + ).map(function(board) { if (board.hasMember(req.userId)) { board.removeMember(req.userId); } diff --git a/server/publications/boards.js b/server/publications/boards.js index 6fbd98605..b54f27a83 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -36,6 +36,7 @@ Meteor.publish('boards', function() { permission: 1, type: 1, }, + sort: { sort: 1 /* boards default sorting */ }, }, ); }); @@ -61,6 +62,7 @@ Meteor.publish('archivedBoards', function() { slug: 1, title: 1, }, + sort: { sort: 1 /* boards default sorting */ }, }, ); }); @@ -90,7 +92,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) { $or, // Sort required to ensure oplog usage }, - { limit: 1, sort: { _id: 1 } }, + { limit: 1, sort: { sort: 1 /* boards default sorting */, _id: 1 } }, ), function(boardId, board) { this.cursor(Lists.find({ boardId, archived: isArchived })); From b3efa71d1373ede679282be35cba947dc2b868ff Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 12:38:56 +0300 Subject: [PATCH 4/9] Add move function to boards mutations --- models/boards.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/models/boards.js b/models/boards.js index fdb07f894..4c2d96da8 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1194,6 +1194,10 @@ Boards.mutations({ setPresentParentTask(presentParentTask) { return { $set: { presentParentTask } }; }, + + move(sortIndex) { + return { $set: { sort: sortIndex } }; + }, }); function boardRemover(userId, doc) { From ef5f38f431dbedbecc81135702764cdc08797177 Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 12:45:04 +0300 Subject: [PATCH 5/9] Make boards sortable --- client/components/boards/boardsList.jade | 4 +- client/components/boards/boardsList.js | 54 ++++++++++++++++++++++++ client/components/boards/boardsList.styl | 15 ++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index 460866931..bbce1d6f9 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -1,10 +1,10 @@ template(name="boardList") .wrapper - ul.board-list.clearfix + ul.board-list.clearfix.js-boards li.js-add-board a.board-list-item.label {{_ 'add-board'}} each boards - li(class="{{#if isStarred}}starred{{/if}}" class=colorClass) + li(class="{{#if isStarred}}starred{{/if}}" class=colorClass).js-board if isInvited .board-list-item span.details diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index aabc98e8a..d2d444073 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -1,4 +1,5 @@ const subManager = new SubsManager(); +const { calculateIndex, enableClickOnTouch } = Utils; Template.boardListHeaderBar.events({ 'click .js-open-archived-board'() { @@ -23,6 +24,59 @@ BlazeComponent.extendComponent({ Meteor.subscribe('setting'); }, + onRendered() { + const self = this; + function userIsAllowedToMove() { + return Meteor.user(); + } + + const itemsSelector = '.js-board:not(.placeholder)'; + + const $boards = this.$('.js-boards'); + $boards.sortable({ + connectWith: '.js-boards', + tolerance: 'pointer', + appendTo: '.board-list', + helper: 'clone', + distance: 7, + items: itemsSelector, + placeholder: 'board-wrapper placeholder', + start(evt, ui) { + ui.helper.css('z-index', 1000); + ui.placeholder.height(ui.helper.height()); + EscapeActions.executeUpTo('popup-close'); + }, + stop(evt, ui) { + // To attribute the new index number, we need to get the DOM element + // of the previous and the following card -- if any. + const prevCardDom = ui.item.prev('.js-board').get(0); + const nextCardDom = ui.item.next('.js-board').get(0); + const sortIndex = calculateIndex(prevCardDom, nextCardDom, 1); + + const boardDomElement = ui.item.get(0); + const board = Blaze.getData(boardDomElement); + // Normally the jquery-ui sortable library moves the dragged DOM element + // to its new position, which disrupts Blaze reactive updates mechanism + // (especially when we move the last card of a list, or when multiple + // users move some cards at the same time). To prevent these UX glitches + // we ask sortable to gracefully cancel the move, and to put back the + // DOM in its initial state. The card move is then handled reactively by + // Blaze with the below query. + $boards.sortable('cancel'); + + board.move(sortIndex.base); + }, + }); + + // ugly touch event hotfix + enableClickOnTouch(itemsSelector); + + // Disable drag-dropping if the current user is not a board member or is comment only + this.autorun(() => { + $boards.sortable('option', 'disabled', !userIsAllowedToMove()); + }); + }, + boards() { let query = { archived: false, diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl index ae366e833..d12a03371 100644 --- a/client/components/boards/boardsList.styl +++ b/client/components/boards/boardsList.styl @@ -11,6 +11,19 @@ $spaceBetweenTiles = 16px box-sizing: border-box position: relative + &.placeholder:after + content: ''; + display: block; + background: darken(white, 20%) + border-radius: 3px; + height: 106px; + margin: 8px; + + &.ui-sortable-helper + cursor: grabbing + transform: rotate(4deg) + display: block !important + &.starred .fa-star, .fa-star-o @@ -183,7 +196,7 @@ $spaceBetweenTiles = 16px overflow: scroll li - width: 50% + width: 50% .board-list-item overflow: hidden From e354715a9d0fdd45cdcf6d9beec35315a200ec6f Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 15:51:29 +0300 Subject: [PATCH 6/9] Remove sorting by _id --- server/publications/boards.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/publications/boards.js b/server/publications/boards.js index b54f27a83..aa6d64d8e 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -92,7 +92,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) { $or, // Sort required to ensure oplog usage }, - { limit: 1, sort: { sort: 1 /* boards default sorting */, _id: 1 } }, + { limit: 1, sort: { sort: 1 /* boards default sorting */ } }, ), function(boardId, board) { this.cursor(Lists.find({ boardId, archived: isArchived })); From 3565ff2700e7080e509017617ad2a767244cabc6 Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 15:52:06 +0300 Subject: [PATCH 7/9] Export sort field --- server/publications/boards.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/publications/boards.js b/server/publications/boards.js index aa6d64d8e..b80a6b23a 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -35,6 +35,7 @@ Meteor.publish('boards', function() { members: 1, permission: 1, type: 1, + sort: 1, }, sort: { sort: 1 /* boards default sorting */ }, }, From 1a065ff351b5c37536d73cc3d46b736fe310e32c Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 15:52:43 +0300 Subject: [PATCH 8/9] Refactor variable names --- client/components/boards/boardsList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index d2d444073..c700084fc 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -49,9 +49,9 @@ BlazeComponent.extendComponent({ stop(evt, ui) { // To attribute the new index number, we need to get the DOM element // of the previous and the following card -- if any. - const prevCardDom = ui.item.prev('.js-board').get(0); - const nextCardDom = ui.item.next('.js-board').get(0); - const sortIndex = calculateIndex(prevCardDom, nextCardDom, 1); + const prevBoardDom = ui.item.prev('.js-board').get(0); + const nextBoardBom = ui.item.next('.js-board').get(0); + const sortIndex = calculateIndex(prevBoardDom, nextBoardBom, 1); const boardDomElement = ui.item.get(0); const board = Blaze.getData(boardDomElement); From b42d8346cda99258f4ab5689ebd02fdc7c2e85c3 Mon Sep 17 00:00:00 2001 From: boeserwolf Date: Sun, 19 Apr 2020 15:53:13 +0300 Subject: [PATCH 9/9] Insert new boards at last position --- models/boards.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/models/boards.js b/models/boards.js index 4c2d96da8..170ebc5a2 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1295,6 +1295,14 @@ if (Meteor.isServer) { }); } +// Insert new board at last position in sort order. +Boards.before.insert((userId, doc) => { + const lastBoard = Boards.findOne({ sort: { $exists: true } }, { sort: { sort: -1 } }); + if (lastBoard && typeof lastBoard.sort !== 'undefined') { + doc.sort = lastBoard.sort + 1; + } +}); + if (Meteor.isServer) { // Let MongoDB ensure that a member is not included twice in the same board Meteor.startup(() => {