diff options
author | Xavier Priour <xavier.priour@bubblyware.com> | 2015-11-13 12:43:34 +0100 |
---|---|---|
committer | Xavier Priour <xavier.priour@bubblyware.com> | 2015-11-13 12:43:34 +0100 |
commit | a7427b9ae4e790989c49408ffe14aea4976db305 (patch) | |
tree | 3b56c753ce05ba9a5253078e2792b2c485d2912e | |
parent | 3dc70b3ea4d49bccb10d4f2b3e317c794da91e37 (diff) | |
parent | cb3bd86396e4d19e1f05fcb94e3527f81e70412e (diff) | |
download | wekan-a7427b9ae4e790989c49408ffe14aea4976db305.tar.gz wekan-a7427b9ae4e790989c49408ffe14aea4976db305.tar.bz2 wekan-a7427b9ae4e790989c49408ffe14aea4976db305.zip |
merge with /devel
27 files changed, 322 insertions, 113 deletions
@@ -1,7 +1,14 @@ ecmaFeatures: experimentalObjectRestSpread: true +plugins: + - meteor + +parser: babel-eslint + rules: + strict: 0 + no-undef: 2 accessor-pairs: 2 comma-dangle: [2, 'always-multiline'] consistent-return: 2 @@ -43,36 +50,39 @@ rules: prefer-spread: 2 prefer-template: 2 -globals: - # Meteor globals - Meteor: false - DDP: false - Mongo: false - Session: false - Accounts: false - Template: false - Blaze: false - UI: false - Match: false - check: false - Tracker: false - Deps: false - ReactiveVar: false - EJSON: false - HTTP: false - Email: false - Assets: false - Handlebars: false - Package: false - App: false - Npm: false - Tinytest: false - Random: false - HTML: false + # eslint-plugin-meteor + ## Meteor API + meteor/globals: 2 + meteor/core: 2 + meteor/pubsub: 2 + meteor/methods: 2 + meteor/check: 2 + meteor/connections: 2 + meteor/collections: 2 + meteor/session: [2, 'no-equal'] + + ## Best practices + meteor/no-session: 0 + meteor/no-zero-timeout: 2 + meteor/no-blaze-lifecycle-assignment: 2 +settings: + meteor: + + # Our collections + collections: + - AccountsTemplates + - Activities + - Attachments + - Boards + - CardComments + - Cards + - Lists + - UnsavedEditCollection + - Users + +globals: # Exported by packages we use - '$': false - _: false autosize: false Avatar: true Avatars: true @@ -80,6 +90,7 @@ globals: BlazeLayout: false DocHead: false ESSearchResults: false + FastRender: false FlowRouter: false FS: false getSlug: false @@ -97,17 +108,6 @@ globals: T9n: false TAPi18n: false - # Our collections - AccountsTemplates: true - Activities: true - Attachments: true - Boards: true - CardComments: true - Cards: true - Lists: true - UnsavedEditCollection: true - Users: true - # Our objects CSSEvents: true EscapeActions: true @@ -4,3 +4,4 @@ .tx/ *.sublime-workspace tmp/ +node_modules/ diff --git a/.meteor/packages b/.meteor/packages index 39242f48..bdb0b60e 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -2,9 +2,6 @@ # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -# -# XXX Should we replace tmeasday:presence by 3stack:presence? Or maybe the -# packages will merge in the future? meteor-base @@ -52,6 +49,7 @@ audit-argument-checks kadira:blaze-layout kadira:dochead kadira:flow-router +meteorhacks:fast-render meteorhacks:picker meteorhacks:subs-manager mquandalle:autofocus diff --git a/.meteor/versions b/.meteor/versions index 44942295..e5de3720 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -35,6 +35,7 @@ cfs:tempstore@0.1.5 cfs:upload-http@0.0.20 cfs:worker@0.1.4 check@1.1.0 +chuangbo:cookie@1.1.0 coffeescript@1.0.11 cosmos:browserify@0.8.3 dburles:collection-helpers@1.0.4 @@ -75,6 +76,8 @@ meteor-base@1.0.1 meteor-platform@1.2.3 meteorhacks:aggregate@1.3.0 meteorhacks:collection-utils@1.2.0 +meteorhacks:fast-render@2.10.0 +meteorhacks:inject-data@1.4.1 meteorhacks:picker@1.0.3 meteorhacks:subs-manager@1.6.2 meteorspark:util@0.2.0 diff --git a/.travis.yml b/.travis.yml index 55f8ed48..a8724631 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: node_js node_js: - "0.10.40" install: - - "npm install -g eslint" - - "npm install -g eslint-plugin-meteor" + - "npm install" script: - - "eslint ./" + - "npm test" @@ -3,9 +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/activities/activities.js b/client/components/activities/activities.js index a491c2e8..64e9865d 100644 --- a/client/components/activities/activities.js +++ b/client/components/activities/activities.js @@ -101,9 +101,9 @@ BlazeComponent.extendComponent({ }, 'submit .js-edit-comment'(evt) { evt.preventDefault(); - const commentText = this.currentComponent().getValue(); + const commentText = this.currentComponent().getValue().trim(); const commentId = Template.parentData().commentId; - if ($.trim(commentText)) { + if (commentText) { CardComments.update(commentId, { $set: { text: commentText, diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js index 08401caa..18bf9ef0 100644 --- a/client/components/activities/comments.js +++ b/client/components/activities/comments.js @@ -24,11 +24,12 @@ BlazeComponent.extendComponent({ }, 'submit .js-new-comment-form'(evt) { const input = this.getInput(); - if ($.trim(input.val())) { + const text = input.val().trim(); + if (text) { CardComments.insert({ + text, boardId: this.currentData().boardId, cardId: this.currentData()._id, - text: input.val(), }); resetCommentInput(input); Tracker.flush(); @@ -72,8 +73,9 @@ EscapeActions.register('inlinedForm', docId: Session.get('currentCard'), }; const commentInput = $('.js-new-comment-input'); - if ($.trim(commentInput.val())) { - UnsavedEdits.set(draftKey, commentInput.val()); + const draft = commentInput.val().trim(); + if (draft) { + UnsavedEdits.set(draftKey, draft); } else { UnsavedEdits.reset(draftKey); } diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 517e53ba..5a74e61b 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -34,7 +34,7 @@ BlazeComponent.extendComponent({ }, openNewListForm() { - this.childrenComponents('addListForm')[0].open(); + this.childComponents('addListForm')[0].open(); }, // XXX Flow components allow us to avoid creating these two setter methods by @@ -45,7 +45,8 @@ BlazeComponent.extendComponent({ }, scrollLeft(position = 0) { - this.$('.js-lists').animate({ + const lists = this.$('.js-lists'); + lists && lists.animate({ scrollLeft: position, }); }, @@ -179,22 +180,24 @@ BlazeComponent.extendComponent({ // Proxy open() { - this.childrenComponents('inlinedForm')[0].open(); + this.childComponents('inlinedForm')[0].open(); }, events() { return [{ submit(evt) { evt.preventDefault(); - const title = this.find('.list-name-input'); - if ($.trim(title.value)) { + const titleInput = this.find('.list-name-input'); + const title = titleInput.value.trim(); + if (title) { Lists.insert({ - title: title.value, + title, boardId: Session.get('currentBoard'), sort: $('.list').length, }); - title.value = ''; + titleInput.value = ''; + titleInput.focus(); } }, }]; diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js index 2d2679ec..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(); }, @@ -75,8 +75,8 @@ BlazeComponent.extendComponent({ }, 'submit .js-card-details-title'(evt) { evt.preventDefault(); - const title = this.currentComponent().getValue(); - if ($.trim(title)) { + const title = this.currentComponent().getValue().trim(); + if (title) { this.data().setTitle(title); } }, @@ -106,7 +106,7 @@ BlazeComponent.extendComponent({ close(isReset = false) { if (this.isOpen.get() && !isReset) { - const draft = $.trim(this.getValue()); + const draft = this.getValue().trim(); if (draft !== Cards.findOne(Session.get('currentCard')).description) { UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue()); } 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 88b31788..36b60d06 100644 --- a/client/components/lists/listBody.js +++ b/client/components/lists/listBody.js @@ -11,8 +11,8 @@ BlazeComponent.extendComponent({ options = options || {}; options.position = options.position || 'top'; - const forms = this.childrenComponents('inlinedForm'); - let form = _.find(forms, (component) => { + const forms = this.childComponents('inlinedForm'); + let form = forms.find((component) => { return component.data().position === options.position; }); if (!form && forms.length > 0) { @@ -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(); 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; } - if ($.trim(title)) { + 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 b5df2c81..d660508a 100644 --- a/client/components/lists/listHeader.js +++ b/client/components/lists/listHeader.js @@ -5,10 +5,10 @@ BlazeComponent.extendComponent({ editTitle(evt) { evt.preventDefault(); - const newTitle = this.childrenComponents('inlinedForm')[0].getValue(); + const newTitle = this.childComponents('inlinedForm')[0].getValue().trim(); const list = this.currentData(); - if ($.trim(newTitle)) { - list.rename(newTitle); + if (newTitle) { + list.rename(newTitle.trim()); } }, diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 168c85d0..82fce641 100644 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -8,8 +8,8 @@ Template.editor.onRendered(() => { { match: /\B:([\-+\w]*)$/, search(term, callback) { - callback($.map(Emoji.values, (emoji) => { - return emoji.indexOf(term) === 0 ? emoji : null; + callback(Emoji.values.map((emoji) => { + return emoji.includes(term) ? emoji : null; })); }, template(value) { @@ -28,9 +28,9 @@ Template.editor.onRendered(() => { match: /\B@(\w*)$/, search(term, callback) { const currentBoard = Boards.findOne(Session.get('currentBoard')); - callback($.map(currentBoard.members, (member) => { + callback(currentBoard.members.map((member) => { const username = Users.findOne(member.userId).username; - return username.indexOf(term) === 0 ? username : null; + return username.includes(term) ? username : null; })); }, template(value) { 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/components/users/userHeader.js b/client/components/users/userHeader.js index a7382769..a478da0c 100644 --- a/client/components/users/userHeader.js +++ b/client/components/users/userHeader.js @@ -18,9 +18,9 @@ Template.memberMenuPopup.events({ Template.editProfilePopup.events({ submit(evt, tpl) { evt.preventDefault(); - const fullname = $.trim(tpl.find('.js-profile-fullname').value); - const username = $.trim(tpl.find('.js-profile-username').value); - const initials = $.trim(tpl.find('.js-profile-initials').value); + const fullname = tpl.find('.js-profile-fullname').value.trim(); + const username = tpl.find('.js-profile-username').value.trim(); + const initials = tpl.find('.js-profile-initials').value.trim(); Users.update(Meteor.userId(), {$set: { 'profile.fullname': fullname, 'profile.initials': initials, @@ -41,7 +41,7 @@ Template.changePasswordPopup.onRendered(function() { Template.changeLanguagePopup.helpers({ languages() { - return TAPi18n.getLanguages().map((lang, tag) => { + return _.map(TAPi18n.getLanguages(), (lang, tag) => { const name = lang.name; return { tag, name }; }); diff --git a/client/lib/modal.js b/client/lib/modal.js index 7b7516e0..e6301cb5 100644 --- a/client/lib/modal.js +++ b/client/lib/modal.js @@ -21,7 +21,7 @@ window.Modal = new class { } } - open(modalName, { onCloseGoTo = ''}) { + open(modalName, { onCloseGoTo = ''} = {}) { this._currentModal.set(modalName); this._onCloseGoTo = onCloseGoTo; } 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/models/attachments.js b/models/attachments.js index 8ef0fef0..01e467ff 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,4 +1,4 @@ -Attachments = new FS.Collection('attachments', { +Attachments = new FS.Collection('attachments', { // eslint-disable-line meteor/collections stores: [ // XXX Add a new store for cover thumbnails so we don't load big images in diff --git a/models/boards.js b/models/boards.js index 1d365a95..98d6ec77 100644 --- a/models/boards.js +++ b/models/boards.js @@ -97,11 +97,11 @@ Boards.helpers({ }, labelIndex(labelId) { - return _.indexOf(_.pluck(this.labels, '_id'), labelId); + return _.pluck(this.labels, '_id').indexOf(labelId); }, memberIndex(memberId) { - return _.indexOf(_.pluck(this.members, 'userId'), memberId); + return _.pluck(this.members, 'userId').indexOf(memberId); }, absoluteUrl() { diff --git a/models/users.js b/models/users.js index 60cab56d..e85671bc 100644 --- a/models/users.js +++ b/models/users.js @@ -1,4 +1,4 @@ -Users = Meteor.users; +Users = Meteor.users; // eslint-disable-line meteor/collections // Search a user in the complete server database by its name or username. This // is used for instance to add a new user to a board. @@ -8,7 +8,23 @@ Users.initEasySearch(searchInFields, { returnFields: [...searchInFields, 'profile.avatarUrl'], }); - +if (Meteor.isClient) { + Users.helpers({ + isBoardMember() { + const board = Boards.findOne(Session.get('currentBoard')); + return board && + _.contains(_.pluck(board.members, 'userId'), this._id) && + _.where(board.members, {userId: this._id})[0].isActive; + }, + + isBoardAdmin() { + const board = Boards.findOne(Session.get('currentBoard')); + return board && + this.isBoardMember(board) && + _.where(board.members, {userId: this._id})[0].isAdmin; + }, + }); +} Users.helpers({ boards() { @@ -25,18 +41,6 @@ Users.helpers({ return _.contains(starredBoards, boardId); }, - isBoardMember() { - const board = Boards.findOne(Session.get('currentBoard')); - return board && _.contains(_.pluck(board.members, 'userId'), this._id) && - _.where(board.members, {userId: this._id})[0].isActive; - }, - - isBoardAdmin() { - const board = Boards.findOne(Session.get('currentBoard')); - return board && this.isBoardMember(board) && - _.where(board.members, {userId: this._id})[0].isAdmin; - }, - getAvatarUrl() { // Although we put the avatar picture URL in the `profile` object, we need // to support Sandstorm which put in the `picture` attribute by default. diff --git a/package.json b/package.json new file mode 100644 index 00000000..77a9fb74 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "wekan", + "version": "1.0.0", + "description": "The open-source Trello-like kanban", + "private": true, + "scripts": { + "lint": "eslint .", + "test": "npm run --silent lint" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wekan/wekan.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/wekan/wekan/issues" + }, + "homepage": "http://wekan.io", + "devDependencies": { + "babel-eslint": "4.1.3", + "eslint": "1.7.3", + "eslint-plugin-meteor": "1.7.0" + } +} diff --git a/sandstorm.js b/sandstorm.js index b3006454..997aed46 100644 --- a/sandstorm.js +++ b/sandstorm.js @@ -22,12 +22,12 @@ 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; - const memberIndex = _.indexOf(_.pluck(boardMembers, 'userId'), userId); + const memberIndex = _.pluck(boardMembers, 'userId').indexOf(userId); let modifier; if (memberIndex > -1) @@ -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); }); @@ -109,7 +132,7 @@ if (isSandstorm && Meteor.isClient) { // sandstorm client to return relative paths instead of absolutes. const _absoluteUrl = Meteor.absoluteUrl; const _defaultOptions = Meteor.absoluteUrl.defaultOptions; - Meteor.absoluteUrl = (path, options) => { + Meteor.absoluteUrl = (path, options) => { // eslint-disable-line meteor/core const url = _absoluteUrl(path, options); return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, ''); }; diff --git a/server/publications/fast-render.js b/server/publications/fast-render.js new file mode 100644 index 00000000..e28b6f2e --- /dev/null +++ b/server/publications/fast-render.js @@ -0,0 +1,7 @@ +FastRender.onAllRoutes(function() { + this.subscribe('boards'); +}); + +FastRender.route('/b/:id/:slug', function({ id }) { + this.subscribe('board', id); +}); |