merge with /devel

This commit is contained in:
Xavier Priour 2015-11-13 12:43:34 +01:00
commit a7427b9ae4
27 changed files with 321 additions and 112 deletions

View file

@ -1,7 +1,14 @@
ecmaFeatures: ecmaFeatures:
experimentalObjectRestSpread: true experimentalObjectRestSpread: true
plugins:
- meteor
parser: babel-eslint
rules: rules:
strict: 0
no-undef: 2
accessor-pairs: 2 accessor-pairs: 2
comma-dangle: [2, 'always-multiline'] comma-dangle: [2, 'always-multiline']
consistent-return: 2 consistent-return: 2
@ -43,36 +50,39 @@ rules:
prefer-spread: 2 prefer-spread: 2
prefer-template: 2 prefer-template: 2
globals: # eslint-plugin-meteor
# Meteor globals ## Meteor API
Meteor: false meteor/globals: 2
DDP: false meteor/core: 2
Mongo: false meteor/pubsub: 2
Session: false meteor/methods: 2
Accounts: false meteor/check: 2
Template: false meteor/connections: 2
Blaze: false meteor/collections: 2
UI: false meteor/session: [2, 'no-equal']
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
## 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 # Exported by packages we use
'$': false
_: false
autosize: false autosize: false
Avatar: true Avatar: true
Avatars: true Avatars: true
@ -80,6 +90,7 @@ globals:
BlazeLayout: false BlazeLayout: false
DocHead: false DocHead: false
ESSearchResults: false ESSearchResults: false
FastRender: false
FlowRouter: false FlowRouter: false
FS: false FS: false
getSlug: false getSlug: false
@ -97,17 +108,6 @@ globals:
T9n: false T9n: false
TAPi18n: 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 # Our objects
CSSEvents: true CSSEvents: true
EscapeActions: true EscapeActions: true

1
.gitignore vendored
View file

@ -4,3 +4,4 @@
.tx/ .tx/
*.sublime-workspace *.sublime-workspace
tmp/ tmp/
node_modules/

View file

@ -2,9 +2,6 @@
# #
# 'meteor add' and 'meteor remove' will edit this file for you, # 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand. # 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 meteor-base
@ -52,6 +49,7 @@ audit-argument-checks
kadira:blaze-layout kadira:blaze-layout
kadira:dochead kadira:dochead
kadira:flow-router kadira:flow-router
meteorhacks:fast-render
meteorhacks:picker meteorhacks:picker
meteorhacks:subs-manager meteorhacks:subs-manager
mquandalle:autofocus mquandalle:autofocus

View file

@ -35,6 +35,7 @@ cfs:tempstore@0.1.5
cfs:upload-http@0.0.20 cfs:upload-http@0.0.20
cfs:worker@0.1.4 cfs:worker@0.1.4
check@1.1.0 check@1.1.0
chuangbo:cookie@1.1.0
coffeescript@1.0.11 coffeescript@1.0.11
cosmos:browserify@0.8.3 cosmos:browserify@0.8.3
dburles:collection-helpers@1.0.4 dburles:collection-helpers@1.0.4
@ -75,6 +76,8 @@ meteor-base@1.0.1
meteor-platform@1.2.3 meteor-platform@1.2.3
meteorhacks:aggregate@1.3.0 meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.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:picker@1.0.3
meteorhacks:subs-manager@1.6.2 meteorhacks:subs-manager@1.6.2
meteorspark:util@0.2.0 meteorspark:util@0.2.0

View file

@ -3,7 +3,6 @@ language: node_js
node_js: node_js:
- "0.10.40" - "0.10.40"
install: install:
- "npm install -g eslint" - "npm install"
- "npm install -g eslint-plugin-meteor"
script: script:
- "eslint ./" - "npm test"

View file

@ -3,9 +3,13 @@
This release features: This release features:
* Card import from Trello * 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 Thanks to GitHub users AlexanderS, fisle, FuzzyWuzzie, ndarilek, and
contributions. xavierpriour for their contributions.
# v0.9 # v0.9

View file

@ -101,9 +101,9 @@ BlazeComponent.extendComponent({
}, },
'submit .js-edit-comment'(evt) { 'submit .js-edit-comment'(evt) {
evt.preventDefault(); evt.preventDefault();
const commentText = this.currentComponent().getValue(); const commentText = this.currentComponent().getValue().trim();
const commentId = Template.parentData().commentId; const commentId = Template.parentData().commentId;
if ($.trim(commentText)) { if (commentText) {
CardComments.update(commentId, { CardComments.update(commentId, {
$set: { $set: {
text: commentText, text: commentText,

View file

@ -24,11 +24,12 @@ BlazeComponent.extendComponent({
}, },
'submit .js-new-comment-form'(evt) { 'submit .js-new-comment-form'(evt) {
const input = this.getInput(); const input = this.getInput();
if ($.trim(input.val())) { const text = input.val().trim();
if (text) {
CardComments.insert({ CardComments.insert({
text,
boardId: this.currentData().boardId, boardId: this.currentData().boardId,
cardId: this.currentData()._id, cardId: this.currentData()._id,
text: input.val(),
}); });
resetCommentInput(input); resetCommentInput(input);
Tracker.flush(); Tracker.flush();
@ -72,8 +73,9 @@ EscapeActions.register('inlinedForm',
docId: Session.get('currentCard'), docId: Session.get('currentCard'),
}; };
const commentInput = $('.js-new-comment-input'); const commentInput = $('.js-new-comment-input');
if ($.trim(commentInput.val())) { const draft = commentInput.val().trim();
UnsavedEdits.set(draftKey, commentInput.val()); if (draft) {
UnsavedEdits.set(draftKey, draft);
} else { } else {
UnsavedEdits.reset(draftKey); UnsavedEdits.reset(draftKey);
} }

View file

@ -34,7 +34,7 @@ BlazeComponent.extendComponent({
}, },
openNewListForm() { openNewListForm() {
this.childrenComponents('addListForm')[0].open(); this.childComponents('addListForm')[0].open();
}, },
// XXX Flow components allow us to avoid creating these two setter methods by // XXX Flow components allow us to avoid creating these two setter methods by
@ -45,7 +45,8 @@ BlazeComponent.extendComponent({
}, },
scrollLeft(position = 0) { scrollLeft(position = 0) {
this.$('.js-lists').animate({ const lists = this.$('.js-lists');
lists && lists.animate({
scrollLeft: position, scrollLeft: position,
}); });
}, },
@ -179,22 +180,24 @@ BlazeComponent.extendComponent({
// Proxy // Proxy
open() { open() {
this.childrenComponents('inlinedForm')[0].open(); this.childComponents('inlinedForm')[0].open();
}, },
events() { events() {
return [{ return [{
submit(evt) { submit(evt) {
evt.preventDefault(); evt.preventDefault();
const title = this.find('.list-name-input'); const titleInput = this.find('.list-name-input');
if ($.trim(title.value)) { const title = titleInput.value.trim();
if (title) {
Lists.insert({ Lists.insert({
title: title.value, title,
boardId: Session.get('currentBoard'), boardId: Session.get('currentBoard'),
sort: $('.list').length, sort: $('.list').length,
}); });
title.value = ''; titleInput.value = '';
titleInput.focus();
} }
}, },
}]; }];

View file

@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
}, },
reachNextPeak() { reachNextPeak() {
const activitiesComponent = this.childrenComponents('activities')[0]; const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage(); activitiesComponent.loadNextPage();
}, },
@ -75,8 +75,8 @@ BlazeComponent.extendComponent({
}, },
'submit .js-card-details-title'(evt) { 'submit .js-card-details-title'(evt) {
evt.preventDefault(); evt.preventDefault();
const title = this.currentComponent().getValue(); const title = this.currentComponent().getValue().trim();
if ($.trim(title)) { if (title) {
this.data().setTitle(title); this.data().setTitle(title);
} }
}, },
@ -106,7 +106,7 @@ BlazeComponent.extendComponent({
close(isReset = false) { close(isReset = false) {
if (this.isOpen.get() && !isReset) { if (this.isOpen.get() && !isReset) {
const draft = $.trim(this.getValue()); const draft = this.getValue().trim();
if (draft !== Cards.findOne(Session.get('currentCard')).description) { if (draft !== Cards.findOne(Session.get('currentCard')).description) {
UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue()); UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
} }

View file

@ -617,8 +617,15 @@ button
margin-right: 5px margin-right: 5px
vertical-align: middle vertical-align: middle
.minicard-label
width: 11px
height: @width
border-radius: 2px
margin: 2px 7px -2px -2px
display: inline-block
&.active &.active
background: #005377 background: #005377
a a, .quiet
color: white color: white

View file

@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
// Proxy // Proxy
openForm(options) { openForm(options) {
this.childrenComponents('listBody')[0].openForm(options); this.childComponents('listBody')[0].openForm(options);
}, },
onCreated() { onCreated() {

View file

@ -22,9 +22,20 @@ template(name="listBody")
template(name="addCardForm") template(name="addCardForm")
.minicard.minicard-composer.js-composer .minicard.minicard-composer.js-composer
.minicard-detailss.clearfix if getLabels
textarea.minicard-composer-textarea.js-card-title(autofocus) .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 .minicard-members.js-minicard-composer-members
each members.get
+userAvatar(userId=this)
.add-controls.clearfix .add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}} button.primary.confirm(type="submit") {{_ 'add'}}
a.fa.fa-times-thin.js-close-inlined-form 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

View file

@ -11,8 +11,8 @@ BlazeComponent.extendComponent({
options = options || {}; options = options || {};
options.position = options.position || 'top'; options.position = options.position || 'top';
const forms = this.childrenComponents('inlinedForm'); const forms = this.childComponents('inlinedForm');
let form = _.find(forms, (component) => { let form = forms.find((component) => {
return component.data().position === options.position; return component.data().position === options.position;
}); });
if (!form && forms.length > 0) { if (!form && forms.length > 0) {
@ -26,8 +26,10 @@ BlazeComponent.extendComponent({
const firstCardDom = this.find('.js-minicard:first'); const firstCardDom = this.find('.js-minicard:first');
const lastCardDom = this.find('.js-minicard:last'); const lastCardDom = this.find('.js-minicard:last');
const textarea = $(evt.currentTarget).find('textarea'); const textarea = $(evt.currentTarget).find('textarea');
const title = textarea.val();
const position = this.currentData().position; const position = this.currentData().position;
const title = textarea.val().trim();
const formComponent = this.childComponents('addCardForm')[0];
let sortIndex; let sortIndex;
if (position === 'top') { if (position === 'top') {
sortIndex = Utils.calculateIndex(null, firstCardDom).base; sortIndex = Utils.calculateIndex(null, firstCardDom).base;
@ -35,9 +37,14 @@ BlazeComponent.extendComponent({
sortIndex = Utils.calculateIndex(lastCardDom, null).base; 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({ const _id = Cards.insert({
title, title,
members,
labelIds,
listId: this.data()._id, listId: this.data()._id,
boardId: this.data().board()._id, boardId: this.data().board()._id,
sort: sortIndex, sort: sortIndex,
@ -53,6 +60,8 @@ BlazeComponent.extendComponent({
if (position === 'bottom') { if (position === 'bottom') {
this.scrollToBottom(); this.scrollToBottom();
} }
formComponent.reset();
} }
}, },
@ -100,11 +109,39 @@ BlazeComponent.extendComponent({
}, },
}).register('listBody'); }).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({ BlazeComponent.extendComponent({
template() { template() {
return 'addCardForm'; 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) { pressKey(evt) {
// Pressing Enter should submit the card // Pressing Enter should submit the card
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
@ -140,4 +177,66 @@ BlazeComponent.extendComponent({
keydown: this.pressKey, 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'); }).register('addCardForm');

View file

@ -5,10 +5,10 @@ BlazeComponent.extendComponent({
editTitle(evt) { editTitle(evt) {
evt.preventDefault(); evt.preventDefault();
const newTitle = this.childrenComponents('inlinedForm')[0].getValue(); const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
const list = this.currentData(); const list = this.currentData();
if ($.trim(newTitle)) { if (newTitle) {
list.rename(newTitle); list.rename(newTitle.trim());
} }
}, },

View file

@ -8,8 +8,8 @@ Template.editor.onRendered(() => {
{ {
match: /\B:([\-+\w]*)$/, match: /\B:([\-+\w]*)$/,
search(term, callback) { search(term, callback) {
callback($.map(Emoji.values, (emoji) => { callback(Emoji.values.map((emoji) => {
return emoji.indexOf(term) === 0 ? emoji : null; return emoji.includes(term) ? emoji : null;
})); }));
}, },
template(value) { template(value) {
@ -28,9 +28,9 @@ Template.editor.onRendered(() => {
match: /\B@(\w*)$/, match: /\B@(\w*)$/,
search(term, callback) { search(term, callback) {
const currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
callback($.map(currentBoard.members, (member) => { callback(currentBoard.members.map((member) => {
const username = Users.findOne(member.userId).username; const username = Users.findOne(member.userId).username;
return username.indexOf(term) === 0 ? username : null; return username.includes(term) ? username : null;
})); }));
}, },
template(value) { template(value) {

View file

@ -54,7 +54,7 @@ BlazeComponent.extendComponent({
}, },
reachNextPeak() { reachNextPeak() {
const activitiesComponent = this.childrenComponents('activities')[0]; const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage(); activitiesComponent.loadNextPage();
}, },

View file

@ -18,9 +18,9 @@ Template.memberMenuPopup.events({
Template.editProfilePopup.events({ Template.editProfilePopup.events({
submit(evt, tpl) { submit(evt, tpl) {
evt.preventDefault(); evt.preventDefault();
const fullname = $.trim(tpl.find('.js-profile-fullname').value); const fullname = tpl.find('.js-profile-fullname').value.trim();
const username = $.trim(tpl.find('.js-profile-username').value); const username = tpl.find('.js-profile-username').value.trim();
const initials = $.trim(tpl.find('.js-profile-initials').value); const initials = tpl.find('.js-profile-initials').value.trim();
Users.update(Meteor.userId(), {$set: { Users.update(Meteor.userId(), {$set: {
'profile.fullname': fullname, 'profile.fullname': fullname,
'profile.initials': initials, 'profile.initials': initials,
@ -41,7 +41,7 @@ Template.changePasswordPopup.onRendered(function() {
Template.changeLanguagePopup.helpers({ Template.changeLanguagePopup.helpers({
languages() { languages() {
return TAPi18n.getLanguages().map((lang, tag) => { return _.map(TAPi18n.getLanguages(), (lang, tag) => {
const name = lang.name; const name = lang.name;
return { tag, name }; return { tag, name };
}); });

View file

@ -21,7 +21,7 @@ window.Modal = new class {
} }
} }
open(modalName, { onCloseGoTo = ''}) { open(modalName, { onCloseGoTo = ''} = {}) {
this._currentModal.set(modalName); this._currentModal.set(modalName);
this._onCloseGoTo = onCloseGoTo; this._onCloseGoTo = onCloseGoTo;
} }

View file

@ -3,8 +3,23 @@
// of the vanilla `textcomplete`. // of the vanilla `textcomplete`.
let dropdownMenuIsOpened = false; let dropdownMenuIsOpened = false;
$.fn.escapeableTextComplete = function(...args) { $.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
this.textcomplete(...args); // 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 // Since commit d474017 jquery-textComplete automatically closes a potential
// opened dropdown menu when the user press Escape. This behavior conflicts // opened dropdown menu when the user press Escape. This behavior conflicts
@ -18,7 +33,14 @@ $.fn.escapeableTextComplete = function(...args) {
}, },
'textComplete:hide'() { 'textComplete:hide'() {
Tracker.afterFlush(() => { 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', EscapeActions.register('textcomplete',
() => {}, () => {},
() => dropdownMenuIsOpened () => dropdownMenuIsOpened, {
noClickEscapeOn: '.textcomplete-dropdown',
}
); );

View file

@ -1,4 +1,4 @@
Attachments = new FS.Collection('attachments', { Attachments = new FS.Collection('attachments', { // eslint-disable-line meteor/collections
stores: [ stores: [
// XXX Add a new store for cover thumbnails so we don't load big images in // XXX Add a new store for cover thumbnails so we don't load big images in

View file

@ -97,11 +97,11 @@ Boards.helpers({
}, },
labelIndex(labelId) { labelIndex(labelId) {
return _.indexOf(_.pluck(this.labels, '_id'), labelId); return _.pluck(this.labels, '_id').indexOf(labelId);
}, },
memberIndex(memberId) { memberIndex(memberId) {
return _.indexOf(_.pluck(this.members, 'userId'), memberId); return _.pluck(this.members, 'userId').indexOf(memberId);
}, },
absoluteUrl() { absoluteUrl() {

View file

@ -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 // 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. // is used for instance to add a new user to a board.
@ -8,7 +8,23 @@ Users.initEasySearch(searchInFields, {
returnFields: [...searchInFields, 'profile.avatarUrl'], 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({ Users.helpers({
boards() { boards() {
@ -25,18 +41,6 @@ Users.helpers({
return _.contains(starredBoards, boardId); 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() { getAvatarUrl() {
// Although we put the avatar picture URL in the `profile` object, we need // Although we put the avatar picture URL in the `profile` object, we need
// to support Sandstorm which put in the `picture` attribute by default. // to support Sandstorm which put in the `picture` attribute by default.

24
package.json Normal file
View file

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

View file

@ -22,12 +22,12 @@ if (isSandstorm && Meteor.isServer) {
}; };
function updateUserPermissions(userId, permissions) { function updateUserPermissions(userId, permissions) {
const isActive = permissions.includes('participate'); const isActive = permissions.indexOf('participate') > -1;
const isAdmin = permissions.includes('configure'); const isAdmin = permissions.indexOf('configure') > -1;
const permissionDoc = { userId, isActive, isAdmin }; const permissionDoc = { userId, isActive, isAdmin };
const boardMembers = Boards.findOne(sandstormBoard._id).members; const boardMembers = Boards.findOne(sandstormBoard._id).members;
const memberIndex = _.indexOf(_.pluck(boardMembers, 'userId'), userId); const memberIndex = _.pluck(boardMembers, 'userId').indexOf(userId);
let modifier; let modifier;
if (memberIndex > -1) if (memberIndex > -1)
@ -78,17 +78,40 @@ if (isSandstorm && Meteor.isServer) {
// unique board document. Note that when the `Users.after.insert` hook is // unique board document. Note that when the `Users.after.insert` hook is
// called, the user is inserted into the database but not connected. So // called, the user is inserted into the database but not connected. So
// despite the appearances `userId` is null in this block. // despite the appearances `userId` is null in this block.
//
// XXX We should support the `preferredHandle` exposed by Sandstorm
Users.after.insert((userId, doc) => { Users.after.insert((userId, doc) => {
if (!Boards.findOne(sandstormBoard._id)) { if (!Boards.findOne(sandstormBoard._id)) {
Boards.insert(sandstormBoard, {validate: false}); Boards.insert(sandstormBoard, { validate: false });
Activities.update( Activities.update(
{ activityTypeId: sandstormBoard._id }, { activityTypeId: sandstormBoard._id },
{ $set: { userId: doc._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); updateUserPermissions(doc._id, doc.services.sandstorm.permissions);
}); });
@ -109,7 +132,7 @@ if (isSandstorm && Meteor.isClient) {
// sandstorm client to return relative paths instead of absolutes. // sandstorm client to return relative paths instead of absolutes.
const _absoluteUrl = Meteor.absoluteUrl; const _absoluteUrl = Meteor.absoluteUrl;
const _defaultOptions = Meteor.absoluteUrl.defaultOptions; const _defaultOptions = Meteor.absoluteUrl.defaultOptions;
Meteor.absoluteUrl = (path, options) => { Meteor.absoluteUrl = (path, options) => { // eslint-disable-line meteor/core
const url = _absoluteUrl(path, options); const url = _absoluteUrl(path, options);
return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, ''); return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, '');
}; };

View file

@ -0,0 +1,7 @@
FastRender.onAllRoutes(function() {
this.subscribe('boards');
});
FastRender.route('/b/:id/:slug', function({ id }) {
this.subscribe('board', id);
});