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.
pull/188/head
Maxime Quandalle 10 years ago
parent 6457615e6a
commit 2c0030da62
  1. 1
      .gitignore
  2. 142
      .jscsrc
  3. 3
      .jshintrc
  4. 5
      client/components/boards/boardBody.jade
  5. 19
      client/components/boards/boardBody.js
  6. 5
      client/components/boards/boardBody.styl
  7. 32
      client/components/boards/boardHeader.jade
  8. 21
      client/components/boards/boardHeader.js
  9. 24
      client/components/boards/colors.styl
  10. 8
      client/components/boards/router.js
  11. 27
      client/components/cards/details.styl
  12. 5
      client/components/cards/labels.styl
  13. 16
      client/components/cards/minicard.jade
  14. 25
      client/components/cards/minicard.js
  15. 119
      client/components/cards/minicard.styl
  16. 5
      client/components/cards/popups.jade
  17. 68
      client/components/forms/forms.styl
  18. 4
      client/components/forms/inlinedform.js
  19. 9
      client/components/lists/body.jade
  20. 23
      client/components/lists/body.js
  21. 61
      client/components/lists/main.js
  22. 5
      client/components/lists/main.styl
  23. 1
      client/components/lists/menu.jade
  24. 8
      client/components/lists/menu.js
  25. 4
      client/components/main/editor.js
  26. 3
      client/components/main/header.styl
  27. 239
      client/components/main/popup.styl
  28. 17
      client/components/sidebar/events.js
  29. 14
      client/components/sidebar/helpers.js
  30. 39
      client/components/sidebar/sidebar.jade
  31. 25
      client/components/sidebar/sidebar.js
  32. 30
      client/components/sidebar/sidebar.styl
  33. 57
      client/components/sidebar/sidebarFilters.jade
  34. 94
      client/components/sidebar/sidebarFilters.js
  35. 77
      client/components/sidebar/templates.html
  36. 307
      client/components/sidebar/templates.html.old
  37. 34
      client/config/router.js
  38. 11
      client/lib/filter.js
  39. 13
      client/lib/keyboard.js
  40. 159
      client/lib/multiSelection.js
  41. 4
      client/lib/popup.js
  42. 38
      client/styles/main.styl
  43. 6
      collections/cards.js
  44. 2
      collections/lists.js
  45. 7
      i18n/en.i18n.json

1
.gitignore vendored

@ -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,

@ -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)

@ -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) {

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

@ -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

@ -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();
}
}];
}

@ -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)

@ -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); }
);

@ -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

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

@ -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

@ -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');

@ -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

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

@ -30,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

@ -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; }
);

@ -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)

@ -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');

@ -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);
}
});

@ -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

@ -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

@ -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) {

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

@ -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

@ -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

@ -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)

@ -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);

@ -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

@ -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; }
);

@ -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

@ -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

@ -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();
}
});

@ -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>

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

@ -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');
// }
// });

@ -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();
}
},

@ -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();
}
};

@ -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(); }
);

@ -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)
);

@ -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

@ -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 }});

@ -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'] });

@ -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"
}

Loading…
Cancel
Save