diff options
author | Maxime Quandalle <maxime@quandalle.com> | 2015-06-15 17:16:56 +0200 |
---|---|---|
committer | Maxime Quandalle <maxime@quandalle.com> | 2015-06-16 14:30:21 +0200 |
commit | 5478fc93dbe3be14c4a38754881e00dc0b6a38f9 (patch) | |
tree | 8b40a29a3cbe07747112e809db3fd12b719ae3bf /client | |
parent | a41e07b37ec9243191804ac2966e2d136ce79710 (diff) | |
download | wekan-5478fc93dbe3be14c4a38754881e00dc0b6a38f9.tar.gz wekan-5478fc93dbe3be14c4a38754881e00dc0b6a38f9.tar.bz2 wekan-5478fc93dbe3be14c4a38754881e00dc0b6a38f9.zip |
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.
Diffstat (limited to 'client')
-rw-r--r-- | client/components/boards/boardBody.styl | 3 | ||||
-rw-r--r-- | client/components/boards/boardHeader.jade | 8 | ||||
-rw-r--r-- | client/components/boards/colors.styl | 1 | ||||
-rw-r--r-- | client/components/lists/body.js | 6 | ||||
-rw-r--r-- | client/components/lists/main.js | 15 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.styl | 13 | ||||
-rw-r--r-- | client/components/sidebar/sidebarFilters.jade | 65 | ||||
-rw-r--r-- | client/components/sidebar/sidebarFilters.js | 58 | ||||
-rw-r--r-- | client/lib/escapeActions.js | 2 | ||||
-rw-r--r-- | client/lib/multiSelection.js | 12 | ||||
-rw-r--r-- | client/lib/utils.js | 13 |
11 files changed, 144 insertions, 52 deletions
diff --git a/client/components/boards/boardBody.styl b/client/components/boards/boardBody.styl index 9db5c1c0..8ce478c3 100644 --- a/client/components/boards/boardBody.styl +++ b/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 diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index e78de3b2..a1d3ce9f 100644 --- a/client/components/boards/boardHeader.jade +++ b/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 diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl index ff351880..d131701c 100644 --- a/client/components/boards/colors.styl +++ b/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%) diff --git a/client/components/lists/body.js b/client/components/lists/body.js index a91f0ca9..27864474 100644 --- a/client/components/lists/body.js +++ b/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)) { diff --git a/client/components/lists/main.js b/client/components/lists/main.js index 520d0772..c7f3f5e8 100644 --- a/client/components/lists/main.js +++ b/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 } }); } diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl index 813e263a..a5b695e9 100644 --- a/client/components/sidebar/sidebar.styl +++ b/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 diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade index 29b65f3b..a40232d2 100644 --- a/client/components/sidebar/sidebarFilters.jade +++ b/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 diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js index 42b6e185..c7b28443 100644 --- a/client/components/sidebar/sidebarFilters.js +++ b/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(); + } +}); diff --git a/client/lib/escapeActions.js b/client/lib/escapeActions.js index 5d8a1f25..c1a8cca5 100644 --- a/client/lib/escapeActions.js +++ b/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'); } }); diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js index 2f96e199..e6db42cd 100644 --- a/client/lib/multiSelection.js +++ b/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' + } ); diff --git a/client/lib/utils.js b/client/lib/utils.js index b9954212..121b3c2b 100644 --- a/client/lib/utils.js +++ b/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}; } } }; |