Browse Source

Merge branch 'boeserwolf-feature-sortable-boards'

Lauri Ojansivu 5 years ago
parent
commit
8cb838c0de

+ 1 - 1
client/components/boards/boardArchive.js

@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
     return Boards.find(
       { archived: true },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ }
       },
     );
   },

+ 2 - 2
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

+ 61 - 11
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'() {
@@ -7,8 +8,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();
@@ -23,20 +24,69 @@ 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 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);
+        // 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,
       type: 'board',
-    }
+    };
     if (FlowRouter.getRouteName() == 'home')
-      query['members.userId'] = Meteor.userId()
-    else
-      query.permission = 'public'
-
-    return Boards.find(
-      query,
-      { sort: ['title'] },
-    );
+      query['members.userId'] = Meteor.userId();
+    else query.permission = 'public';
+
+    return Boards.find(query, { sort: { sort: 1 /* boards default sorting */ } });
   },
   isStarred() {
     const user = Meteor.user();

+ 14 - 1
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

+ 3 - 3
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);

+ 2 - 2
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;

+ 1 - 1
client/components/rules/actions/boardActions.js

@@ -11,7 +11,7 @@ BlazeComponent.extendComponent({
         },
       },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
     );
     return boards;

+ 1 - 1
client/components/settings/settingBody.js

@@ -48,7 +48,7 @@ BlazeComponent.extendComponent({
         'members.isAdmin': true,
       },
       {
-        sort: ['title'],
+        sort: { sort: 1 /* boards default sorting */ },
       },
     );
   },

+ 3 - 3
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 */ },
       },
     );
   },

+ 27 - 2
models/boards.js

@@ -493,6 +493,14 @@ Boards.attachSchema(
       type: String,
       defaultValue: 'board',
     },
+    sort: {
+      /**
+       * Sort value
+       */
+      type: Number,
+      decimal: true,
+      defaultValue: -1,
+    },
   }),
 );
 
@@ -1186,6 +1194,10 @@ Boards.mutations({
   setPresentParentTask(presentParentTask) {
     return { $set: { presentParentTask } };
   },
+
+  move(sortIndex) {
+    return { $set: { sort: sortIndex } };
+  },
 });
 
 function boardRemover(userId, doc) {
@@ -1283,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(() => {
@@ -1466,7 +1486,7 @@ if (Meteor.isServer) {
           'members.userId': paramUserId,
         },
         {
-          sort: ['title'],
+          sort: { sort: 1 /* boards default sorting */ },
         },
       ).map(function(board) {
         return {
@@ -1496,7 +1516,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,

+ 23 - 7
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);
             }

+ 12 - 0
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
+      );
+    }
+  });
+});

+ 4 - 1
server/publications/boards.js

@@ -35,7 +35,9 @@ Meteor.publish('boards', function() {
         members: 1,
         permission: 1,
         type: 1,
+        sort: 1,
       },
+      sort: { sort: 1 /* boards default sorting */ },
     },
   );
 });
@@ -61,6 +63,7 @@ Meteor.publish('archivedBoards', function() {
         slug: 1,
         title: 1,
       },
+      sort: { sort: 1 /* boards default sorting */ },
     },
   );
 });
@@ -90,7 +93,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 */ } },
     ),
     function(boardId, board) {
       this.cursor(Lists.find({ boardId, archived: isArchived }));