diff options
-rw-r--r-- | .meteor/versions | 8 | ||||
-rw-r--r-- | History.md | 6 | ||||
-rw-r--r-- | client/components/boards/boardBody.js | 3 | ||||
-rw-r--r-- | client/components/cards/cardDetails.js | 2 | ||||
-rw-r--r-- | client/components/forms/forms.styl | 9 | ||||
-rw-r--r-- | client/components/lists/list.js | 2 | ||||
-rw-r--r-- | client/components/lists/listBody.jade | 15 | ||||
-rw-r--r-- | client/components/lists/listBody.js | 103 | ||||
-rw-r--r-- | client/components/lists/listHeader.js | 2 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.jade | 2 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.js | 2 | ||||
-rw-r--r-- | client/lib/textComplete.js | 32 | ||||
-rw-r--r-- | config/accounts.js (renamed from client/config/accounts.js) | 0 | ||||
-rw-r--r-- | sandstorm.js | 33 |
14 files changed, 193 insertions, 26 deletions
diff --git a/.meteor/versions b/.meteor/versions index db5d11e7..5c4c189b 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -37,7 +37,7 @@ cfs:worker@0.1.4 check@1.1.0 chuangbo:cookie@1.1.0 coffeescript@1.0.11 -cosmos:browserify@0.8.1 +cosmos:browserify@0.8.3 dburles:collection-helpers@1.0.4 ddp@1.2.2 ddp-client@1.2.1 @@ -63,8 +63,8 @@ idmontie:migrations@1.0.1 jquery@1.11.4 kadira:blaze-layout@2.2.0 kadira:dochead@1.3.2 -kadira:flow-router@2.7.0 -kenton:accounts-sandstorm@0.1.7 +kadira:flow-router@2.8.0 +kenton:accounts-sandstorm@0.1.8 launch-screen@1.0.4 livedata@1.0.15 localstorage@1.0.5 @@ -106,7 +106,7 @@ ordered-dict@1.0.4 peerlibrary:assert@0.2.5 peerlibrary:base-component@0.14.0 peerlibrary:blaze-components@0.15.1 -peerlibrary:computed-field@0.3.0 +peerlibrary:computed-field@0.3.1 peerlibrary:reactive-field@0.1.0 perak:markdown@1.0.5 promise@0.5.1 @@ -3,11 +3,13 @@ This release features: * Card import from Trello +* Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start the + a board member autocompletion, or <kbd>#</kbd> for a label. * Accelerate the initial page rendering by sending the data on the intial HTTP response instead of waiting for the DDP connection to open. -Thanks to GitHub users AlexanderS, fisle, ndarilek, and xavierpriour for their -contributions. +Thanks to GitHub users AlexanderS, fisle, FuzzyWuzzie, ndarilek, and +xavierpriour for their contributions. # v0.9 diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 5c1c974f..5a74e61b 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -45,7 +45,8 @@ BlazeComponent.extendComponent({ }, scrollLeft(position = 0) { - this.$('.js-lists').animate({ + const lists = this.$('.js-lists'); + lists && lists.animate({ scrollLeft: position, }); }, diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index fa818c5a..b4fdca52 100644 --- a/client/components/cards/cardDetails.js +++ b/client/components/cards/cardDetails.js @@ -13,7 +13,7 @@ BlazeComponent.extendComponent({ }, reachNextPeak() { - const activitiesComponent = this.childrenComponents('activities')[0]; + const activitiesComponent = this.childComponents('activities')[0]; activitiesComponent.loadNextPage(); }, diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl index 83d25370..9ae95140 100644 --- a/client/components/forms/forms.styl +++ b/client/components/forms/forms.styl @@ -617,8 +617,15 @@ button margin-right: 5px vertical-align: middle + .minicard-label + width: 11px + height: @width + border-radius: 2px + margin: 2px 7px -2px -2px + display: inline-block + &.active background: #005377 - a + a, .quiet color: white diff --git a/client/components/lists/list.js b/client/components/lists/list.js index 75e816b5..f5410ed0 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -7,7 +7,7 @@ BlazeComponent.extendComponent({ // Proxy openForm(options) { - this.childrenComponents('listBody')[0].openForm(options); + this.childComponents('listBody')[0].openForm(options); }, onCreated() { diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade index b0a374ea..e659b179 100644 --- a/client/components/lists/listBody.jade +++ b/client/components/lists/listBody.jade @@ -22,9 +22,20 @@ template(name="listBody") template(name="addCardForm") .minicard.minicard-composer.js-composer - .minicard-detailss.clearfix - textarea.minicard-composer-textarea.js-card-title(autofocus) + if getLabels + .minicard-labels + each getLabels + .minicard-label(class="card-label-{{color}}" title="{{name}}") + textarea.minicard-composer-textarea.js-card-title(autofocus) + if members.get .minicard-members.js-minicard-composer-members + each members.get + +userAvatar(userId=this) + .add-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} a.fa.fa-times-thin.js-close-inlined-form + +template(name="autocompleteLabelLine") + .minicard-label(class="card-label-{{colorName}}" title=labelName) + span(class="{{#if hasNoName}}quiet{{/if}}")= labelName diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js index 25aeffcc..36b60d06 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -11,7 +11,7 @@ BlazeComponent.extendComponent({ options = options || {}; options.position = options.position || 'top'; - const forms = this.childrenComponents('inlinedForm'); + const forms = this.childComponents('inlinedForm'); let form = forms.find((component) => { return component.data().position === options.position; }); @@ -26,8 +26,10 @@ BlazeComponent.extendComponent({ const firstCardDom = this.find('.js-minicard:first'); const lastCardDom = this.find('.js-minicard:last'); const textarea = $(evt.currentTarget).find('textarea'); - const title = textarea.val().trim(); const position = this.currentData().position; + const title = textarea.val().trim(); + + const formComponent = this.childComponents('addCardForm')[0]; let sortIndex; if (position === 'top') { sortIndex = Utils.calculateIndex(null, firstCardDom).base; @@ -35,9 +37,14 @@ BlazeComponent.extendComponent({ sortIndex = Utils.calculateIndex(lastCardDom, null).base; } + const members = formComponent.members.get(); + const labelIds = formComponent.labels.get(); + if (title) { const _id = Cards.insert({ title, + members, + labelIds, listId: this.data()._id, boardId: this.data().board()._id, sort: sortIndex, @@ -53,6 +60,8 @@ BlazeComponent.extendComponent({ if (position === 'bottom') { this.scrollToBottom(); } + + formComponent.reset(); } }, @@ -100,11 +109,39 @@ BlazeComponent.extendComponent({ }, }).register('listBody'); +function toggleValueInReactiveArray(reactiveValue, value) { + const array = reactiveValue.get(); + const valueIndex = array.indexOf(value); + if (valueIndex === -1) { + array.push(value); + } else { + array.splice(valueIndex, 1); + } + reactiveValue.set(array); +} + BlazeComponent.extendComponent({ template() { return 'addCardForm'; }, + onCreated() { + this.labels = new ReactiveVar([]); + this.members = new ReactiveVar([]); + }, + + reset() { + this.labels.set([]); + this.members.set([]); + }, + + getLabels() { + const currentBoardId = Session.get('currentBoard'); + return Boards.findOne(currentBoardId).labels.filter((label) => { + return this.labels.get().indexOf(label._id) > -1; + }); + }, + pressKey(evt) { // Pressing Enter should submit the card if (evt.keyCode === 13) { @@ -140,4 +177,66 @@ BlazeComponent.extendComponent({ keydown: this.pressKey, }]; }, + + onRendered() { + const editor = this; + this.$('textarea').escapeableTextComplete([ + // User mentions + { + match: /\B@(\w*)$/, + search(term, callback) { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + callback($.map(currentBoard.members, (member) => { + const user = Users.findOne(member.userId); + return user.username.indexOf(term) === 0 ? user : null; + })); + }, + template(user) { + return user.username; + }, + replace(user) { + toggleValueInReactiveArray(editor.members, user._id); + return ''; + }, + index: 1, + }, + + // Labels + { + match: /\B#(\w*)$/, + search(term, callback) { + const currentBoard = Boards.findOne(Session.get('currentBoard')); + callback($.map(currentBoard.labels, (label) => { + if (label.name.indexOf(term) > -1 || + label.color.indexOf(term) > -1) { + return label; + } + })); + }, + template(label) { + return Blaze.toHTMLWithData(Template.autocompleteLabelLine, { + hasNoName: !Boolean(label.name), + colorName: label.color, + labelName: label.name || label.color, + }); + }, + replace(label) { + toggleValueInReactiveArray(editor.labels, label._id); + return ''; + }, + index: 1, + }, + ], { + // When the autocomplete menu is shown we want both a press of both `Tab` + // or `Enter` to validation the auto-completion. We also need to stop the + // event propagation to prevent the card from submitting (on `Enter`) or + // going on the next column (on `Tab`). + onKeydown(evt, commands) { + if (evt.keyCode === 9 || evt.keyCode === 13) { + evt.stopPropagation(); + return commands.KEY_ENTER; + } + }, + }); + }, }).register('addCardForm'); diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js index dbf9fced..d660508a 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -5,7 +5,7 @@ BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); - const newTitle = this.childrenComponents('inlinedForm')[0].getValue().trim(); + const newTitle = this.childComponents('inlinedForm')[0].getValue().trim(); const list = this.currentData(); if (newTitle) { list.rename(newTitle.trim()); diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 91047056..f98ea4ee 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -89,7 +89,7 @@ template(name="addMemberPopup") a.name.js-select-member(title="{{profile.name}} ({{username}})") +userAvatar(userId=_id esSearch=true) span.full-name - = profile.name + = profile.fullname | (<span class="username">{{username}}</span>) if isBoardMember .quiet ({{_ 'joined'}}) diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index ccb9f2f5..ef071fe0 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -54,7 +54,7 @@ BlazeComponent.extendComponent({ }, reachNextPeak() { - const activitiesComponent = this.childrenComponents('activities')[0]; + const activitiesComponent = this.childComponents('activities')[0]; activitiesComponent.loadNextPage(); }, diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js index e50d7cbc..3e69d07f 100644 --- a/client/lib/textComplete.js +++ b/client/lib/textComplete.js @@ -3,8 +3,23 @@ // of the vanilla `textcomplete`. let dropdownMenuIsOpened = false; -$.fn.escapeableTextComplete = function(...args) { - this.textcomplete(...args); +$.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) { + // When the autocomplete menu is shown we want both a press of both `Tab` + // or `Enter` to validation the auto-completion. We also need to stop the + // event propagation to prevent EscapeActions side effect, for instance the + // minicard submission (on `Enter`) or going on the next column (on `Tab`). + options = { + onKeydown(evt, commands) { + if (evt.keyCode === 9 || evt.keyCode === 13) { + evt.stopPropagation(); + return commands.KEY_ENTER; + } + }, + ...options, + }; + + // Proxy to the vanilla jQuery component + this.textcomplete(strategies, options, ...otherArgs); // Since commit d474017 jquery-textComplete automatically closes a potential // opened dropdown menu when the user press Escape. This behavior conflicts @@ -18,7 +33,14 @@ $.fn.escapeableTextComplete = function(...args) { }, 'textComplete:hide'() { Tracker.afterFlush(() => { - dropdownMenuIsOpened = false; + // XXX Hack. We unfortunately need to set a setTimeout here to make the + // `noClickEscapeOn` work bellow, otherwise clicking on a autocomplete + // item will close both the autocomplete menu (as expected) but also the + // next item in the stack (for example the minicard editor) which we + // don't want. + setTimeout(() => { + dropdownMenuIsOpened = false; + }, 100); }); }, }); @@ -26,5 +48,7 @@ $.fn.escapeableTextComplete = function(...args) { EscapeActions.register('textcomplete', () => {}, - () => dropdownMenuIsOpened + () => dropdownMenuIsOpened, { + noClickEscapeOn: '.textcomplete-dropdown', + } ); diff --git a/client/config/accounts.js b/config/accounts.js index d475e6b2..d475e6b2 100644 --- a/client/config/accounts.js +++ b/config/accounts.js diff --git a/sandstorm.js b/sandstorm.js index 65f24866..997aed46 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -22,8 +22,8 @@ if (isSandstorm && Meteor.isServer) { }; function updateUserPermissions(userId, permissions) { - const isActive = permissions.includes('participate'); - const isAdmin = permissions.includes('configure'); + const isActive = permissions.indexOf('participate') > -1; + const isAdmin = permissions.indexOf('configure') > -1; const permissionDoc = { userId, isActive, isAdmin }; const boardMembers = Boards.findOne(sandstormBoard._id).members; @@ -78,17 +78,40 @@ if (isSandstorm && Meteor.isServer) { // unique board document. Note that when the `Users.after.insert` hook is // called, the user is inserted into the database but not connected. So // despite the appearances `userId` is null in this block. - // - // XXX We should support the `preferredHandle` exposed by Sandstorm Users.after.insert((userId, doc) => { if (!Boards.findOne(sandstormBoard._id)) { - Boards.insert(sandstormBoard, {validate: false}); + Boards.insert(sandstormBoard, { validate: false }); Activities.update( { activityTypeId: sandstormBoard._id }, { $set: { userId: doc._id }} ); } + // We rely on username uniqueness for the user mention feature, but + // Sandstorm doesn't enforce this property -- see #352. Our strategy to + // generate unique usernames from the Sandstorm `preferredHandle` is to + // append a number that we increment until we generate a username that no + // one already uses (eg, 'max', 'max1', 'max2'). + function generateUniqueUsername(username, appendNumber) { + return username + String(appendNumber === 0 ? '' : appendNumber); + } + + const username = doc.services.sandstorm.preferredHandle; + let appendNumber = 0; + while (Users.findOne({ + _id: { $ne: doc._id }, + username: generateUniqueUsername(username, appendNumber), + })) { + appendNumber += 1; + } + + Users.update(doc._id, { + $set: { + username: generateUniqueUsername(username, appendNumber), + 'profile.fullname': doc.services.sandstorm.name, + }, + }); + updateUserPermissions(doc._id, doc.services.sandstorm.permissions); }); |