{{_ 'close-board-pop'}}
+ + + + +{{_ 'remove-member-pop' + name=user.profile.name + username=user.username + boardTitle=board.title}}
+ + + + +{{_ 'no-results'}}
+{{_ 'search-member-desc'}}
+{{_ 'last-admin-desc'}}
+ {{/if}} + diff --git a/client/components/sidebar/templates.html.old b/client/components/sidebar/templates.html.old deleted file mode 100644 index d8b063f04..000000000 --- a/client/components/sidebar/templates.html.old +++ /dev/null @@ -1,307 +0,0 @@ - - - - {{_ 'show-sidebar'}} - - - - - -{{ > menuWidget }} -{{ > membersWidget }} -{{ > activityWidget }} - - - - - - - -{{_ 'close-board-pop'}}
- - - - -{{_ 'remove-member-pop' - name=user.profile.name - username=user.username - boardTitle=board.title}}
- - - - -{{_ 'no-results'}}
-{{_ 'search-member-desc'}}
-{{_ 'last-admin-desc'}}
- {{/if}} - diff --git a/client/config/router.js b/client/config/router.js index d4bc3c4fd..ed9a069d6 100644 --- a/client/config/router.js +++ b/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'); +// } +// }); diff --git a/client/lib/filter.js b/client/lib/filter.js index d96fa89cd..359b65d3f 100644 --- a/client/lib/filter.js +++ b/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(); } }, diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js index 0fbdbfd5c..8b105c280 100644 --- a/client/lib/keyboard.js +++ b/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(); } }; diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js new file mode 100644 index 000000000..53c16da05 --- /dev/null +++ b/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(); } +); diff --git a/client/lib/popup.js b/client/lib/popup.js index 6298ba810..46c137e8e 100644 --- a/client/lib/popup.js +++ b/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) ); diff --git a/client/styles/main.styl b/client/styles/main.styl index 521e1f567..4b78b9ecb 100644 --- a/client/styles/main.styl +++ b/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 diff --git a/collections/cards.js b/collections/cards.js index 538b6af41..374dcbc3a 100644 --- a/collections/cards.js +++ b/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 }}); diff --git a/collections/lists.js b/collections/lists.js index 196477ec1..1a30dbbaf 100644 --- a/collections/lists.js +++ b/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'] }); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index fcab8d206..60063ea88 100644 --- a/i18n/en.i18n.json +++ b/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" }