From 2c0030da62b9a1e59a55e3429fe514bbd51e1ee3 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Fri, 29 May 2015 23:35:30 +0200 Subject: 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. --- client/components/sidebar/events.js | 17 -- client/components/sidebar/helpers.js | 14 -- client/components/sidebar/sidebar.jade | 39 +--- client/components/sidebar/sidebar.js | 25 ++- client/components/sidebar/sidebar.styl | 30 ++- client/components/sidebar/sidebarFilters.jade | 57 +++++ client/components/sidebar/sidebarFilters.js | 94 ++++++++ client/components/sidebar/templates.html | 77 +++++++ client/components/sidebar/templates.html.old | 307 -------------------------- 9 files changed, 282 insertions(+), 378 deletions(-) create mode 100644 client/components/sidebar/sidebarFilters.jade create mode 100644 client/components/sidebar/sidebarFilters.js create mode 100644 client/components/sidebar/templates.html delete mode 100644 client/components/sidebar/templates.html.old (limited to 'client/components/sidebar') diff --git a/client/components/sidebar/events.js b/client/components/sidebar/events.js index 1067421f..a1aeb13a 100644 --- a/client/components/sidebar/events.js +++ b/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) diff --git a/client/components/sidebar/helpers.js b/client/components/sidebar/helpers.js index 15035bd4..9d3340ad 100644 --- a/client/components/sidebar/helpers.js +++ b/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); diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 07d6bbcf..9dd47b0d 100644 --- a/client/components/sidebar/sidebar.jade +++ b/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 - | ({{ username }}) - 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 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index b737e9de..777d72e1 100644 --- a/client/components/sidebar/sidebar.js +++ b/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; } ); diff --git a/client/components/sidebar/sidebar.styl b/client/components/sidebar/sidebar.styl index a5bc3dc5..e24b7de2 100644 --- a/client/components/sidebar/sidebar.styl +++ b/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 diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade new file mode 100644 index 00000000..34b3074f --- /dev/null +++ b/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 + | ({{ username }}) + 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 diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js new file mode 100644 index 00000000..df0db529 --- /dev/null +++ b/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(); + } +}); diff --git a/client/components/sidebar/templates.html b/client/components/sidebar/templates.html new file mode 100644 index 00000000..12e7be0a --- /dev/null +++ b/client/components/sidebar/templates.html @@ -0,0 +1,77 @@ + + + + + + + + diff --git a/client/components/sidebar/templates.html.old b/client/components/sidebar/templates.html.old deleted file mode 100644 index d8b063f0..00000000 --- a/client/components/sidebar/templates.html.old +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -- cgit v1.2.3-1-g7c22