Pārlūkot izejas kodu

Improve the multi-selection experience

New features:
- select all filtered cards
- assign or unassign a member to selected cards
- archive selected cards

This commit also fix the card sort indexes calculation when a multi-
selection is drag-dropped.
Maxime Quandalle 10 gadi atpakaļ
vecāks
revīzija
5478fc93db

+ 2 - 1
client/components/boards/boardBody.styl

@@ -32,7 +32,8 @@ position()
     &.is-dragging-active
 
       .list-composer,
-      .open-minicard-composer
+      .open-minicard-composer,
+      .minicard-wrapper.is-checked
         display: none
 
   .lists

+ 2 - 6
client/components/boards/boardHeader.jade

@@ -21,24 +21,20 @@ template(name="headerBoard")
         title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
         class="{{#if Filter.isActive}}emphasis{{/if}}")
       i.fa.fa-filter
+      span {{#if Filter.isActive}}{{_ 'filter-on'}}{{else}}{{_ 'filter'}}{{/if}}
       if Filter.isActive
-        span {{_ 'filter-on'}}
         a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
           i.fa.fa-times-thin
-      else
-        span {{_ 'filter'}}
 
     if currentUser.isBoardMember
       a.board-header-btn.js-multiselection-activate(
           title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
           class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
         i.fa.fa-check-square-o
+        span Multi-Selection {{#if MultiSelection.isActive}}is on{{/if}}
         if MultiSelection.isActive
-          span Multi-Selection is on
           a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
             i.fa.fa-times-thin
-        else
-          span Multi-Selection
 
     .separator
     a.board-header-btn.js-open-board-menu

+ 1 - 0
client/components/boards/colors.styl

@@ -19,6 +19,7 @@ setBoardColor(color)
     background-color: darken(color, 20%)
 
   &.pop-over .pop-over-list li a:hover,
+  .sidebar .sidebar-content .sidebar-btn:hover,
   .sidebar-list li a:hover
     background-color: lighten(color, 10%)
 

+ 4 - 2
client/components/lists/body.js

@@ -27,10 +27,12 @@ BlazeComponent.extendComponent({
     var title = textarea.val();
     var position = Blaze.getData(evt.currentTarget).position;
     var sortIndex;
+    var firstCard = this.find('.js-minicard:first');
+    var lastCard = this.find('.js-minicard:last');
     if (position === 'top') {
-      sortIndex = Utils.getSortIndex(null, this.find('.js-minicard:first'));
+      sortIndex = Utils.calculateIndex(null, firstCard).base;
     } else if (position === 'bottom') {
-      sortIndex = Utils.getSortIndex(this.find('.js-minicard:last'), null);
+      sortIndex = Utils.calculateIndex(lastCard, null).base;
     }
 
     if ($.trim(title)) {

+ 8 - 7
client/components/lists/main.js

@@ -56,22 +56,23 @@ BlazeComponent.extendComponent({
       stop: function(evt, ui) {
         // To attribute the new index number, we need to get the dom element
         // of the previous and the following card -- if any.
-        var cardDomElement = ui.item.get(0);
-        var prevCardDomElement = ui.item.prev('.js-minicard').get(0);
-        var nextCardDomElement = ui.item.next('.js-minicard').get(0);
-        var sort = Utils.getSortIndex(prevCardDomElement, nextCardDomElement);
+        var prevCardDom = ui.item.prev('.js-minicard').get(0);
+        var nextCardDom = ui.item.next('.js-minicard').get(0);
+        var nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
+        var sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
         var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
 
         if (MultiSelection.isActive()) {
-          Cards.find(MultiSelection.getMongoSelector()).forEach(function(c) {
+          Cards.find(MultiSelection.getMongoSelector()).forEach(function(c, i) {
             Cards.update(c._id, {
               $set: {
                 listId: listId,
-                sort: sort
+                sort: sortIndex.base + i * sortIndex.increment
               }
             });
           });
         } else {
+          var cardDomElement = ui.item.get(0);
           var cardId = Blaze.getData(cardDomElement)._id;
           Cards.update(cardId, {
             $set: {
@@ -79,7 +80,7 @@ BlazeComponent.extendComponent({
               // XXX Using the same sort index for multiple cards is
               // unacceptable. Keep that only until we figure out if we want to
               // refactor the whole sorting mecanism or do something more basic.
-              sort: sort
+              sort: sortIndex.base
             }
           });
         }

+ 13 - 0
client/components/sidebar/sidebar.styl

@@ -51,6 +51,19 @@
         .fa.fa-check
           margin: 0 4px
 
+    .sidebar-btn
+      display: block
+      margin: 5px 0
+      padding: 10px
+      border-radius: 3px
+      background: darken(white, 10%)
+
+      &:hover *
+        color: white
+
+      i.fa
+        margin-right: 10px
+
 .board-sidebar
   width: 248px
   right: -@width

+ 46 - 19
client/components/sidebar/sidebarFilters.jade

@@ -1,7 +1,7 @@
 //-
-  XXX There is a *lot* of code duplication in the above templates and in the
+  XXX There is a *lot* of code duplication in the below templates and in the
   corresponding JavaScript components. We will probably need the upcoming #let
-  and #each x in y constructors.
+  and #each x in y constructors to fix this.
 
 template(name="filterSidebar")
   ul.sidebar-list
@@ -16,22 +16,27 @@ template(name="filterSidebar")
               span.quiet {{_ "label-default" color}}
           if Filter.labelIds.isSelected _id
             i.fa.fa-check
+  hr
+  ul.sidebar-list
+    each currentBoard.members
+      if isActive
+        with getUser userId
+          li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
+            a.name.js-toogle-member-filter
+              +userAvatar(userId=this._id)
+              span.sidebar-list-item-description
+                = profile.name
+                | (<span class="username">{{ username }}</span>)
+              if Filter.members.isSelected _id
+                i.fa.fa-check
+  if Filter.isActive
     hr
-    ul.sidebar-list
-      each currentBoard.members
-        if isActive
-          with getUser userId
-            li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
-              a.name.js-toogle-member-filter
-                +userAvatar(userId=this._id)
-                span.sidebar-list-item-description
-                  = profile.name
-                  | (<span class="username">{{ username }}</span>)
-                if Filter.members.isSelected _id
-                  i.fa.fa-check
-    hr
-    a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}")
-      | {{_ 'filter-clear'}}
+    a.sidebar-btn.js-clear-all
+      i.fa.fa-filter
+      span {{_ 'filter-clear'}}
+    a.sidebar-btn.js-filter-to-selection
+      i.fa.fa-check-square-o
+      span Filter to selection
 
 template(name="multiselectionSidebar")
   ul.sidebar-list
@@ -48,10 +53,32 @@ template(name="multiselectionSidebar")
             i.fa.fa-check
           else if someSelectedElementHave 'label' _id
             i.fa.fa-ellipsis-h
-  //-
-    XXX We should be able to assign a member to the list of selected cards.
+  hr
+  ul.sidebar-list
+    each currentBoard.members
+      if isActive
+        with getUser userId
+          li(class="{{#if Filter.members.isSelected _id}}active{{/if}}")
+            a.name.js-toogle-member-multiselection
+              +userAvatar(userId=this._id)
+              span.sidebar-list-item-description
+                = profile.name
+                | (<span class="username">{{ username }}</span>)
+              if allSelectedElementHave 'member' _id
+                i.fa.fa-check
+              else if someSelectedElementHave 'member' _id
+                i.fa.fa-ellipsis-h
+  hr
+  a.sidebar-btn.js-archive-selection
+    i.fa.fa-archive
+    span Archive selection
 
 template(name="disambiguateMultiLabelPopup")
   p What do you want to do?
   button.wide.js-remove-label Remove the label
   button.wide.js-add-label Add the label
+
+template(name="disambiguateMultiMemberPopup")
+  p What do you want to do?
+  button.wide.js-unassign-member Unassign member
+  button.wide.js-assign-member Assign member

+ 50 - 8
client/components/sidebar/sidebarFilters.js

@@ -5,19 +5,26 @@ BlazeComponent.extendComponent({
 
   events: function() {
     return [{
-      'click .js-toggle-label-filter': function(event) {
+      'click .js-toggle-label-filter': function(evt) {
+        evt.preventDefault();
         Filter.labelIds.toogle(this.currentData()._id);
         Filter.resetExceptions();
-        event.preventDefault();
       },
-      'click .js-toogle-member-filter': function(event) {
+      'click .js-toogle-member-filter': function(evt) {
+        evt.preventDefault();
         Filter.members.toogle(this.currentData()._id);
         Filter.resetExceptions();
-        event.preventDefault();
       },
-      'click .js-clear-all': function(event) {
+      'click .js-clear-all': function(evt) {
+        evt.preventDefault();
         Filter.reset();
-        event.preventDefault();
+      },
+      'click .js-filter-to-selection': function(evt) {
+        evt.preventDefault();
+        var selectedCards = Cards.find(Filter.mongoSelector()).map(function(c) {
+          return c._id;
+        });
+        MultiSelection.add(selectedCards);
       }
     }];
   }
@@ -57,7 +64,7 @@ BlazeComponent.extendComponent({
 
   events: function() {
     return [{
-      'click .js-toggle-label-multiselection': function(evt, tpl) {
+      'click .js-toggle-label-multiselection': function(evt) {
         var labelId = this.currentData()._id;
         var mappedSelection = this.mapSelection('label', labelId);
         var operation;
@@ -69,7 +76,7 @@ BlazeComponent.extendComponent({
           var popup = Popup.open('disambiguateMultiLabel');
           // XXX We need to have a better integration between the popup and the
           // UI components systems.
-          return popup.call(this.currentData(), evt, tpl);
+          return popup.call(this.currentData(), evt);
         }
 
         var query = {};
@@ -77,6 +84,30 @@ BlazeComponent.extendComponent({
           labelIds: labelId
         };
         updateSelectedCards(query);
+      },
+      'click .js-toogle-member-multiselection': function(evt) {
+        var memberId = this.currentData()._id;
+        var mappedSelection = this.mapSelection('member', memberId);
+        var operation;
+        if (_.every(mappedSelection))
+          operation = '$pull';
+        else if (_.every(mappedSelection, function(bool) { return ! bool; }))
+          operation = '$addToSet';
+        else {
+          var popup = Popup.open('disambiguateMultiMember');
+          // XXX We need to have a better integration between the popup and the
+          // UI components systems.
+          return popup.call(this.currentData(), evt);
+        }
+
+        var query = {};
+        query[operation] = {
+          members: memberId
+        };
+        updateSelectedCards(query);
+      },
+      'click .js-archive-selection': function() {
+        updateSelectedCards({$set: {archived: true}});
       }
     }];
   }
@@ -92,3 +123,14 @@ Template.disambiguateMultiLabelPopup.events({
     Popup.close();
   }
 });
+
+Template.disambiguateMultiMemberPopup.events({
+  'click .js-unassign-member': function() {
+    updateSelectedCards({$pull: {members: this._id}});
+    Popup.close();
+  },
+  'click .js-assign-member': function() {
+    updateSelectedCards({$addToSet: {members: this._id}});
+    Popup.close();
+  }
+});

+ 1 - 1
client/lib/escapeActions.js

@@ -153,6 +153,6 @@ Mousetrap.bindGlobal('esc', function() {
 $(document).on('click', function(evt) {
   if (evt.which === 1 &&
     $(evt.target).closest('a,button,.is-editable').length === 0) {
-    EscapeActions.clickExecute(evt, 'detailsPane');
+    EscapeActions.clickExecute(evt, 'multiselection');
   }
 });

+ 9 - 3
client/lib/multiSelection.js

@@ -72,17 +72,21 @@ MultiSelection = {
     return this._isActive.get();
   },
 
+  count: function() {
+    return Cards.find(this.getMongoSelector()).count();
+  },
+
   isEmpty: function() {
-    return this._selectedCards.get().length === 0;
+    return this.count() === 0;
   },
 
   activate: function() {
     if (! this.isActive()) {
       EscapeActions.executeUpTo('detailsPane');
       this._isActive.set(true);
-      Sidebar.setView(this.sidebarView);
       Tracker.flush();
     }
+    Sidebar.setView(this.sidebarView);
   },
 
   disable: function() {
@@ -152,5 +156,7 @@ Blaze.registerHelper('MultiSelection', MultiSelection);
 
 EscapeActions.register('multiselection',
   function() { MultiSelection.disable(); },
-  function() { return MultiSelection.isActive(); }
+  function() { return MultiSelection.isActive(); }, {
+    noClickEscapeOn: '.js-minicard,.js-board-sidebar-content'
+  }
 );

+ 8 - 5
client/lib/utils.js

@@ -37,23 +37,26 @@ Utils = {
   },
 
   // Determine the new sort index
-  getSortIndex: function(prevCardDomElement, nextCardDomElement) {
+  calculateIndex: function(prevCardDomElement, nextCardDomElement, nCards) {
+    nCards = nCards || 1;
+
     // If we drop the card to an empty column
     if (! prevCardDomElement && ! nextCardDomElement) {
-      return 0;
+      return {base: 0, increment: 1};
     // If we drop the card in the first position
     } else if (! prevCardDomElement) {
-      return Blaze.getData(nextCardDomElement).sort - 1;
+      return {base: Blaze.getData(nextCardDomElement).sort - 1, increment: -1};
     // If we drop the card in the last position
     } else if (! nextCardDomElement) {
-      return Blaze.getData(prevCardDomElement).sort + 1;
+      return {base: Blaze.getData(prevCardDomElement).sort + 1, increment: 1};
     }
     // In the general case take the average of the previous and next element
     // sort indexes.
     else {
       var prevSortIndex = Blaze.getData(prevCardDomElement).sort;
       var nextSortIndex = Blaze.getData(nextCardDomElement).sort;
-      return (prevSortIndex + nextSortIndex) / 2;
+      var increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
+      return {base: prevSortIndex + increment, increment: increment};
     }
   }
 };

+ 2 - 1
i18n/en.i18n.json

@@ -190,5 +190,6 @@
     "changeAvatarPopup-title": "Change Avatar",
     "changePasswordPopup-title": "Change Password",
     "cardDetailsActionsPopup-title": "Card Actions",
-    "disambiguateMultiLabelPopup-title": "Disambiguate Label Action"
+    "disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
+    "disambiguateMultiMemberPopup-title": "Disambiguate Member Action"
 }