2
0
Эх сурвалжийг харах

Implement multi-selection

The UI and the internal APIs are still rough around the edges but the
feature is basically working. You can now select multiple cards and
move them together or (un|)assign them a label.
Maxime Quandalle 10 жил өмнө
parent
commit
2c0030da62
45 өөрчлөгдсөн 883 нэмэгдсэн , 933 устгасан
  1. 1 0
      .gitignore
  2. 71 71
      .jscsrc
  3. 2 1
      .jshintrc
  4. 4 1
      client/components/boards/boardBody.jade
  5. 13 6
      client/components/boards/boardBody.js
  6. 5 0
      client/components/boards/boardBody.styl
  7. 30 2
      client/components/boards/boardHeader.jade
  8. 17 4
      client/components/boards/boardHeader.js
  9. 21 3
      client/components/boards/colors.styl
  10. 4 4
      client/components/boards/router.js
  11. 0 27
      client/components/cards/details.styl
  12. 5 0
      client/components/cards/labels.styl
  13. 10 6
      client/components/cards/minicard.jade
  14. 24 1
      client/components/cards/minicard.js
  15. 66 53
      client/components/cards/minicard.styl
  16. 2 3
      client/components/cards/popups.jade
  17. 30 38
      client/components/forms/forms.styl
  18. 2 2
      client/components/forms/inlinedform.js
  19. 4 5
      client/components/lists/body.jade
  20. 7 16
      client/components/lists/body.js
  21. 43 18
      client/components/lists/main.js
  22. 4 1
      client/components/lists/main.styl
  23. 1 0
      client/components/lists/menu.jade
  24. 8 0
      client/components/lists/menu.js
  25. 2 2
      client/components/main/editor.js
  26. 3 0
      client/components/main/header.styl
  27. 0 239
      client/components/main/popup.styl
  28. 0 17
      client/components/sidebar/events.js
  29. 0 14
      client/components/sidebar/helpers.js
  30. 6 33
      client/components/sidebar/sidebar.jade
  31. 20 5
      client/components/sidebar/sidebar.js
  32. 28 2
      client/components/sidebar/sidebar.styl
  33. 57 0
      client/components/sidebar/sidebarFilters.jade
  34. 94 0
      client/components/sidebar/sidebarFilters.js
  35. 77 0
      client/components/sidebar/templates.html
  36. 0 307
      client/components/sidebar/templates.html.old
  37. 28 6
      client/config/router.js
  38. 10 1
      client/lib/filter.js
  39. 11 2
      client/lib/keyboard.js
  40. 159 0
      client/lib/multiSelection.js
  41. 2 2
      client/lib/popup.js
  42. 0 38
      client/styles/main.styl
  43. 6 0
      collections/cards.js
  44. 1 1
      collections/lists.js
  45. 5 2
      i18n/en.i18n.json

+ 1 - 0
.gitignore

@@ -3,3 +3,4 @@
 .meteor-spk
 .tx/
 *.sublime-workspace
+tmp/

+ 71 - 71
.jscsrc

@@ -1,73 +1,73 @@
 {
-    "disallowSpacesInNamedFunctionExpression": {
-        "beforeOpeningRoundBrace": true
-    },
-    "disallowSpacesInFunctionExpression": {
-        "beforeOpeningRoundBrace": true
-    },
-    "disallowSpacesInAnonymousFunctionExpression": {
-        "beforeOpeningRoundBrace": true
-    },
-    "disallowSpacesInFunctionDeclaration": {
-        "beforeOpeningRoundBrace": true
-    },
-    "disallowEmptyBlocks": true,
-    "disallowSpacesInsideArrayBrackets": true,
-    "disallowSpacesInsideParentheses": true,
-    "disallowQuotedKeysInObjects": "allButReserved",
-    "disallowSpaceAfterObjectKeys": true,
-    "disallowSpaceAfterPrefixUnaryOperators": [
-        "++",
-        "--",
-        "+",
-        "-",
-        "~"
-    ],
-    "disallowSpaceBeforePostfixUnaryOperators": true,
-    "disallowSpaceBeforeBinaryOperators": [
-        ","
-    ],
-    "disallowMixedSpacesAndTabs": true,
-    "disallowTrailingWhitespace": true,
-    "disallowTrailingComma": true,
-    "disallowYodaConditions": true,
-    "disallowKeywords": [ "with" ],
-    "disallowMultipleLineBreaks": true,
-    "disallowMultipleVarDecl": "exceptUndefined",
-    "requireSpaceBeforeBlockStatements": true,
-    "requireParenthesesAroundIIFE": true,
-    "requireSpacesInConditionalExpression": true,
-    "requireBlocksOnNewline": 1,
-    "requireCommaBeforeLineBreak": true,
-    "requireSpaceAfterPrefixUnaryOperators": [
-        "!"
-    ],
-    "requireSpaceBeforeBinaryOperators": true,
-    "requireSpaceAfterBinaryOperators": true,
-    "requireCamelCaseOrUpperCaseIdentifiers": true,
-    "requireLineFeedAtFileEnd": true,
-    "requireCapitalizedConstructors": true,
-    "requireDotNotation": true,
-    "requireSpacesInForStatement": true,
-    "requireSpaceBetweenArguments": true,
-    "requireCurlyBraces": [
-        "do"
-    ],
-    "requireSpaceAfterKeywords": [
-        "if",
-        "else",
-        "for",
-        "while",
-        "do",
-        "switch",
-        "case",
-        "return",
-        "try",
-        "catch",
-        "typeof"
-    ],
-    "validateLineBreaks": "LF",
-    "validateQuoteMarks": "'",
-    "validateIndentation": 2,
-    "maximumLineLength": 80
+  "disallowSpacesInNamedFunctionExpression": {
+    "beforeOpeningRoundBrace": true
+  },
+  "disallowSpacesInFunctionExpression": {
+    "beforeOpeningRoundBrace": true
+  },
+  "disallowSpacesInAnonymousFunctionExpression": {
+    "beforeOpeningRoundBrace": true
+  },
+  "disallowSpacesInFunctionDeclaration": {
+    "beforeOpeningRoundBrace": true
+  },
+  "disallowEmptyBlocks": true,
+  "disallowSpacesInsideArrayBrackets": true,
+  "disallowSpacesInsideParentheses": true,
+  "disallowQuotedKeysInObjects": "allButReserved",
+  "disallowSpaceAfterObjectKeys": true,
+  "disallowSpaceAfterPrefixUnaryOperators": [
+    "++",
+    "--",
+    "+",
+    "-",
+    "~"
+  ],
+  "disallowSpaceBeforePostfixUnaryOperators": true,
+  "disallowSpaceBeforeBinaryOperators": [
+    ","
+  ],
+  "disallowMixedSpacesAndTabs": true,
+  "disallowTrailingWhitespace": true,
+  "disallowTrailingComma": true,
+  "disallowYodaConditions": true,
+  "disallowKeywords": [ "with" ],
+  "disallowMultipleLineBreaks": true,
+  "disallowMultipleVarDecl": "exceptUndefined",
+  "requireSpaceBeforeBlockStatements": true,
+  "requireParenthesesAroundIIFE": true,
+  "requireSpacesInConditionalExpression": true,
+  "requireBlocksOnNewline": 1,
+  "requireCommaBeforeLineBreak": true,
+  "requireSpaceAfterPrefixUnaryOperators": [
+    "!"
+  ],
+  "requireSpaceBeforeBinaryOperators": true,
+  "requireSpaceAfterBinaryOperators": true,
+  "requireCamelCaseOrUpperCaseIdentifiers": true,
+  "requireLineFeedAtFileEnd": true,
+  "requireCapitalizedConstructors": true,
+  "requireDotNotation": true,
+  "requireSpacesInForStatement": true,
+  "requireSpaceBetweenArguments": true,
+  "requireCurlyBraces": [
+    "do"
+  ],
+  "requireSpaceAfterKeywords": [
+    "if",
+    "else",
+    "for",
+    "while",
+    "do",
+    "switch",
+    "case",
+    "return",
+    "try",
+    "catch",
+    "typeof"
+  ],
+  "validateLineBreaks": "LF",
+  "validateQuoteMarks": "'",
+  "validateIndentation": 2,
+  "maximumLineLength": 80
 }

+ 2 - 1
.jshintrc

@@ -69,9 +69,10 @@
     // Our objects
     "EscapeActions": true,
     "Filter": true,
+    "Filter": true,
     "Mixins": true,
+    "MultiSelection": true,
     "Popup": true,
-    "Filter": true,
     "Sidebar": true,
     "Utils": true,
 

+ 4 - 1
client/components/boards/boardBody.jade

@@ -8,7 +8,10 @@ template(name="board")
 template(name="boardComponent")
   if this
     .board-wrapper(class=colorClass)
-      .board-canvas(class=sidebarSize)
+      .board-canvas(
+        class=sidebarSize
+        class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
+        class="{{#if draggingActive.get}}is-dragging-active{{/if}}")
         .lists.js-lists
           each lists
             +list(this)

+ 13 - 6
client/components/boards/boardBody.js

@@ -12,14 +12,16 @@ BlazeComponent.extendComponent({
     return 'boardComponent';
   },
 
+  onCreated: function() {
+    this.draggingActive = new ReactiveVar(false);
+  },
+
   openNewListForm: function() {
     this.componentChildren('addListForm')[0].open();
   },
 
-  showNewCardForms: function(value) {
-    _.each(this.componentChildren('list'), function(listComponent) {
-      listComponent.showNewCardForm(value);
-    });
+  setIsDragging: function(bool) {
+    this.draggingActive.set(bool);
   },
 
   scrollLeft: function(position) {
@@ -79,8 +81,8 @@ BlazeComponent.extendComponent({
       helper: 'clone',
       items: '.js-list:not(.js-list-composer)',
       placeholder: 'list placeholder',
-      start: function(event, ui) {
-        $('.list.placeholder').height(ui.item.height());
+      start: function(evt, ui) {
+        ui.placeholder.height(ui.helper.height());
         Popup.close();
       },
       stop: function() {
@@ -97,6 +99,11 @@ BlazeComponent.extendComponent({
       }
     });
 
+    // Disable drag-dropping while in multi-selection mode
+    self.autorun(function() {
+      self.$(lists).sortable('option', 'disabled', MultiSelection.isActive());
+    });
+
     // If there is no data in the board (ie, no lists) we autofocus the list
     // creation form by clicking on the corresponding element.
     if (self.data().lists().count() === 0) {

+ 5 - 0
client/components/boards/boardBody.styl

@@ -19,6 +19,11 @@
     &.next-sidebar
       margin-right: 248px
 
+    &.is-dragging-active
+
+      .open-minicard-composer
+        display: none
+
 .lists
   align-items: flex-start
   display: flex

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

@@ -27,15 +27,43 @@ template(name="headerBoard")
           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
+        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
       i.board-header-btn-icon.fa.fa-cog
 
 template(name="boardMenuPopup")
+  if currentUser.isBoardMember
+    ul.pop-over-list
+      li: a Archived elements
+      li: a.js-change-board-color Change color
+      li: a Permissions
+    hr
   ul.pop-over-list
-    li: a.js-change-board-color Change color
     li: a Copy this board
-    li: a Permissions
+    //-
+      XXX Language should be handled by sandstorm, but for now display a
+      language selection link in the board menu. This link is normally present
+      in the header bar that is not displayed on sandstorm.
+    if isSandstorm
+      li: a.js-change-language {{_ 'language'}}
+  unless isSandstorm
+    if currentUser.isBoardAdmin
+      hr
+      ul.pop-over-list
+        li: a Close Board…
 
 template(name="boardVisibilityList")
   ul.pop-over-list

+ 17 - 4
client/components/boards/boardHeader.js

@@ -1,6 +1,7 @@
 Template.boardMenuPopup.events({
   'click .js-rename-board': Popup.open('boardChangeTitle'),
-  'click .js-change-board-color': Popup.open('boardChangeColor')
+  'click .js-change-board-color': Popup.open('boardChangeColor'),
+  'click .js-change-language': Popup.open('setLanguage')
 });
 
 Template.boardChangeTitlePopup.events({
@@ -24,14 +25,15 @@ BlazeComponent.extendComponent({
   },
 
   isStarred: function() {
-    var boardId = this.currentData()._id;
+    var currentBoard  = this.currentData();
     var user = Meteor.user();
-    return boardId && user && user.hasStarred(boardId);
+    return currentBoard && user && user.hasStarred(currentBoard._id);
   },
 
   // Only show the star counter if the number of star is greater than 2
   showStarCounter: function() {
-    return this.currentData().stars > 2;
+    var currentBoard = this.currentData();
+    return currentBoard && currentBoard.stars > 2;
   },
 
   events: function() {
@@ -49,6 +51,17 @@ BlazeComponent.extendComponent({
         evt.stopPropagation();
         Sidebar.setView();
         Filter.reset();
+      },
+      'click .js-multiselection-activate': function() {
+        var currentCard = Session.get('currentCard');
+        MultiSelection.activate();
+        if (currentCard) {
+          MultiSelection.add(currentCard);
+        }
+      },
+      'click .js-multiselection-reset': function(evt) {
+        evt.stopPropagation();
+        MultiSelection.disable();
       }
     }];
   }

+ 21 - 3
client/components/boards/colors.styl

@@ -1,6 +1,10 @@
 // We define a set of six board colors that we took from the FlatUI palette.
 // http://flatuicolors.com
-
+//
+// XXX Centralizing all these properties in a single file just because their
+// value is derivedform the same color, doesn't make any sense. We should create
+// a macro that would generate 6 version of a given propertie and dispatch this
+// list in the other stylus files.
 setBoardColor(color)
   &#header,
   &.sk-spinner div,
@@ -8,13 +12,16 @@ setBoardColor(color)
   .board-list & a
     background-color: color
 
-  & .minicard.is-selected .minicard-details
+  .is-selected .minicard
     border-left: 3px solid color
 
-  &.pop-over .pop-over-list li a:hover,
   button[type=submit].primary, input[type=submit].primary
     background-color: darken(color, 20%)
 
+  &.pop-over .pop-over-list li a:hover,
+  .sidebar-list li a:hover
+    background-color: lighten(color, 10%)
+
   &#header #header-quick-access ul li.current
     border-bottom: 2px solid lighten(color, 10%)
 
@@ -28,6 +35,17 @@ setBoardColor(color)
     &:hover .board-header-btn-close
       background: darken(complement(color), 20%)
 
+  .materialCheckBox.is-checked
+    border-bottom: 2px solid color
+    border-right: 2px solid color
+
+  .is-multiselection-active .multi-selection-checkbox
+    &.is-checked + .minicard
+      background: lighten(color, 90%)
+
+    &:not(.is-checked) + .minicard:hover:not(.minicard-composer)
+      background: lighten(color, 97%)
+
 .board-color-nephritis
   setBoardColor(#27AE60)
 

+ 4 - 4
client/components/boards/router.js

@@ -19,7 +19,6 @@ Router.route('/boards/:_id/:slug', {
   onAfterAction: function() {
     // XXX We probably shouldn't rely on Session
     Session.set('sidebarIsOpen', true);
-    Session.set('currentWidget', 'home');
     Session.set('menuWidgetIsOpen', false);
   },
   waitOn: function() {
@@ -37,6 +36,7 @@ Router.route('/boards/:_id/:slug', {
 Router.route('/boards/:boardId/:slug/:cardId', {
   name: 'Card',
   template: 'board',
+  noEscapeActions: true,
   onAfterAction: function() {
     Tracker.nonreactive(function() {
       if (! Session.get('currentCard') && Sidebar) {
@@ -57,7 +57,7 @@ Router.route('/boards/:boardId/:slug/:cardId', {
 });
 
 // Close the card details pane by pressing escape
-EscapeActions.register('detailedPane',
-  function() { return ! Session.equals('currentCard', null); },
-  function() { Utils.goBoardId(Session.get('currentBoard')); }
+EscapeActions.register('detailsPane',
+  function() { Utils.goBoardId(Session.get('currentBoard')); },
+  function() { return ! Session.equals('currentCard', null); }
 );

+ 0 - 27
client/components/cards/details.styl

@@ -134,33 +134,6 @@
 .card-composer
   padding-bottom: 8px
 
-.cc-controls
-  margin-top: 1px
-
-  input[type="submit"]
-    float: left
-    margin-top: 0
-    padding: 5px 18px
-
-  .icon-lg
-    float: left
-
-  .cc-opt
-    float: right
-
-.minicard-placeholder,
-.minicard.placeholder
-  background: silver
-  border: none
-  min-height: 18px
-
-  .hook
-    height: 18px
-    position: absolute
-    right: 0
-    top: 0
-    width: 18px
-
 input[type="text"].attachment-add-link-input
   float: left
   margin: 0 0 8px

+ 5 - 0
client/components/cards/labels.styl

@@ -19,6 +19,11 @@
   &:hover
     color: white
 
+  &.square
+    height: 30px
+    width: @height
+    padding: 0
+
 .card-label-green
   background-color: #3cb500
 

+ 10 - 6
client/components/cards/minicard.jade

@@ -1,7 +1,11 @@
 template(name="minicard")
-  .minicard.card.js-minicard(
-    class="{{#if isSelected}}is-selected{{/if}}")
-    a.minicard-details.clearfix.show(href=absoluteUrl)
+  a.minicard-wrapper.js-minicard(href=absoluteUrl
+    class="{{#if isSelected}}is-selected{{/if}}"
+    class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
+    if MultiSelection.isActive
+      .materialCheckBox.multi-selection-checkbox.js-toggle-multi-selection(
+        class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
+    .minicard
       if cover
         .minicard-cover.js-card-cover(style="background-image: url({{cover.url}});")
       if labels
@@ -16,12 +20,12 @@ template(name="minicard")
       .badges
         if comments.count
           .badge(title="{{_ 'card-comments-title' comments.count }}")
-            span.badge-icon.icon-sm.fa.fa-comment-o
+            span.badge-icon.fa.fa-comment-o
             .badge-text= comments.count
         if description
           .badge.badge-state-image-only(title=description)
-            span.badge-icon.icon-sm.fa.fa-align-left
+            span.badge-icon.fa.fa-align-left
         if attachments.count
           .badge
-            span.badge-icon.icon-sm.fa.fa-paperclip
+            span.badge-icon.fa.fa-paperclip
             span.badge-text= attachments.count

+ 24 - 1
client/components/cards/minicard.js

@@ -2,7 +2,6 @@
 //   'click .member': Popup.open('cardMember')
 // });
 
-
 BlazeComponent.extendComponent({
   template: function() {
     return 'minicard';
@@ -10,5 +9,29 @@ BlazeComponent.extendComponent({
 
   isSelected: function() {
     return Session.equals('currentCard', this.currentData()._id);
+  },
+
+  toggleMultiSelection: function(evt) {
+    evt.stopPropagation();
+    evt.preventDefault();
+    MultiSelection.toogle(this.currentData()._id);
+  },
+
+  clickOnMiniCard: function(evt) {
+    if (MultiSelection.isActive() || evt.shiftKey) {
+      evt.stopImmediatePropagation();
+      evt.preventDefault();
+      var methodName = evt.shiftKey ? 'toogleRange' : 'toogle';
+      MultiSelection[methodName](this.currentData()._id);
+    }
+  },
+
+  events: function() {
+    return [{
+      submit: this.addCard,
+      'click .js-toggle-multi-selection': this.toggleMultiSelection,
+      'click .js-minicard': this.clickOnMiniCard,
+      'click .open-minicard-composer': this.scrollToBottom
+    }];
   }
 }).register('minicard');

+ 66 - 53
client/components/cards/minicard.styl

@@ -1,30 +1,57 @@
+.minicard-wrapper
+  cursor: pointer
+  position: relative
+  display: flex
+  align-items: center
+  margin-bottom: 9px
+
+  &.draggable-hover-card
+    background-color: #f0f0f0
+    border-bottom-color: #c2c2c2
+
+  &.placeholder
+    background: darken(white, 20%)
+    border-radius: 2px
+
+  &.ui-sortable-helper
+    transform: rotate(4deg)
+    display: block !important
+
+    .and-n-other
+      width: 100%
+      height: 16px
+      padding: 4px
+      background-color: darken(white, 5%)
+      text-align: center
+      border-radius: 3px
+
+    .multi-selection-checkbox
+      display: none
+
+  .multi-selection-checkbox + .minicard
+    margin-left: 8px
+
 .minicard
+  padding: 6px 8px 2px
+  position: relative
+  flex: 1
+  flex-wrap: wrap
   background-color: #fff
+  min-height: 20px
   box-shadow: 0 1px 2px rgba(0,0,0,.2)
   border-radius: 2px
-  cursor: pointer
-  margin-bottom: 9px
-  min-height: 20px
-  position: relative
-  z-index: 0
+  color: #4d4d4d
   overflow: hidden
   transition: transform 0.2s,
               border-radius 0.2s,
               border-left 0.2s
 
-  a
-    color: #4d4d4d
-
-    &.active-card
-      background-color: #f0f0f0
-      border-bottom-color: #c2c2c2
-
-      .minicard-operation
-        display: block
-
-  &.draggable-hover-card
-    background-color: #f0f0f0
-    border-bottom-color: #c2c2c2
+  .is-selected &
+    transform: translateX(11px)
+    border-bottom-right-radius: 0
+    border-top-right-radius: 0
+    z-index: 100
+    box-shadow: -2px 1px 2px rgba(0,0,0,.2)
 
   .minicard-cover
     background-position: center
@@ -39,21 +66,6 @@
       background-size: auto
       background-position: center
 
-  .minicard-details
-    padding: 6px 8px 2px
-    position: relative
-    // z-index: 1
-
-  &.is-selected
-    transform: translateX(11px)
-    border-bottom-right-radius: 0
-    border-top-right-radius: 0
-    z-index: 100
-    box-shadow: -2px 1px 2px rgba(0,0,0,.2)
-
-  a.minicard-details
-    text-decoration:none
-
   .minicard-details-overlay
     background: transparent
     bottom: 0
@@ -121,23 +133,24 @@
   .minicard-members:empty
     display: none
 
-  &.ui-sortable-helper
-    transform: rotate(4deg)
-
-.badges
-  float: left
-
-  &:empty
-    display: none
-
-textarea.minicard-composer-textarea,
-textarea.minicard-composer-textarea:focus
-  background: none
-  border: none
-  box-shadow: none
-  height: auto
-  margin-bottom: 4px
-  padding: 0
-  max-height: 162px
-  min-height: 54px
-  overflow-y: auto
+  .badges
+    float: left
+
+    &:empty
+      display: none
+
+  &.minicard-composer
+    margin-bottom: 10px
+
+    textarea.minicard-composer-textarea,
+    textarea.minicard-composer-textarea:focus
+      resize: none
+      background: none
+      border: none
+      box-shadow: none
+      height: auto
+      margin: 0
+      padding: 0
+      max-height: 162px
+      min-height: 54px
+      overflow-y: auto

+ 2 - 3
client/components/cards/popups.jade

@@ -1,8 +1,7 @@
 template(name="cardMembersPopup")
-  //- input.js-search-mem(autofocus placeholder="Search members…" type="text")
-  ul.pop-over-member-list.checkable.js-mem-list
+  ul.pop-over-member-list.js-mem-list
     each board.members
-      li.item.js-member-item(class="{{#if isCardMember}}active{{/if}}")
+      li.item(class="{{#if isCardMember}}active{{/if}}")
         a.name.js-select-member(href="#")
           +userAvatar(user=user size="small")
           span.full-name

+ 30 - 38
client/components/forms/forms.styl

@@ -30,10 +30,6 @@ input[type="radio"]
   -webkit-appearance: radio
   min-height: inherit
 
-input[type="checkbox"]
-  -webkit-appearance: checkbox
-  margin-right: 4px
-
 input[type="text"],
 input[type="password"],
 input[type="email"]
@@ -182,10 +178,6 @@ fieldset
 input[type="hidden"]
   display: none
 
-input[type="checkbox"],
-input[type="radio"]
-  display: inline
-
 .radio-div,
 .check-div
   display: block
@@ -233,6 +225,36 @@ textarea
     font-size: 26px
     margin: 3px 4px
 
+// Material Design checkboxes
+[type="checkbox"]:not(:checked),
+[type="checkbox"]:checked
+  position: absolute
+  left: -9999px
+  visibility: hidden
+
+.materialCheckBox
+  position: relative
+  width: 13px
+  height: @width
+  z-index: 0
+  border: 2px solid #5a5a5a
+  border-radius: 1px
+  transition: .2s
+  margin: 0
+  cursor: pointer
+
+  &.is-checked
+    top: -4px
+    left: -3px
+    width: 7px
+    height: 15px
+    margin-right: 6px
+    border-top: 2px solid transparent
+    border-left: 2px solid transparent
+    transform: rotate(40deg)
+    -webkit-backface-visibility: hidden
+    transform-origin: 100% 100%
+
 .button-link
   background: #fff
   background: linear-gradient(#fff, #f5f5f5)
@@ -355,9 +377,6 @@ textarea
       background-color: rgba(255, 255, 255, .3)
       border-color: transparent
 
-    .icon-sm
-      color: #fff
-
   &:active
     background: #2e85b8
     background: linear-gradient(#2e85b8, #28739f)
@@ -401,7 +420,6 @@ textarea
       border-color: #8b0e0e
 
 button
-
   &.quiet-button,
   &.loud-text-button
     background: none
@@ -438,11 +456,6 @@ button
   &.w-img
     padding-left: 28px
 
-  .icon-sm
-    left: 6px
-    position: absolute
-    top: 6px
-
   &:hover
     color: #4d4d4d
     background: #dcdcdc
@@ -575,29 +588,8 @@ button
     border-color: #2e85b8
     color: #fff
 
-.form-grid
-  display: flex
-  flex-wrap: wrap
-  width: 100%
-
-.form-grid-child
-  flex: 1
-  margin: 0 0 8px
-
-.form-grid-child-full
-  flex: 1 1 100%
-
-.form-grid-child-threequarters
-  flex: 3
-  margin-right: 8px
-
-.form-grid-child-twothirds
-  flex: 2
-  margin-right: 8px
-
 .dropdown-menu
   border-radius: 2px
-  // padding-bottom: 3px
   overflow: hidden
 
   li

+ 2 - 2
client/components/forms/inlinedform.js

@@ -97,6 +97,6 @@ BlazeComponent.extendComponent({
 
 // Press escape to close the currently opened inlinedForm
 EscapeActions.register('inlinedForm',
-  function() { return currentlyOpenedForm.get() !== null; },
-  function() { currentlyOpenedForm.get().close(); }
+  function() { currentlyOpenedForm.get().close(); },
+  function() { return currentlyOpenedForm.get() !== null; }
 );

+ 4 - 5
client/components/lists/body.jade

@@ -10,13 +10,12 @@ template(name="listBody")
         +inlinedForm(autoclose=false position="bottom")
           +addCardForm(listId=_id position="bottom")
         else
-          if newCardFormIsVisible.get
-            a.open-card-composer.js-open-inlined-form
-              i.fa.fa-plus
-              | {{_ 'add-card'}}
+          a.open-minicard-composer.js-open-inlined-form
+            i.fa.fa-plus
+            | {{_ 'add-card'}}
 
 template(name="addCardForm")
-  .minicard.js-composer
+  .minicard.minicard-composer.js-composer
     .minicard-labels.js-minicard-composer-labels
     .minicard-details.clearfix
       textarea.minicard-composer-textarea.js-card-title(autofocus)

+ 7 - 16
client/components/lists/body.js

@@ -34,18 +34,17 @@ BlazeComponent.extendComponent({
     }
 
     if ($.trim(title)) {
-      Cards.insert({
+      var _id = Cards.insert({
         title: title,
         listId: this.data()._id,
         boardId: this.data().board()._id,
         sort: sortIndex
-      }, function(err, _id) {
-        // In case the filter is active we need to add the newly inserted card
-        // in the list of exceptions -- cards that are not filtered. Otherwise
-        // the card will disappear instantly.
-        // See https://github.com/libreboard/libreboard/issues/80
-        Filter.addException(_id);
       });
+      // In case the filter is active we need to add the newly inserted card in
+      // the list of exceptions -- cards that are not filtered. Otherwise the
+      // card will disappear instantly.
+      // See https://github.com/libreboard/libreboard/issues/80
+      Filter.addException(_id);
 
       // We keep the form opened, empty it, and scroll to it.
       textarea.val('').focus();
@@ -55,10 +54,6 @@ BlazeComponent.extendComponent({
     }
   },
 
-  showNewCardForm: function(value) {
-    this.newCardFormIsVisible.set(value);
-  },
-
   scrollToBottom: function() {
     var container = this.firstNode();
     $(container).animate({
@@ -66,14 +61,10 @@ BlazeComponent.extendComponent({
     });
   },
 
-  onCreated: function() {
-    this.newCardFormIsVisible = new ReactiveVar(true);
-  },
-
   events: function() {
     return [{
       submit: this.addCard,
-      'click .open-card-composer': this.scrollToBottom
+      'click .open-minicard-composer': this.scrollToBottom
     }];
   }
 }).register('listBody');

+ 43 - 18
client/components/lists/main.js

@@ -8,10 +8,6 @@ BlazeComponent.extendComponent({
     this.componentChildren('listBody')[0].openForm(options);
   },
 
-  showNewCardForm: function(value) {
-    this.componentChildren('listBody')[0].showNewCardForm(value);
-  },
-
   onCreated: function() {
     this.newCardFormIsVisible = new ReactiveVar(true);
   },
@@ -35,30 +31,59 @@ BlazeComponent.extendComponent({
       connectWith: '.js-minicards',
       tolerance: 'pointer',
       appendTo: '.js-lists',
-      helper: 'clone',
+      helper: function(evt, item) {
+        var helper = item.clone();
+        if (MultiSelection.isActive()) {
+          var andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
+          if (andNOthers > 0) {
+            helper.append($(Blaze.toHTML(HTML.DIV(
+              // XXX Super bad class name
+              {'class': 'and-n-other'},
+              // XXX Need to translate
+              'and ' + andNOthers + ' other cards.'
+            ))));
+          }
+        }
+        return helper;
+      },
       items: itemsSelector,
-      placeholder: 'minicard placeholder',
-      start: function(event, ui) {
+      placeholder: 'minicard-wrapper placeholder',
+      start: function(evt, ui) {
         ui.placeholder.height(ui.helper.height());
-        Popup.close();
-        boardComponent.showNewCardForms(false);
+        EscapeActions.executeLowerThan('popup');
+        boardComponent.setIsDragging(true);
       },
-      stop: function(event, ui) {
+      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 cardId = Blaze.getData(cardDomElement)._id;
         var listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
-        Cards.update(cardId, {
-          $set: {
-            listId: listId,
-            sort: sort
-          }
-        });
-        boardComponent.showNewCardForms(true);
+
+        if (MultiSelection.isActive()) {
+          Cards.find(MultiSelection.getMongoSelector()).forEach(function(c) {
+            Cards.update(c._id, {
+              $set: {
+                listId: listId,
+                sort: sort
+              }
+            });
+          });
+        } else {
+          var cardId = Blaze.getData(cardDomElement)._id;
+          Cards.update(cardId, {
+            $set: {
+              listId: listId,
+              // 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
+            }
+          });
+        }
+        boardComponent.setIsDragging(false);
       }
     });
 

+ 4 - 1
client/components/lists/main.styl

@@ -93,10 +93,13 @@
   overflow-y: auto
   padding: 5px 11px
 
+  .minicards form
+    margin-bottom: 9px
+
   .ps-scrollbar-y-rail
     transform: translateX(2px)
 
-.open-card-composer
+.open-minicard-composer
   border-radius: 2px
   color: #8c8c8c
   display: block

+ 1 - 0
client/components/lists/menu.jade

@@ -5,6 +5,7 @@ template(name="listActionPopup")
   if cards.count
     hr
     ul.pop-over-list
+      li: a.js-select-cards {{_ 'list-select-cards'}}
       li: a.js-move-cards {{_ 'list-move-cards'}}
       li: a.js-archive-cards {{_ 'list-archive-cards'}}
   hr

+ 8 - 0
client/components/lists/menu.js

@@ -6,6 +6,14 @@ Template.listActionPopup.events({
     Popup.close();
   },
   'click .js-list-subscribe': function() {},
+  'click .js-select-cards': function() {
+    var cardIds = Cards.find(
+      {listId: this._id},
+      {fields: { _id: 1 }}
+    ).map(function(card) { return card._id; });
+    MultiSelection.add(cardIds);
+    Popup.close();
+  },
   'click .js-move-cards': Popup.open('listMoveCards'),
   'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() {
     Cards.find({listId: this._id}).forEach(function(card) {

+ 2 - 2
client/components/main/editor.js

@@ -61,6 +61,6 @@ Template.editor.onRendered(function() {
 });
 
 EscapeActions.register('textcomplete',
-  function() { return dropdownMenuIsOpened; },
-  function() {}
+  function() {},
+  function() { return dropdownMenuIsOpened; }
 );

+ 3 - 0
client/components/main/header.styl

@@ -58,6 +58,9 @@
         margin: 4px 8px 0 0
         float: left
 
+        i.fa-chevron-down
+          margin-right: 4px
+
   #header-main-bar
     height: 28px * 1.618034 - 6px
     padding: 7px 10px 0

+ 0 - 239
client/components/main/popup.styl

@@ -35,21 +35,9 @@
     margin: 4px 0 12px
     width: 100%
 
-  .empty
-    margin: 0
-
   img
     max-width: 270px
 
-  .custom-image img
-    height: 18px
-    left: 9px
-    top: 9px
-    width: 18px
-
-  .title
-    line-height: 32px
-
   .header
     height: 36px
     position: relative
@@ -68,10 +56,6 @@
       text-overflow: ellipsis
       white-space: nowrap
 
-    .back-btn, .close-btn
-      &:hover .icon-sm
-        color: darken(white, 80%)
-
     .back-btn
       float: left
       overflow: hidden
@@ -91,7 +75,6 @@
       top: 0
       right: 0
 
-
   &.no-title .header
     background: none
 
@@ -134,15 +117,11 @@
   margin-bottom: 8px
 
 .pop-over-list
-
   &.navigable li.not-selectable>a:hover,
   li.not-selectable>a:hover
     color: #8c8c8c
     cursor: default
 
-    .icon-sm
-      color: #a6a6a6
-
   li > a
     cursor: pointer
     display: block
@@ -168,9 +147,6 @@
       .unread-indicator
         background: #fff
 
-      .icon-sm
-        color: #fff
-
     .sub-name
       clear: both
       color: #8c8c8c
@@ -208,9 +184,6 @@
       .vis-icon
         opacity: .35
 
-      .icon-sm
-        color: #a6a6a6
-
       &:hover
         background: none
 
@@ -218,9 +191,6 @@
         .quiet
           color: #8c8c8c
 
-        .icon-sm
-          color: #a6a6a6
-
       &:active
         background: none
 
@@ -268,9 +238,6 @@
       .quiet
         color: #8c8c8c
 
-      .icon-sm
-        color: #a6a6a6
-
     li.selected > a
       background-color: #005377
       color: #fff
@@ -287,14 +254,10 @@
       .unread-indicator
         background: #fff
 
-      .icon-sm
-        color: #fff
-
       &:active
         background-color: #005377
 
 .pop-over.miniprofile
-
   .header
     border-bottom-color: transparent
     height: 30px
@@ -329,205 +292,3 @@
 
       &:hover
         text-decoration: underline
-
-.pop-over.avdetail .header
-  border-bottom-color: transparent
-  height: 20px
-  position: absolute
-  top: 8px
-  left: 8px
-  right: 8px
-  z-index: 0
-
-.pop-over.avdetail .header-title
-  display: none
-
-.pop-over.avdetail .content
-  text-align: center
-
-.pop-over.avdetail .mem-info
-  margin: 2px 24px 8px
-  position: relative
-  z-index: 1
-  width: 222px
-
-.pop-over.avdetail .mem-info h3 a
-  text-decoration: none
-
-.pop-over.avdetail .mem-info h3 a:hover
-  text-decoration: underline
-
-.pop-over-label-list li,
-.pop-over-member-list li
-
-  &.disabled a
-    cursor:default
-
-  &:not(.disabled):hover a
-    background-color: #005377
-    color: #fff
-
-
-.pop-over-label-list,
-.pop-over-member-list,
-.pop-over-emoji-list,
-.pop-over-card-list
-  li
-    a
-      border-radius: 3px
-      display: block
-      height: 30px
-      line-height: 30px
-      overflow: hidden
-      position: relative
-      text-overflow: ellipsis
-      text-decoration: none
-      white-space: nowrap
-      padding: 4px
-      margin-bottom: 2px
-
-      &.multi-line
-        line-height: 16px
-
-      .member
-        margin-right: 8px
-
-      .card-label
-        float: left
-        height: 30px
-        margin: 0 8px 0 0
-        padding: 0
-        width: 30px
-
-      .option,
-      .icon-check
-        background-clip: content-box
-        background-origin: content-box
-        padding: 11px
-        position: absolute
-        top: 0
-        right: 0
-
-      .sub-name
-        font-size: 12px
-
-
-    &:last-child a
-      margin-bottom: 0
-
-    &.disabled
-      opacity: .5
-
-      &.active a,
-      &.selected a
-        background: none
-        color: #4d4d4d
-        cursor: default
-
-        .quiet
-          color: #8c8c8c
-
-    &.email-invite
-
-      .member
-        display: none
-
-      a
-        padding: 0 10px
-
-    &.selected a
-      background-color: #005377
-      color: #fff
-
-      .quiet
-        color: #eee
-
-      .card-label
-        border-radius: 3px
-
-      .icon-check
-        color: #fff
-
-    &.active a .icon-check
-      display: block
-
-    &.unconfirmed a.name
-      line-height: 16px
-
-  &.options li
-
-    &.selected a
-      padding-right: 28px
-
-      .option
-        display: block
-        opacity: .5
-
-        &:hover
-          opacity: 1
-
-    &.disabled.selected a
-      padding-right: 0
-
-      .option
-        display: none
-
-
-    &.no-option.selected a
-      padding-right: 6px
-
-      .option
-        display: none
-
-  &.collapsed
-
-    &.checkable li.active a
-      padding-right: 0
-
-    li
-      float: left
-      margin: 0 3px 3px 0
-
-      a
-        padding: 0
-        margin: 0
-        width: 30px
-
-        .member
-          opacity: .8
-
-        .full-name
-          display: none
-
-      &.selected a .member,
-      &.active.selected a .member
-        border-color: #005377
-        opacity: .9
-
-      &.active a
-
-        .member
-          border-color: #2e85b8
-          opacity: 1
-
-        .icon-check
-          border-radius: 3px
-          background-color: #2e85b8
-          bottom: 0
-          color: #fff
-          display: block
-          padding: 0
-          right: 0
-          top: auto
-
-  &.checkable li.active a
-    padding-right: 28px
-
-  &.filtered li
-    display: none
-
-    &.matches-filter
-      display: block
-
-  &.limited li.exceeds-limit
-    display: none

+ 0 - 17
client/components/sidebar/events.js

@@ -1,20 +1,3 @@
-Template.filterSidebar.events({
-  'click .js-toggle-label-filter': function(event) {
-    Filter.labelIds.toogle(this._id);
-    Filter.resetExceptions();
-    event.preventDefault();
-  },
-  'click .js-toogle-member-filter': function(event) {
-    Filter.members.toogle(this._id);
-    Filter.resetExceptions();
-    event.preventDefault();
-  },
-  'click .js-clear-all': function(event) {
-    Filter.reset();
-    event.preventDefault();
-  }
-});
-
 var getMemberIndex = function(board, searchId) {
   for (var i = 0; i < board.members.length; i++) {
     if (board.members[i].userId === searchId)

+ 0 - 14
client/components/sidebar/helpers.js

@@ -1,17 +1,3 @@
-var widgetTitles = {
-  filter: 'filter-cards',
-  background: 'change-background'
-};
-
-Template.sidebar.helpers({
-  currentWidget: function() {
-    return Session.get('currentWidget') + 'Sidebar';
-  },
-  currentWidgetTitle: function() {
-    return TAPi18n.__(widgetTitles[Session.get('currentWidget')]);
-  }
-});
-
 // Template.addMemberPopup.helpers({
 //   isBoardMember: function() {
 //     var user = Users.findOne(this._id);

+ 6 - 33
client/components/sidebar/sidebar.jade

@@ -4,49 +4,22 @@ template(name="sidebar")
       class="{{#if isTongueHidden}}is-hidden{{/if}}")
       i.fa.fa-chevron-left
     .sidebar-content.js-board-sidebar-content.js-perfect-scrollbar
+      unless isDefaultView
+        h2
+          a.fa.fa-chevron-left.js-back-home
+          = getViewTitle
       +Template.dynamic(template=getViewTemplate)
 
 template(name='homeSidebar')
   +membersWidget
-  hr.clear
+  hr
   +labelsWidget
-  hr.clear
+  hr
   h3
     i.fa.fa-comments-o
     | {{_ 'activities'}}
   +activities(mode="board")
 
-template(name="filterSidebar")
-  ul.pop-over-label-list.checkable
-    each currentBoard.labels
-      li.item.matches-filter
-        a.name.js-toggle-label-filter
-          span.card-label(class="card-label-{{color}}")
-          span.full-name
-            if name
-              = name
-            else
-              span.quiet {{_ "label-default" color}}
-            if Filter.labelIds.isSelected _id}}
-              span.icon-sm.fa.fa-check
-    hr
-    ul.pop-over-member-list.checkable
-      each currentBoard.members
-        if isActive
-          with getUser userId
-            li.item.js-member-item(
-              class="{{#if Filter.members.isSelected _id}}active{{/if}}")
-              a.name.js-toogle-member-filter
-                +userAvatar(user=this size="small")
-                span.full-name
-                  = profile.name
-                  | (<span class="username">{{ username }}</span>)
-                if Filter.members.isSelected _id
-                  span.icon-sm.fa.fa-check
-    hr
-    a.js-clear-all(class="{{#unless Filter.isActive}}disabled{{/unless}}")
-      | {{_ 'filter-clear'}}
-
 template(name="membersWidget")
   .board-widget.board-widget-members
     h3

+ 20 - 5
client/components/sidebar/sidebar.js

@@ -1,6 +1,11 @@
+Sidebar = null;
+
 var defaultView = 'home';
 
-Sidebar = null;
+var viewTitles = {
+  filter: 'filter-cards',
+  multiselection: 'multi-selection'
+};
 
 BlazeComponent.extendComponent({
   template: function() {
@@ -60,14 +65,23 @@ BlazeComponent.extendComponent({
   },
 
   setView: function(view) {
-    view = view || defaultView;
+    view = _.isString(view) ? view : defaultView;
     this._view.set(view);
+    this.open();
+  },
+
+  isDefaultView: function() {
+    return this.getView() === defaultView;
   },
 
   getViewTemplate: function() {
     return this.getView() + 'Sidebar';
   },
 
+  getViewTitle: function() {
+    return TAPi18n.__(viewTitles[this.getView()]);
+  },
+
   // Board members can assign people or labels by drag-dropping elements from
   // the sidebar to the cards on the board. In order to re-initialize the
   // jquery-ui plugin any time a draggable member or label is modified or
@@ -108,12 +122,13 @@ BlazeComponent.extendComponent({
     // XXX Hacky, we need some kind of `super`
     var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
     return mixinEvents.concat([{
-      'click .js-toogle-sidebar': this.toogle
+      'click .js-toogle-sidebar': this.toogle,
+      'click .js-back-home': this.setView
     }]);
   }
 }).register('sidebar');
 
 EscapeActions.register('sidebarView',
-  function() { return Sidebar && Sidebar.getView() !== defaultView; },
-  function() { Sidebar.setView(defaultView); }
+  function() { Sidebar.setView(defaultView); },
+  function() { return Sidebar && Sidebar.getView() !== defaultView; }
 );

+ 28 - 2
client/components/sidebar/sidebar.styl

@@ -7,7 +7,7 @@
   right: 0
 
   .sidebar-content
-    padding: 10px 20px
+    padding: 12px
     background: white
     box-shadow: -10px 0px 5px -10px darken(white, 30%)
     z-index: 10
@@ -23,7 +23,33 @@
       color: darken(white, 50%)
 
     hr
-      margin: 8px 0
+      margin: 13px 0
+
+    ul.sidebar-list
+      display: flex
+      flex-direction: column
+
+      li a
+        display: flex
+        height: 30px
+        margin: 0
+        padding: 4px
+        border-radius: 3px
+        align-items: center
+
+        &:hover
+          &, i, .quiet
+            color white
+
+        .member, .card-label
+          margin-right: 7px
+
+        .sidebar-list-item-description
+          flex: 1
+          overflow: ellipsis
+
+        .fa.fa-check
+          margin: 0 4px
 
 .board-sidebar
   width: 248px

+ 57 - 0
client/components/sidebar/sidebarFilters.jade

@@ -0,0 +1,57 @@
+//-
+  XXX There is a *lot* of code duplication in the above templates and in the
+  corresponding JavaScript components. We will probably need the upcoming #let
+  and #each x in y constructors.
+
+template(name="filterSidebar")
+  ul.sidebar-list
+    each currentBoard.labels
+      li
+        a.name.js-toggle-label-filter
+          span.card-label.square(class="card-label-{{color}}")
+          span.sidebar-list-item-description
+            if name
+              = name
+            else
+              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(user=this size="small")
+                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'}}
+
+template(name="multiselectionSidebar")
+  ul.sidebar-list
+    each currentBoard.labels
+      li
+        a.name.js-toggle-label-multiselection
+          span.card-label.square(class="card-label-{{color}}")
+          span.sidebar-list-item-description
+            if name
+              = name
+            else
+              span.quiet {{_ "label-default" color}}
+          if allSelectedElementHave 'label' _id
+            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.
+
+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

+ 94 - 0
client/components/sidebar/sidebarFilters.js

@@ -0,0 +1,94 @@
+BlazeComponent.extendComponent({
+  template: function() {
+    return 'filterSidebar';
+  },
+
+  events: function() {
+    return [{
+      'click .js-toggle-label-filter': function(event) {
+        Filter.labelIds.toogle(this._id);
+        Filter.resetExceptions();
+        event.preventDefault();
+      },
+      'click .js-toogle-member-filter': function(event) {
+        Filter.members.toogle(this._id);
+        Filter.resetExceptions();
+        event.preventDefault();
+      },
+      'click .js-clear-all': function(event) {
+        Filter.reset();
+        event.preventDefault();
+      }
+    }];
+  }
+}).register('filterSidebar');
+
+var updateSelectedCards = function(query) {
+  Cards.find(MultiSelection.getMongoSelector()).forEach(function(card) {
+    Cards.update(card._id, query);
+  });
+};
+
+BlazeComponent.extendComponent({
+  template: function() {
+    return 'multiselectionSidebar';
+  },
+
+  mapSelection: function(kind, _id) {
+    return Cards.find(MultiSelection.getMongoSelector()).map(function(card) {
+      var methodName = kind === 'label' ? 'hasLabel' : 'isAssigned';
+      return card[methodName](_id);
+    });
+  },
+
+  allSelectedElementHave: function(kind, _id) {
+    if (MultiSelection.isEmpty())
+      return false;
+    else
+      return _.every(this.mapSelection(kind, _id));
+  },
+
+  someSelectedElementHave: function(kind, _id) {
+    if (MultiSelection.isEmpty())
+      return false;
+    else
+      return _.some(this.mapSelection(kind, _id));
+  },
+
+  events: function() {
+    return [{
+      'click .js-toggle-label-multiselection': function(evt, tpl) {
+        var labelId = this.currentData()._id;
+        var mappedSelection = this.mapSelection('label', labelId);
+        var operation;
+        if (_.every(mappedSelection))
+          operation = '$pull';
+        else if (_.every(mappedSelection, function(bool) { return ! bool; }))
+          operation = '$addToSet';
+        else {
+          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);
+        }
+
+        var query = {};
+        query[operation] = {
+          labelIds: labelId
+        };
+        updateSelectedCards(query);
+      }
+    }];
+  }
+}).register('multiselectionSidebar');
+
+Template.disambiguateMultiLabelPopup.events({
+  'click .js-remove-label': function() {
+    updateSelectedCards({$pull: {labelIds: this._id}});
+    Popup.close();
+  },
+  'click .js-add-label': function() {
+    updateSelectedCards({$addToSet: {labelIds: this._id}});
+    Popup.close();
+  }
+});

+ 77 - 0
client/components/sidebar/templates.html

@@ -0,0 +1,77 @@
+<!-- XXX Translate these template into jade -->
+<template name="closeBoardPopup">
+    <p>{{_ 'close-board-pop'}}</p>
+    <input type="submit" class="js-confirm negate full" value="{{_ 'close'}}">
+</template>
+
+<template name="removeMemberPopup">
+    <p>{{_ 'remove-member-pop'
+            name=user.profile.name
+            username=user.username
+            boardTitle=board.title}}</p>
+    <input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}">
+</template>
+
+<template name="addMemberPopup">
+    <div class="search-with-spinner">
+        {{> esInput index="users" }}
+    </div>
+
+    <div class="manage-member-section hide js-search-results" style="display: block;">
+        <ul class="pop-over-member-list options js-list">
+            {{# esEach index="users"}}
+                <li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}">
+                    <a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})">
+                        {{> userAvatar user=this size="small" }}
+                        <span class="full-name">
+                            {{ profile.name }}  (<span class="username">{{ username }}</span>)
+                        </span>
+                       {{# if isBoardMember }}
+                           <div class="extra-text quiet">({{_ 'joined'}})</div>
+                       {{/if}}
+                        <span class="icon-sm fa fa-chevron-right light option js-open-option"></span>
+                    </a>
+                </li>
+            {{/esEach }}
+        </ul>
+    </div>
+
+    {{# ifEsIsSearching index='users' }}
+        <div class="tac">
+            <span class="tabbed-pane-main-col-loading-spinner spinner"></span>
+        </div>
+    {{ /ifEsIsSearching }}
+
+     {{# ifEsHasNoResults index="users" }}
+        <div class="manage-member-section js-no-results">
+            <p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p>
+        </div>
+    {{ /ifEsHasNoResults }}
+
+    <div class="manage-member-section js-helper">
+        <p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p>
+    </div>
+</template>
+
+<template name="changePermissionsPopup">
+    <ul class="pop-over-list">
+        <li>
+            <a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}">
+                {{_ 'admin'}}
+                {{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}}
+                <span class="sub-name">{{_ 'admin-desc'}}</span>
+            </a>
+        </li>
+        <li>
+            <a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}">
+                {{_ 'normal'}}
+                {{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}}
+                <span class="sub-name">{{_ 'normal-desc'}}</span>
+            </a>
+        </li>
+    </ul>
+    {{#if isLastAdmin}}
+        <hr>
+        <p class="quiet bottom">{{_ 'last-admin-desc'}}</p>
+    {{/if}}
+</template>

+ 0 - 307
client/components/sidebar/templates.html.old

@@ -1,307 +0,0 @@
-<template name="boardWidgets">
-    <a href="#" class="sidebar-show-btn dark-hover js-show-sidebar">
-        <span class="icon-sm fa fa-chevron-left"></span>
-        <span class="text">{{_ 'show-sidebar'}}</span>
-    </a>
-    <div class="board-widgets {{#if session 'sidebarIsOpen'}}show{{else}}hide{{/if}}">
-        <div>
-            <a href="#" class="sidebar-hide-btn dark-hover js-hide-sidebar" title="{{_ 'close-sidebar-title'}}">
-                <span class="icon-sm fa fa-chevron-right"></span>
-            </a>
-            {{#unless isTrue currentWidget "homeWidget"}}
-                <div class="board-widgets-title clearfix">
-                    <a href="#" class="board-sidebar-back-btn js-pop-widget-view">
-                        <span class="left-arrow"></span>{{_ 'back'}}
-                    </a>
-                    <h3 class="text">{{currentWidgetTitle}}</h3>
-                    <hr>
-                </div>
-            {{/unless}}
-            <div class="board-widgets-content-wrapper">
-                <div class="board-widgets-content default fancy-scrollbar short{{#unless session 'menuWidgetIsOpen'}} short{{/unless}}">
-                    {{> UI.dynamic template=currentWidget data=this }}
-                </div>
-            </div>
-        </div>
-    </div>
-</template>
-
-<template name="homeWidget">
-{{ > menuWidget }}
-{{ > membersWidget }}
-{{ > activityWidget }}
-</template>
-
-<template name="menuWidget">
-    <div class="board-widget board-widget-nav clearfix{{#unless session 'menuWidgetIsOpen'}} collapsed{{/unless}}">
-        <h3 class="dark-hover toggle-widget-nav js-toggle-widget-nav">{{_ 'menu'}}
-            <span class="icon-sm fa fa-chevron-circle-down toggle-menu-icon"></span>
-        </h3>
-        <ul class="nav-list">
-            <hr style="margin-top: 0;">
-            <li>
-                <a href="#" class="nav-list-item js-open-archive">
-                    <span class="icon-sm fa fa-archive icon-type"></span>
-                    {{_ 'archived-items'}}
-                </a>
-            </li>
-            <li>
-                <a href="#" class="nav-list-item js-open-card-filter">
-                    <span class="icon-sm fa fa-filter icon-type"></span>
-                    {{_ 'filter-cards'}}
-                </a>
-            </li>
-            {{#if currentUser.isBoardAdmin}}
-                <hr>
-                <li>
-                    <a class="nav-list-item nav-list-sub-item board-settings-background js-change-background">
-                        <span class="board-settings-background-preview" style="background-color:{{board.background.color}}"></span>
-                        {{_ 'change-background'}}…
-                    </a>
-                </li>
-                {{#unless isSandstorm }}
-                    <li>
-                        <a class="nav-list-item nav-list-sub-item js-close-board" href="#">{{_ 'close-board'}}</a>
-                    </li>
-                {{/unless}}
-            {{/if}}
-            {{!
-                XXX Language should be handled by sandstorm, but for now display a language selection link in the board menu.
-                This link is normally present in the header bar that is not displayed on sandstorm.
-            }}
-            {{#if isSandstorm}}
-                <hr>
-                <li>
-                    <a class="nav-list-item nav-list-sub-item js-language">{{_ 'language'}}</a>
-                </li>
-            {{/if}}
-        </ul>
-    </div>
-</template>
-
-<template name="membersWidget">
-    <hr>
-    <div class="board-widget board-widget-members clearfix">
-        <div class="board-widget-title">
-            <h3>{{_ 'members'}}</h3>
-        </div>
-        <div class="board-widget-content">
-            <div class="board-widget-members js-list-board-members clearfix js-list-draggable-board-members">
-                {{# each board.members }}
-                    {{> userAvatar userId=this.userId draggable=true size="small" showBadges=true}}
-                {{/ each }}
-            </div>
-            {{# unless isSandstrom }}
-                {{# if currentUser.isBoardAdmin }}
-                    <a href="#" class="button-link js-open-manage-board-members">
-                        <span class="icon-sm fa fa-user"></span> {{_ 'add-members'}}
-                    </a>
-                {{/ if }}
-            {{/ unless }}
-        </div>
-    </div>
-</template>
-
-<template name="activityWidget">
-    {{# if board.activities.count }}
-        <hr>
-        <div class="board-widget board-widget-activity bottom clearfix">
-            <div class="board-widget-title">
-                <h3>{{_ 'activity'}}</h3>
-            </div>
-            <div class="board-widget-content">
-                <div class="activity-gradient-t"></div>
-                <div class="activity-gradient-b"></div>
-                <div class="board-actions-list fancy-scrollbar">
-                    {{ > activities }}
-                </div>
-            </div>
-        </div>
-    {{/if}}
-</template>
-
-<template name="memberPopup">
-    <div class="board-member-menu">
-        <div class="mini-profile-info">
-            {{> userAvatar user=user}}
-            <div class="info">
-                <h3 class="bottom" style="margin-right: 40px;">
-                    <a class="js-profile" href="{{ pathFor route='Profile' username=user.username }}">{{ user.profile.name }}</a>
-                </h3>
-                <p class="quiet bottom">@{{ user.username }}</p>
-            </div>
-        </div>
-        {{# if currentUser.isBoardMember }}
-            <ul class="pop-over-list">
-                {{# if currentUser.isBoardAdmin }}
-                    <li>
-                        <a class="js-change-role" href="#">
-                            {{_ 'change-permissions'}} <span class="quiet" style="font-weight: normal;">({{ memberType }})</span>
-                        </a>
-                    </li>
-                {{/ if }}
-
-                <li>
-                    {{# if currentUser.isBoardAdmin }}
-                        <a class="js-remove-member">{{_ 'remove-from-board'}}</a>
-                    {{ else }}
-                        <a class="js-leave-member">{{_ 'leave-board'}}</a>
-                    {{/ if }}
-                </li>
-            </ul>
-        {{/ if }}
-    </div>
-</template>
-
-<template name="filterWidget">
-    <ul class="pop-over-label-list checkable">
-        {{#each board.labels}}
-            <li class="item matches-filter">
-                <a class="name js-toggle-label-filter">
-                    <span class="card-label card-label-{{color}}"></span>
-                    <span class="full-name">
-                        {{#if name}}
-                            {{name}}
-                        {{else}}
-                            <span class="quiet">{{_ "label-default" color}}</span>
-                        {{/if}}
-                    </span>
-                    {{#if Filter.labelIds.isSelected _id}}
-                        <span class="icon-sm fa fa-check"></span>
-                    {{/if}}
-                </a>
-            </li>
-        {{/each}}
-    </ul>
-    <hr>
-    <ul class="pop-over-member-list checkable">
-        {{#each board.members}}
-            {{#with getUser userId}}
-                <li class="item js-member-item {{#if Filter.members.isSelected _id}}active{{/if}}">
-                    <a href="#" class="name js-toogle-member-filter">
-                        {{> userAvatar user=this size="small" }}
-                        <span class="full-name">
-                            {{ profile.name }}
-                            (<span class="username">{{ username }}</span>)
-                        </span>
-                        {{#if Filter.members.isSelected _id}}
-                            <span class="icon-sm fa fa-check checked-icon"></span>
-                        {{/if}}
-                    </a>
-                 </li>
-            {{/with}}
-        {{/each}}
-    </ul>
-    <hr>
-    <ul class="pop-over-list inset normal-weight">
-        <li>
-            <a class="js-clear-all {{#unless Filter.isActive}}disabled{{/unless}}" style="padding-left: 40px;">
-                {{_ 'filter-clear'}}
-            </a>
-        </li>
-    </ul>
-</template>
-
-<template name="backgroundWidget">
-    <div class="board-widgets-content-wrapper fancy-scrollbar">
-        <div class="board-widgets-content">
-            <div class="board-backgrounds-list clearfix">
-                {{#each backgroundColors}}
-                    <div class="board-background-select js-select-background">
-                        <span class="background-box " style="background-color: {{this}}; "></span>
-                    </div>
-                {{/each}}
-            </div>
-            {{!--
-                <h2 class="clear">Photos</h2>
-                <div class="board-backgrounds-list relative clearfix js-gold-photos-list disabled">
-                    <div class="board-background-select js-select-background">
-                        <span class="background-box " style="background-image: url(&quot;{{url}}&quot;);">
-                            <a class="background-option js-background-attribution" href={{href}} target="_blank" title={{title}}>
-                                <img src="https://d78fikflryjgj.cloudfront.net/images/d906fe5c1274c56c5571d49705547587/cc.png" style="height: 14px; width: 14px; vertical-align: text-top;" title="http://creativecommons.org/licenses/by/2.0/deed.en">
-                                <span class="text" style="margin-left: 2px;">{{author}}</span>
-                            </a>
-                        </span>
-                    </div>
-                </div>
-            --}}
-        </div>
-    </div>
-</template>
-
-<template name="closeBoardPopup">
-    <p>{{_ 'close-board-pop'}}</p>
-    <input type="submit" class="js-confirm negate full" value="{{_ 'close'}}">
-</template>
-
-<template name="removeMemberPopup">
-    <p>{{_ 'remove-member-pop'
-            name=user.profile.name
-            username=user.username
-            boardTitle=board.title}}</p>
-    <input type="submit" class="js-confirm negate full" value="{{_ 'remove-member'}}">
-</template>
-
-<template name="addMemberPopup">
-    <div class="search-with-spinner">
-        {{> esInput index="users" }}
-    </div>
-
-    <div class="manage-member-section hide js-search-results" style="display: block;">
-        <ul class="pop-over-member-list options js-list">
-            {{# esEach index="users"}}
-                <li class="item js-member-item {{# if isBoardMember }}disabled{{/if}}">
-                    <a href="#" class="name js-select-member {{# if isBoardMember }}multi-line{{/if}}" title="{{ profile.name }} ({{ username }})">
-                        {{> userAvatar user=this size="small" }}
-                        <span class="full-name">
-                            {{ profile.name }}  (<span class="username">{{ username }}</span>)
-                        </span>
-                       {{# if isBoardMember }}
-                           <div class="extra-text quiet">({{_ 'joined'}})</div>
-                       {{/if}}
-                        <span class="icon-sm fa fa-chevron-right light option js-open-option"></span>
-                    </a>
-                </li>
-            {{/esEach }}
-        </ul>
-    </div>
-
-    {{# ifEsIsSearching index='users' }}
-        <div class="tac">
-            <span class="tabbed-pane-main-col-loading-spinner spinner"></span>
-        </div>
-    {{ /ifEsIsSearching }}
-
-     {{# ifEsHasNoResults index="users" }}
-        <div class="manage-member-section js-no-results">
-            <p class="quiet center" style="padding: 16px 4px;">{{_ 'no-results'}}</p>
-        </div>
-    {{ /ifEsHasNoResults }}
-
-    <div class="manage-member-section js-helper">
-        <p class="bottom quiet" style="padding: 6px;">{{_ 'search-member-desc'}}</p>
-    </div>
-</template>
-
-<template name="changePermissionsPopup">
-    <ul class="pop-over-list">
-        <li>
-            <a class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}">
-                {{_ 'admin'}}
-                {{#if isAdmin}}<span class="icon-sm fa fa-check"></span>{{/if}}
-                <span class="sub-name">{{_ 'admin-desc'}}</span>
-            </a>
-        </li>
-        <li>
-            <a class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}">
-                {{_ 'normal'}}
-                {{#unless isAdmin}}<span class="icon-sm fa fa-check"></span>{{/unless}}
-                <span class="sub-name">{{_ 'normal-desc'}}</span>
-            </a>
-        </li>
-    </ul>
-    {{#if isLastAdmin}}
-        <hr>
-        <p class="quiet bottom">{{_ 'last-admin-desc'}}</p>
-    {{/if}}
-</template>

+ 28 - 6
client/config/router.js

@@ -1,3 +1,6 @@
+// XXX Switch to Flow-Router?
+var previousRoute;
+
 Router.configure({
   loadingTemplate: 'spinner',
   notFoundTemplate: 'notfound',
@@ -6,24 +9,43 @@ Router.configure({
   onBeforeAction: function() {
     var options = this.route.options;
 
+    var loggedIn = Tracker.nonreactive(function() {
+      return !! Meteor.userId();
+    });
+
     // Redirect logged in users to Boards view when they try to open Login or
     // signup views.
-    if (Meteor.userId() && options.redirectLoggedInUsers) {
+    if (loggedIn && options.redirectLoggedInUsers) {
       return this.redirect('Boards');
     }
 
     // Authenticated
-    if (! Meteor.userId() && options.authenticated) {
+    if (! loggedIn && options.authenticated) {
       return this.redirect('atSignIn');
     }
 
-    // Reset default sessions
-    Session.set('error', false);
-
     Tracker.nonreactive(function() {
-      EscapeActions.executeLowerThan(40);
+      if (! options.noEscapeActions &&
+          ! (previousRoute && previousRoute.options.noEscapeActions))
+      EscapeActions.executeAll();
     });
 
+    previousRoute = this.route;
+
     this.next();
   }
 });
+
+// We want to execute our EscapeActions.executeLowerThan method any time the
+// route is changed, but not if the stays the same but only the parameters
+// change (eg when a user is navigation from a card A to a card B). This is why
+// we can’t put this function in the above `onBeforeAction` that is being run
+// too many times, instead we register a dependency only on the route name and
+// use Tracker.autorun. The following paragraph explains the problem quite well:
+// https://github.com/meteorhacks/flow-router#routercurrent-is-evil
+// Tracker.autorun(function(computation) {
+//   routeName.get();
+//   if (! computation.firstRun) {
+//     EscapeActions.executeLowerThan('inlinedForm');
+//   }
+// });

+ 10 - 1
client/lib/filter.js

@@ -91,7 +91,7 @@ Filter = {
     });
   },
 
-  getMongoSelector: function() {
+  _getMongoSelector: function() {
     var self = this;
 
     if (! self.isActive())
@@ -110,6 +110,14 @@ Filter = {
     return {$or: [filterSelector, exceptionsSelector]};
   },
 
+  mongoSelector: function(additionalSelector) {
+    var filterSelector = this._getMongoSelector();
+    if (_.isUndefined(additionalSelector))
+      return filterSelector;
+    else
+      return {$and: [filterSelector, additionalSelector]};
+  },
+
   reset: function() {
     var self = this;
     _.forEach(self._fields, function(fieldName) {
@@ -123,6 +131,7 @@ Filter = {
     if (this.isActive()) {
       this._exceptions.push(_id);
       this._exceptionsDep.changed();
+      Tracker.flush();
     }
   },
 

+ 11 - 2
client/lib/keyboard.js

@@ -47,11 +47,16 @@ EscapeActions = {
     'textcomplete',
     'popup',
     'inlinedForm',
+    'multiselection-disable',
     'sidebarView',
-    'detailedPane'
+    'detailsPane',
+    'multiselection-reset'
   ],
 
-  register: function(label, condition, action) {
+  register: function(label, action, condition) {
+    if (_.isUndefined(condition))
+      condition = function() { return true; };
+
     // XXX Rewrite this with ES6: .push({ priority, condition, action })
     var priority = this.hierarchy.indexOf(label);
     if (priority === -1) {
@@ -87,6 +92,10 @@ EscapeActions = {
       if (!! currentAction.condition())
         currentAction.action();
     }
+  },
+
+  executeAll: function() {
+    return this.executeLowerThan();
   }
 };
 

+ 159 - 0
client/lib/multiSelection.js

@@ -0,0 +1,159 @@
+
+var getCardsBetween = function(idA, idB) {
+
+  var pluckId = function(doc) {
+    return doc._id;
+  };
+
+  var getListsStrictlyBetween = function(id1, id2) {
+    return Lists.find({
+      $and: [
+        { sort: { $gt: Lists.findOne(id1).sort } },
+        { sort: { $lt: Lists.findOne(id2).sort } }
+      ],
+      archived: false
+    }).map(pluckId);
+  };
+
+  var cards = _.sortBy([Cards.findOne(idA), Cards.findOne(idB)], function(c) {
+    return c.sort;
+  });
+
+  var selector;
+  if (cards[0].listId === cards[1].listId) {
+    selector = {
+      listId: cards[0].listId,
+      sort: {
+        $gte: cards[0].sort,
+        $lte: cards[1].sort
+      },
+      archived: false
+    };
+  } else {
+    selector = {
+      $or: [{
+        listId: cards[0].listId,
+        sort: { $lte: cards[0].sort }
+      }, {
+        listId: {
+          $in: getListsStrictlyBetween(cards[0].listId, cards[1].listId)
+        }
+      }, {
+        listId: cards[1].listId,
+        sort: { $gte: cards[1].sort }
+      }],
+      archived: false
+    };
+  }
+
+  return Cards.find(Filter.mongoSelector(selector)).map(pluckId);
+};
+
+MultiSelection = {
+  sidebarView: 'multiselection',
+
+  _selectedCards: new ReactiveVar([]),
+
+  _isActive: new ReactiveVar(false),
+
+  startRangeCardId: null,
+
+  reset: function() {
+    this._selectedCards.set([]);
+  },
+
+  getMongoSelector: function() {
+    return Filter.mongoSelector({
+      _id: { $in: this._selectedCards.get() }
+    });
+  },
+
+  isActive: function() {
+    return this._isActive.get();
+  },
+
+  isEmpty: function() {
+    return this._selectedCards.get().length === 0;
+  },
+
+  activate: function() {
+    if (! this.isActive()) {
+      EscapeActions.executeLowerThan('detailsPane');
+      this._isActive.set(true);
+      Sidebar.setView(this.sidebarView);
+      Tracker.flush();
+    }
+  },
+
+  disable: function() {
+    if (this.isActive()) {
+      this._isActive.set(false);
+      if (Sidebar && Sidebar.getView() === this.sidebarView) {
+        Sidebar.setView();
+      }
+    }
+  },
+
+  add: function(cardIds) {
+    return this.toogle(cardIds, { add: true, remove: false });
+  },
+
+  remove: function(cardIds) {
+    return this.toogle(cardIds, { add: false, remove: true });
+  },
+
+  toogleRange: function(cardId) {
+    var selectedCards = this._selectedCards.get();
+    var startRange;
+    this.reset();
+    if (! this.isActive() || selectedCards.length === 0) {
+      this.toogle(cardId);
+    } else {
+      startRange = selectedCards[selectedCards.length - 1];
+      this.toogle(getCardsBetween(startRange, cardId));
+    }
+  },
+
+  toogle: function(cardIds, options) {
+    var self = this;
+    cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
+    options = _.extend({
+      add: true,
+      remove: true
+    }, options || {});
+
+    if (! self.isActive()) {
+      self.reset();
+      self.activate();
+    }
+
+    var selectedCards = self._selectedCards.get();
+
+    _.each(cardIds, function(cardId) {
+      var indexOfCard = selectedCards.indexOf(cardId);
+
+      if (options.remove && indexOfCard > -1)
+        selectedCards.splice(indexOfCard, 1);
+
+      else if (options.add)
+        selectedCards.push(cardId);
+    });
+
+    self._selectedCards.set(selectedCards);
+  },
+
+  isSelected: function(cardId) {
+    return this._selectedCards.get().indexOf(cardId) > -1;
+  }
+};
+
+Blaze.registerHelper('MultiSelection', MultiSelection);
+
+EscapeActions.register('multiselection-disable',
+  function() { MultiSelection.disable(); },
+  function() { return MultiSelection.isActive(); }
+);
+
+EscapeActions.register('multiselection-reset',
+  function() { MultiSelection.reset(); }
+);

+ 2 - 2
client/lib/popup.js

@@ -205,6 +205,6 @@ $(document).on('click', function(evt) {
 // Press escape to close the popup.
 var bindPopup = function(f) { return _.bind(f, Popup); };
 EscapeActions.register('popup',
-  bindPopup(Popup.isOpen),
-  bindPopup(Popup.close)
+  bindPopup(Popup.close),
+  bindPopup(Popup.isOpen)
 );

+ 0 - 38
client/styles/main.styl

@@ -318,44 +318,6 @@ dd
 .card-composer
   padding-bottom: 8px
 
-.cc-controls
-  margin-top: 1px
-
-  input[type="submit"]
-    float: left
-    margin-top: 0
-    padding: 5px 18px
-
-  .icon-lg
-    float: left
-
-  .cc-opt
-    float: right
-
-.minicard-placeholder,
-.minicard.placeholder
-  background: silver
-  border: none
-  min-height: 18px
-
-  .hook
-    height: 18px
-    position: absolute
-    right: 0
-    top: 0
-    width: 18px
-
-input[type="text"].attachment-add-link-input
-  float: left
-  margin: 0 0 8px
-  width: 80%
-
-input[type="submit"].attachment-add-link-submit
-  float: left
-  margin: 0 0 8px 4px
-  padding: 6px 12px
-  width: 18%
-
 .card-detail-badge
   background-color: #dbdbdb
   border-radius: 3px

+ 6 - 0
collections/cards.js

@@ -120,9 +120,15 @@ Cards.helpers({
     });
     return cardLabels;
   },
+  hasLabel: function(labelId) {
+    return _.contains(this.labelIds, labelId);
+  },
   user: function() {
     return Users.findOne(this.userId);
   },
+  isAssigned: function(memberId) {
+    return _.contains(this.members, memberId);
+  },
   activities: function() {
     return Activities.find({ type: 'card', cardId: this._id },
                                                     { sort: { createdAt: -1 }});

+ 1 - 1
collections/lists.js

@@ -44,7 +44,7 @@ if (Meteor.isServer) {
 
 Lists.helpers({
   cards: function() {
-    return Cards.find(_.extend(Filter.getMongoSelector(), {
+    return Cards.find(Filter.mongoSelector({
       listId: this._id,
       archived: false
     }), { sort: ['sort'] });

+ 5 - 2
i18n/en.i18n.json

@@ -74,7 +74,7 @@
     "email-placeholder": "e.g., doc@frankenstein.com",
     "filter": "Filter",
     "filter-cards": "Filter Cards",
-    "filter-clear": "Clear filter.",
+    "filter-clear": "Clear filter",
     "filter-on": "Filter is on",
     "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.",
     "fullname": "Full Name",
@@ -98,6 +98,7 @@
     "leave-board": "Leave Board…",
     "link-card": "Link to this card",
     "list-move-cards": "Move All Cards in This List…",
+    "list-select-cards": "Select All Cards in This List",
     "list-archive-cards": "Archive All Cards in This List…",
     "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click “Menu” > “Archived Items”.",
     "log-in": "Log In",
@@ -107,6 +108,7 @@
     "members-title": "Add or remove members of the board from the card.",
     "menu": "Menu",
     "modal-close-title": "Close this dialog window.",
+    "multi-selection": "Multi-Selection",
     "my-boards": "My Boards",
     "name": "Name",
     "name": "Name",
@@ -181,5 +183,6 @@
     "changePermissionsPopup-title": "Change Permissions",
     "setLanguagePopup-title": "Change Language",
     "cardAttachmentsPopup-title": "Attach From…",
-    "attachmentDeletePopup-title": "Delete Attachment?"
+    "attachmentDeletePopup-title": "Delete Attachment?",
+    "disambiguateMultiLabelPopup-title": "Disambiguate Label Action"
 }