diff options
author | Maxime Quandalle <maxime@quandalle.com> | 2015-05-29 23:35:30 +0200 |
---|---|---|
committer | Maxime Quandalle <maxime@quandalle.com> | 2015-05-30 03:50:14 +0200 |
commit | 2c0030da62b9a1e59a55e3429fe514bbd51e1ee3 (patch) | |
tree | b2834702806e59cb05ea02e2c377266eb17d6c8f | |
parent | 6457615e6ac6717d2175be9483388d4d70ea1c4a (diff) | |
download | wekan-2c0030da62b9a1e59a55e3429fe514bbd51e1ee3.tar.gz wekan-2c0030da62b9a1e59a55e3429fe514bbd51e1ee3.tar.bz2 wekan-2c0030da62b9a1e59a55e3429fe514bbd51e1ee3.zip |
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.
45 files changed, 883 insertions, 933 deletions
@@ -3,3 +3,4 @@ .meteor-spk .tx/ *.sublime-workspace +tmp/ @@ -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 } @@ -69,9 +69,10 @@ // Our objects "EscapeActions": true, "Filter": true, + "Filter": true, "Mixins": true, + "MultiSelection": true, "Popup": true, - "Filter": true, "Sidebar": true, "Utils": true, diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade index 672a3860..57970c4f 100644 --- a/client/components/boards/boardBody.jade +++ b/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) diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index cf32f764..b5e4154a 100644 --- a/client/components/boards/boardBody.js +++ b/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) { diff --git a/client/components/boards/boardBody.styl b/client/components/boards/boardBody.styl index de4963ab..70d8f3d6 100644 --- a/client/components/boards/boardBody.styl +++ b/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 diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 0ea359fc..f10fcb22 100644 --- a/client/components/boards/boardHeader.jade +++ b/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 diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index a78012ca..28238d4c 100644 --- a/client/components/boards/boardHeader.js +++ b/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(); } }]; } diff --git a/client/components/boards/colors.styl b/client/components/boards/colors.styl index 2b60dde3..1097b20a 100644 --- a/client/components/boards/colors.styl +++ b/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) diff --git a/client/components/boards/router.js b/client/components/boards/router.js index 81fc3d91..e5ccecdb 100644 --- a/client/components/boards/router.js +++ b/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); } ); diff --git a/client/components/cards/details.styl b/client/components/cards/details.styl index 68a436f9..6b1a4cd4 100644 --- a/client/components/cards/details.styl +++ b/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 diff --git a/client/components/cards/labels.styl b/client/components/cards/labels.styl index 27058b21..9514ce45 100644 --- a/client/components/cards/labels.styl +++ b/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 diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index e1176264..ad51ce22 100644 --- a/client/components/cards/minicard.jade +++ b/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 diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index b339580b..81d8c0d4 100644 --- a/client/components/cards/minicard.js +++ b/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'); diff --git a/client/components/cards/minicard.styl b/client/components/cards/minicard.styl index 775d31eb..a5110584 100644 --- a/client/components/cards/minicard.styl +++ b/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 diff --git a/client/components/cards/popups.jade b/client/components/cards/popups.jade index 0b5aa4c0..0d10c147 100644 --- a/client/components/cards/popups.jade +++ b/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 diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl index c572863d..06796170 100644 --- a/client/components/forms/forms.styl +++ b/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 diff --git a/client/components/forms/inlinedform.js b/client/components/forms/inlinedform.js index f2774084..b8442a28 100644 --- a/client/components/forms/inlinedform.js +++ b/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; } ); diff --git a/client/components/lists/body.jade b/client/components/lists/body.jade index 3e769206..3e780850 100644 --- a/client/components/lists/body.jade +++ b/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) diff --git a/client/components/lists/body.js b/client/components/lists/body.js index 8400af96..04f122cb 100644 --- a/client/components/lists/body.js +++ b/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'); diff --git a/client/components/lists/main.js b/client/components/lists/main.js index beae784d..bcdba7c4 100644 --- a/client/components/lists/main.js +++ b/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); } }); diff --git a/client/components/lists/main.styl b/client/components/lists/main.styl index 47dfcf28..3e51ac08 100644 --- a/client/components/lists/main.styl +++ b/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 diff --git a/client/components/lists/menu.jade b/client/components/lists/menu.jade index ff7820a4..052f064c 100644 --- a/client/components/lists/menu.jade +++ b/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 diff --git a/client/components/lists/menu.js b/client/components/lists/menu.js index f2abd3bf..dda1270c 100644 --- a/client/components/lists/menu.js +++ b/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) { diff --git a/client/components/main/editor.js b/client/components/main/editor.js index a35ecd06..e1a90cb1 100644 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -61,6 +61,6 @@ Template.editor.onRendered(function() { }); EscapeActions.register('textcomplete', - function() { return dropdownMenuIsOpened; }, - function() {} + function() {}, + function() { return dropdownMenuIsOpened; } ); diff --git a/client/components/main/header.styl b/client/components/main/header.styl index 248e2851..8e1682eb 100644 --- a/client/components/main/header.styl +++ b/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 diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl index 141f4261..cf1fd46e 100644 --- a/client/components/main/popup.styl +++ b/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 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 - | (<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 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 + | (<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 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 @@ +<!-- 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> 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 @@ -<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("{{url}}");"> - <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> diff --git a/client/config/router.js b/client/config/router.js index d4bc3c4f..ed9a069d 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 d96fa89c..359b65d3 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 0fbdbfd5..8b105c28 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 00000000..53c16da0 --- /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 6298ba81..46c137e8 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 521e1f56..4b78b9ec 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 538b6af4..374dcbc3 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 196477ec..1a30dbba 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 fcab8d20..60063ea8 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" } |