Enforce a consistent ES6 coding style

Replace the old (and broken) jshint + jscsrc by eslint and configure
it to support some of the ES6 features.

The command `eslint` currently has one error which is a bug that was
discovered by its static analysis and should be fixed (usage of a
dead object).
This commit is contained in:
Maxime Quandalle 2015-09-03 23:12:46 +02:00
parent 039cfe7edf
commit b3851817ec
60 changed files with 1604 additions and 1692 deletions

157
.eslintrc Normal file
View file

@ -0,0 +1,157 @@
ecmaFeatures:
experimentalObjectRestSpread: true
rules:
indent:
- 2
- 2
semi:
- 2
- always
comma-dangle:
- 2
- always-multiline
no-inner-declarations:
- 0
dot-notation:
- 2
eqeqeq:
- 2
no-eval:
- 2
radix:
- 2
# Stylistic Issues
camelcase:
- 2
comma-spacing:
- 2
comma-style:
- 2
new-parens:
- 2
no-lonely-if:
- 2
no-multiple-empty-lines:
- 2
no-nested-ternary:
- 2
linebreak-style:
- 2
- unix
quotes:
- 2
- single
semi-spacing:
- 2
spaced-comment:
- 2
- always
- markers:
- '/'
space-unary-ops:
- 2
# ECMAScript 6
arrow-parens:
- 2
arrow-spacing:
- 2
no-class-assign:
- 2
no-dupe-class-members:
- 2
no-var:
- 2
object-shorthand:
- 2
prefer-const:
- 2
prefer-template:
- 2
prefer-spread:
- 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
# Exported by packages we use
'$': false
_: false
autosize: false
Avatar: true
Avatars: true
BlazeComponent: false
BlazeLayout: false
FlowRouter: false
FS: false
getSlug: false
Migrations: false
Mousetrap: false
Picker: false
Presence: true
Presences: true
Ps: true
ReactiveTabs: false
SimpleSchema: false
SubsManager: false
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
Filter: true
Filter: true
Mixins: true
Modal: true
MultiSelection: true
Popup: true
Sidebar: true
Utils: true
InlinedForm: true
UnsavedEdits: true
# XXX Temp, we should remove these
allowIsBoardAdmin: true
allowIsBoardMember: true
Emoji: true
env:
es6: true
node: true
browser: true
extends: 'eslint:recommended'

73
.jscsrc
View file

@ -1,73 +0,0 @@
{
"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
}

View file

@ -1,91 +0,0 @@
{
// JSHint options: http://jshint.com/docs/options/
"maxerr": 50,
// Enforcing
"camelcase": true,
"eqeqeq": true,
"undef": true,
"unused": true,
// Environments
"browser": true,
"devel": true,
// Authorized globals
"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,
// Exported by packages we use
"_": false,
"$": false,
"autosize": false,
"Router": false,
"SimpleSchema": false,
"getSlug": false,
"Migrations": false,
"FS": false,
"BlazeComponent": false,
"TAPi18n": false,
"T9n": false,
"SubsManager": false,
"Mousetrap": false,
"Avatar": true,
"Avatars": true,
"Ps": true,
"Presence": true,
"Presences": true,
// Our collections
"Boards": true,
"Lists": true,
"Cards": true,
"CardComments": true,
"Activities": true,
"Attachments": true,
"Users": true,
"AccountsTemplates": true,
// Our objects
"CSSEvents": true,
"EscapeActions": true,
"Filter": true,
"Filter": true,
"Mixins": true,
"Modal": true,
"MultiSelection": true,
"Popup": true,
"Sidebar": true,
"Utils": true,
"InlinedForm": true,
// XXX Temp, we should remove these
"allowIsBoardAdmin": true,
"allowIsBoardMember": true,
"Emoji": true
}
}

View file

@ -9,12 +9,14 @@
meteor-base meteor-base
# Build system # Build system
es5-shim
ecmascript ecmascript
standard-minifiers standard-minifiers
mquandalle:jade mquandalle:jade
mquandalle:stylus mquandalle:stylus
# Polyfills
es5-shim
# Collections # Collections
mongo mongo
aldeed:collection2 aldeed:collection2
@ -63,6 +65,7 @@ fortawesome:fontawesome
mousetrap:mousetrap mousetrap:mousetrap
mquandalle:jquery-textcomplete mquandalle:jquery-textcomplete
mquandalle:jquery-ui-drag-drop-sort mquandalle:jquery-ui-drag-drop-sort
mquandalle:mousetrap-bindglobal
mquandalle:perfect-scrollbar mquandalle:perfect-scrollbar
peerlibrary:blaze-components peerlibrary:blaze-components
perak:markdown perak:markdown

View file

@ -90,6 +90,7 @@ mquandalle:jade-compiler@0.4.3
mquandalle:jquery-textcomplete@0.3.9_1 mquandalle:jquery-textcomplete@0.3.9_1
mquandalle:jquery-ui-drag-drop-sort@0.1.0 mquandalle:jquery-ui-drag-drop-sort@0.1.0
mquandalle:moment@1.0.0 mquandalle:moment@1.0.0
mquandalle:mousetrap-bindglobal@0.0.1
mquandalle:perfect-scrollbar@0.6.5_2 mquandalle:perfect-scrollbar@0.6.5_2
mquandalle:stylus@1.1.1 mquandalle:stylus@1.1.1
npm-bcrypt@0.7.8_2 npm-bcrypt@0.7.8_2

View file

@ -1,99 +1,98 @@
var activitiesPerPage = 20; const activitiesPerPage = 20;
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'activities'; return 'activities';
}, },
onCreated: function() { onCreated() {
var self = this;
// XXX Should we use ReactiveNumber? // XXX Should we use ReactiveNumber?
self.page = new ReactiveVar(1); this.page = new ReactiveVar(1);
self.loadNextPageLocked = false; this.loadNextPageLocked = false;
var sidebar = self.componentParent(); // XXX for some reason not working const sidebar = this.componentParent(); // XXX for some reason not working
sidebar.callFirstWith(null, 'resetNextPeak'); sidebar.callFirstWith(null, 'resetNextPeak');
self.autorun(function() { this.autorun(() => {
var mode = self.data().mode; const mode = this.data().mode;
var capitalizedMode = Utils.capitalize(mode); const capitalizedMode = Utils.capitalize(mode);
var id = Session.get('current' + capitalizedMode); const id = Session.get(`current${capitalizedMode}`);
var limit = self.page.get() * activitiesPerPage; const limit = this.page.get() * activitiesPerPage;
if (id === null) if (id === null)
return; return;
self.subscribe('activities', mode, id, limit, function() { this.subscribe('activities', mode, id, limit, () => {
self.loadNextPageLocked = false; this.loadNextPageLocked = false;
// If the sibear peak hasn't increased, that mean that there are no more // If the sibear peak hasn't increased, that mean that there are no more
// activities, and we can stop calling new subscriptions. // activities, and we can stop calling new subscriptions.
// XXX This is hacky! We need to know excatly and reactively how many // XXX This is hacky! We need to know excatly and reactively how many
// activities there are, we probably want to denormalize this number // activities there are, we probably want to denormalize this number
// dirrectly into card and board documents. // dirrectly into card and board documents.
var a = sidebar.callFirstWith(null, 'getNextPeak'); const nextPeakBefore = sidebar.callFirstWith(null, 'getNextPeak');
sidebar.calculateNextPeak(); sidebar.calculateNextPeak();
var b = sidebar.callFirstWith(null, 'getNextPeak'); const nextPeakAfter = sidebar.callFirstWith(null, 'getNextPeak');
if (a === b) { if (nextPeakBefore === nextPeakAfter) {
sidebar.callFirstWith(null, 'resetNextPeak'); sidebar.callFirstWith(null, 'resetNextPeak');
} }
}); });
}); });
}, },
loadNextPage: function() { loadNextPage() {
if (this.loadNextPageLocked === false) { if (this.loadNextPageLocked === false) {
this.page.set(this.page.get() + 1); this.page.set(this.page.get() + 1);
this.loadNextPageLocked = true; this.loadNextPageLocked = true;
} }
}, },
boardLabel: function() { boardLabel() {
return TAPi18n.__('this-board'); return TAPi18n.__('this-board');
}, },
cardLabel: function() { cardLabel() {
return TAPi18n.__('this-card'); return TAPi18n.__('this-card');
}, },
cardLink: function() { cardLink() {
var card = this.currentData().card(); const card = this.currentData().card();
return card && Blaze.toHTML(HTML.A({ return card && Blaze.toHTML(HTML.A({
href: card.absoluteUrl(), href: card.absoluteUrl(),
'class': 'action-card' 'class': 'action-card',
}, card.title)); }, card.title));
}, },
memberLink: function() { memberLink() {
return Blaze.toHTMLWithData(Template.memberName, { return Blaze.toHTMLWithData(Template.memberName, {
user: this.currentData().member() user: this.currentData().member(),
}); });
}, },
attachmentLink: function() { attachmentLink() {
var attachment = this.currentData().attachment(); const attachment = this.currentData().attachment();
return attachment && Blaze.toHTML(HTML.A({ return attachment && Blaze.toHTML(HTML.A({
href: attachment.url(), href: attachment.url(),
'class': 'js-open-attachment-viewer' 'class': 'js-open-attachment-viewer',
}, attachment.name())); }, attachment.name()));
}, },
events: function() { events() {
return [{ return [{
// XXX We should use Popup.afterConfirmation here // XXX We should use Popup.afterConfirmation here
'click .js-delete-comment': function() { 'click .js-delete-comment'() {
var commentId = this.currentData().commentId; const commentId = this.currentData().commentId;
CardComments.remove(commentId); CardComments.remove(commentId);
}, },
'submit .js-edit-comment': function(evt) { 'submit .js-edit-comment'(evt) {
evt.preventDefault(); evt.preventDefault();
var commentText = this.currentComponent().getValue(); const commentText = this.currentComponent().getValue();
var commentId = Template.parentData().commentId; const commentId = Template.parentData().commentId;
if ($.trim(commentText)) { if ($.trim(commentText)) {
CardComments.update(commentId, { CardComments.update(commentId, {
$set: { $set: {
text: commentText text: commentText,
} },
}); });
} }
} },
}]; }];
} },
}).register('activities'); }).register('activities');

View file

@ -1,4 +1,4 @@
let commentFormIsOpen = new ReactiveVar(false); const commentFormIsOpen = new ReactiveVar(false);
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template() { template() {
@ -19,16 +19,16 @@ BlazeComponent.extendComponent({
events() { events() {
return [{ return [{
'click .js-new-comment:not(.focus)': function() { 'click .js-new-comment:not(.focus)'() {
commentFormIsOpen.set(true); commentFormIsOpen.set(true);
}, },
'submit .js-new-comment-form': function(evt) { 'submit .js-new-comment-form'(evt) {
let input = this.getInput(); const input = this.getInput();
if ($.trim(input.val())) { if ($.trim(input.val())) {
CardComments.insert({ CardComments.insert({
boardId: this.currentData().boardId, boardId: this.currentData().boardId,
cardId: this.currentData()._id, cardId: this.currentData()._id,
text: input.val() text: input.val(),
}); });
resetCommentInput(input); resetCommentInput(input);
Tracker.flush(); Tracker.flush();
@ -37,13 +37,13 @@ BlazeComponent.extendComponent({
evt.preventDefault(); evt.preventDefault();
}, },
// Pressing Ctrl+Enter should submit the form // Pressing Ctrl+Enter should submit the form
'keydown form textarea': function(evt) { 'keydown form textarea'(evt) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
this.find('button[type=submit]').click(); this.find('button[type=submit]').click();
} }
} },
}]; }];
} },
}).register('commentForm'); }).register('commentForm');
// XXX This should be a static method of the `commentForm` component // XXX This should be a static method of the `commentForm` component
@ -63,15 +63,15 @@ Tracker.autorun(() => {
Tracker.afterFlush(() => { Tracker.afterFlush(() => {
autosize.update($('.js-new-comment-input')); autosize.update($('.js-new-comment-input'));
}); });
}) });
EscapeActions.register('inlinedForm', EscapeActions.register('inlinedForm',
function() { () => {
const draftKey = { const draftKey = {
fieldName: 'cardComment', fieldName: 'cardComment',
docId: Session.get('currentCard') docId: Session.get('currentCard'),
}; };
let commentInput = $('.js-new-comment-input'); const commentInput = $('.js-new-comment-input');
if ($.trim(commentInput.val())) { if ($.trim(commentInput.val())) {
UnsavedEdits.set(draftKey, commentInput.val()); UnsavedEdits.set(draftKey, commentInput.val());
} else { } else {
@ -79,7 +79,7 @@ EscapeActions.register('inlinedForm',
} }
resetCommentInput(commentInput); resetCommentInput(commentInput);
}, },
function() { return commentFormIsOpen.get(); }, { () => { return commentFormIsOpen.get(); }, {
noClickEscapeOn: '.js-new-comment' noClickEscapeOn: '.js-new-comment',
} }
); );

View file

@ -1,8 +1,8 @@
Template.headerTitle.events({ Template.headerTitle.events({
'click .js-open-archived-board': function() { 'click .js-open-archived-board'() {
Modal.open('archivedBoards') Modal.open('archivedBoards');
} },
}) });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template() { template() {
@ -10,26 +10,26 @@ BlazeComponent.extendComponent({
}, },
onCreated() { onCreated() {
this.subscribe('archivedBoards') this.subscribe('archivedBoards');
}, },
archivedBoards() { archivedBoards() {
return Boards.find({ archived: true }, { return Boards.find({ archived: true }, {
sort: ['title'] sort: ['title'],
}) });
}, },
events() { events() {
return [{ return [{
'click .js-restore-board': function() { 'click .js-restore-board'() {
let boardId = this.currentData()._id const boardId = this.currentData()._id;
Boards.update(boardId, { Boards.update(boardId, {
$set: { $set: {
archived: false archived: false,
} },
}) });
Utils.goBoardId(boardId) Utils.goBoardId(boardId);
} },
}] }];
}, },
}).register('archivedBoards') }).register('archivedBoards');

View file

@ -1,11 +1,11 @@
var subManager = new SubsManager(); const subManager = new SubsManager();
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'board'; return 'board';
}, },
onCreated: function() { onCreated() {
this.draggingActive = new ReactiveVar(false); this.draggingActive = new ReactiveVar(false);
this.showOverlay = new ReactiveVar(false); this.showOverlay = new ReactiveVar(false);
this.isBoardReady = new ReactiveVar(false); this.isBoardReady = new ReactiveVar(false);
@ -15,15 +15,15 @@ BlazeComponent.extendComponent({
// XXX The boardId should be readed from some sort the component "props", // XXX The boardId should be readed from some sort the component "props",
// unfortunatly, Blaze doesn't have this notion. // unfortunatly, Blaze doesn't have this notion.
this.autorun(() => { this.autorun(() => {
let currentBoardId = Session.get('currentBoard'); const currentBoardId = Session.get('currentBoard');
if (! currentBoardId) if (!currentBoardId)
return; return;
var handle = subManager.subscribe('board', currentBoardId); const handle = subManager.subscribe('board', currentBoardId);
Tracker.nonreactive(() => { Tracker.nonreactive(() => {
Tracker.autorun(() => { Tracker.autorun(() => {
this.isBoardReady.set(handle.ready()); this.isBoardReady.set(handle.ready());
}) });
}) });
}); });
this._isDragging = false; this._isDragging = false;
@ -33,52 +33,52 @@ BlazeComponent.extendComponent({
this.mouseHasEnterCardDetails = false; this.mouseHasEnterCardDetails = false;
}, },
openNewListForm: function() { openNewListForm() {
this.componentChildren('addListForm')[0].open(); this.componentChildren('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
// exposing a public API to modify the component state. We need to investigate // exposing a public API to modify the component state. We need to investigate
// best practices here. // best practices here.
setIsDragging: function(bool) { setIsDragging(bool) {
this.draggingActive.set(bool); this.draggingActive.set(bool);
}, },
scrollLeft: function(position = 0) { scrollLeft(position = 0) {
this.$('.js-lists').animate({ this.$('.js-lists').animate({
scrollLeft: position scrollLeft: position,
}); });
}, },
currentCardIsInThisList: function() { currentCardIsInThisList() {
var currentCard = Cards.findOne(Session.get('currentCard')); const currentCard = Cards.findOne(Session.get('currentCard'));
var listId = this.currentData()._id; const listId = this.currentData()._id;
return currentCard && currentCard.listId === listId; return currentCard && currentCard.listId === listId;
}, },
events: function() { events() {
return [{ return [{
// XXX The board-overlay div should probably be moved to the parent // XXX The board-overlay div should probably be moved to the parent
// component. // component.
'mouseenter .board-overlay': function() { 'mouseenter .board-overlay'() {
if (this.mouseHasEnterCardDetails) { if (this.mouseHasEnterCardDetails) {
this.showOverlay.set(false); this.showOverlay.set(false);
} }
}, },
// Click-and-drag action // Click-and-drag action
'mousedown .board-canvas': function(evt) { 'mousedown .board-canvas'(evt) {
if ($(evt.target).closest('a,.js-list-header').length === 0) { if ($(evt.target).closest('a,.js-list-header').length === 0) {
this._isDragging = true; this._isDragging = true;
this._lastDragPositionX = evt.clientX; this._lastDragPositionX = evt.clientX;
} }
}, },
'mouseup': function(evt) { 'mouseup'() {
if (this._isDragging) { if (this._isDragging) {
this._isDragging = false; this._isDragging = false;
} }
}, },
'mousemove': function(evt) { 'mousemove'(evt) {
if (this._isDragging) { if (this._isDragging) {
// Update the canvas position // Update the canvas position
this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX; this.listsDom.scrollLeft -= evt.clientX - this._lastDragPositionX;
@ -91,40 +91,40 @@ BlazeComponent.extendComponent({
EscapeActions.executeUpTo('popup-close'); EscapeActions.executeUpTo('popup-close');
EscapeActions.preventNextClick(); EscapeActions.preventNextClick();
} }
} },
}]; }];
} },
}).register('board'); }).register('board');
Template.boardBody.onRendered(function() { Template.boardBody.onRendered(function() {
var self = BlazeComponent.getComponentForElement(this.firstNode); const self = BlazeComponent.getComponentForElement(this.firstNode);
self.listsDom = this.find('.js-lists'); self.listsDom = this.find('.js-lists');
if (! Session.get('currentCard')) { if (!Session.get('currentCard')) {
self.scrollLeft(); self.scrollLeft();
} }
// We want to animate the card details window closing. We rely on CSS // We want to animate the card details window closing. We rely on CSS
// transition for the actual animation. // transition for the actual animation.
self.listsDom._uihooks = { self.listsDom._uihooks = {
removeElement: function(node) { removeElement(node) {
var removeNode = _.once(function() { const removeNode = _.once(() => {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
}); });
if ($(node).hasClass('js-card-details')) { if ($(node).hasClass('js-card-details')) {
$(node).css({ $(node).css({
flexBasis: 0, flexBasis: 0,
padding: 0 padding: 0,
}); });
$(self.listsDom).one(CSSEvents.transitionend, removeNode); $(self.listsDom).one(CSSEvents.transitionend, removeNode);
} else { } else {
removeNode(); removeNode();
} }
} },
}; };
if (! Meteor.user() || ! Meteor.user().isBoardMember()) if (!Meteor.user() || !Meteor.user().isBoardMember())
return; return;
self.$(self.listsDom).sortable({ self.$(self.listsDom).sortable({
@ -134,63 +134,63 @@ Template.boardBody.onRendered(function() {
items: '.js-list:not(.js-list-composer)', items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder', placeholder: 'list placeholder',
distance: 7, distance: 7,
start: function(evt, ui) { start(evt, ui) {
ui.placeholder.height(ui.helper.height()); ui.placeholder.height(ui.helper.height());
Popup.close(); Popup.close();
}, },
stop: function() { stop() {
self.$('.js-lists').find('.js-list:not(.js-list-composer)').each( self.$('.js-lists').find('.js-list:not(.js-list-composer)').each(
function(i, list) { (i, list) => {
var data = Blaze.getData(list); const data = Blaze.getData(list);
Lists.update(data._id, { Lists.update(data._id, {
$set: { $set: {
sort: i sort: i,
} },
}); });
} }
); );
} },
}); });
// Disable drag-dropping while in multi-selection mode // Disable drag-dropping while in multi-selection mode
self.autorun(function() { self.autorun(() => {
self.$(self.listsDom).sortable('option', 'disabled', self.$(self.listsDom).sortable('option', 'disabled',
MultiSelection.isActive()); MultiSelection.isActive());
}); });
// If there is no data in the board (ie, no lists) we autofocus the list // If there is no data in the board (ie, no lists) we autofocus the list
// creation form by clicking on the corresponding element. // creation form by clicking on the corresponding element.
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
if (currentBoard.lists().count() === 0) { if (currentBoard.lists().count() === 0) {
self.openNewListForm(); self.openNewListForm();
} }
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'addListForm'; return 'addListForm';
}, },
// Proxy // Proxy
open: function() { open() {
this.componentChildren('inlinedForm')[0].open(); this.componentChildren('inlinedForm')[0].open();
}, },
events: function() { events() {
return [{ return [{
submit: function(evt) { submit(evt) {
evt.preventDefault(); evt.preventDefault();
var title = this.find('.list-name-input'); const title = this.find('.list-name-input');
if ($.trim(title.value)) { if ($.trim(title.value)) {
Lists.insert({ Lists.insert({
title: title.value, title: title.value,
boardId: Session.get('currentBoard'), boardId: Session.get('currentBoard'),
sort: $('.list').length sort: $('.list').length,
}); });
title.value = ''; title.value = '';
} }
} },
}]; }];
} },
}).register('addListForm'); }).register('addListForm');

View file

@ -1,143 +1,143 @@
Template.boardMenuPopup.events({ Template.boardMenuPopup.events({
'click .js-rename-board': Popup.open('boardChangeTitle'), 'click .js-rename-board': Popup.open('boardChangeTitle'),
'click .js-open-archives': function() { 'click .js-open-archives'() {
Sidebar.setView('archives'); Sidebar.setView('archives');
Popup.close(); Popup.close();
}, },
'click .js-change-board-color': Popup.open('boardChangeColor'), 'click .js-change-board-color': Popup.open('boardChangeColor'),
'click .js-change-language': Popup.open('changeLanguage'), 'click .js-change-language': Popup.open('changeLanguage'),
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() { 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', () => {
var boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
Boards.update(boardId, { $set: { archived: true }}); Boards.update(boardId, { $set: { archived: true }});
// XXX We should have some kind of notification on top of the page to // XXX We should have some kind of notification on top of the page to
// confirm that the board was successfully archived. // confirm that the board was successfully archived.
FlowRouter.go('home'); FlowRouter.go('home');
}) }),
}); });
Template.boardChangeTitlePopup.events({ Template.boardChangeTitlePopup.events({
submit: function(evt, t) { submit(evt, tpl) {
var title = t.$('.js-board-name').val().trim(); const title = tpl.$('.js-board-name').val().trim();
if (title) { if (title) {
Boards.update(this._id, { Boards.update(this._id, {
$set: { $set: {
title: title title,
} },
}); });
Popup.close(); Popup.close();
} }
evt.preventDefault(); evt.preventDefault();
} },
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'headerBoard'; return 'headerBoard';
}, },
isStarred: function() { isStarred() {
var boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
var user = Meteor.user(); const user = Meteor.user();
return user && user.hasStarred(boardId); return user && user.hasStarred(boardId);
}, },
// Only show the star counter if the number of star is greater than 2 // Only show the star counter if the number of star is greater than 2
showStarCounter: function() { showStarCounter() {
var currentBoard = this.currentData(); const currentBoard = this.currentData();
return currentBoard && currentBoard.stars >= 2; return currentBoard && currentBoard.stars >= 2;
}, },
events: function() { events() {
return [{ return [{
'click .js-edit-board-title': Popup.open('boardChangeTitle'), 'click .js-edit-board-title': Popup.open('boardChangeTitle'),
'click .js-star-board': function() { 'click .js-star-board'() {
Meteor.user().toggleBoardStar(Session.get('currentBoard')); Meteor.user().toggleBoardStar(Session.get('currentBoard'));
}, },
'click .js-open-board-menu': Popup.open('boardMenu'), 'click .js-open-board-menu': Popup.open('boardMenu'),
'click .js-change-visibility': Popup.open('boardChangeVisibility'), 'click .js-change-visibility': Popup.open('boardChangeVisibility'),
'click .js-open-filter-view': function() { 'click .js-open-filter-view'() {
Sidebar.setView('filter'); Sidebar.setView('filter');
}, },
'click .js-filter-reset': function(evt) { 'click .js-filter-reset'(evt) {
evt.stopPropagation(); evt.stopPropagation();
Sidebar.setView(); Sidebar.setView();
Filter.reset(); Filter.reset();
}, },
'click .js-multiselection-activate': function() { 'click .js-multiselection-activate'() {
var currentCard = Session.get('currentCard'); const currentCard = Session.get('currentCard');
MultiSelection.activate(); MultiSelection.activate();
if (currentCard) { if (currentCard) {
MultiSelection.add(currentCard); MultiSelection.add(currentCard);
} }
}, },
'click .js-multiselection-reset': function(evt) { 'click .js-multiselection-reset'(evt) {
evt.stopPropagation(); evt.stopPropagation();
MultiSelection.disable(); MultiSelection.disable();
} },
}]; }];
} },
}).register('headerBoard'); }).register('headerBoard');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'boardChangeColorPopup'; return 'boardChangeColorPopup';
}, },
backgroundColors: function() { backgroundColors() {
return Boards.simpleSchema()._schema.color.allowedValues; return Boards.simpleSchema()._schema.color.allowedValues;
}, },
isSelected: function() { isSelected() {
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return currentBoard.color === this.currentData().toString(); return currentBoard.color === this.currentData().toString();
}, },
events: function() { events() {
return [{ return [{
'click .js-select-background': function(evt) { 'click .js-select-background'(evt) {
var currentBoardId = Session.get('currentBoard'); const currentBoardId = Session.get('currentBoard');
Boards.update(currentBoardId, { Boards.update(currentBoardId, {
$set: { $set: {
color: this.currentData().toString() color: this.currentData().toString(),
} },
}); });
evt.preventDefault(); evt.preventDefault();
} },
}]; }];
} },
}).register('boardChangeColorPopup'); }).register('boardChangeColorPopup');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'createBoardPopup'; return 'createBoardPopup';
}, },
onCreated: function() { onCreated() {
this.visibilityMenuIsOpen = new ReactiveVar(false); this.visibilityMenuIsOpen = new ReactiveVar(false);
this.visibility = new ReactiveVar('private'); this.visibility = new ReactiveVar('private');
}, },
visibilityCheck: function() { visibilityCheck() {
return this.currentData() === this.visibility.get(); return this.currentData() === this.visibility.get();
}, },
setVisibility: function(visibility) { setVisibility(visibility) {
this.visibility.set(visibility); this.visibility.set(visibility);
this.visibilityMenuIsOpen.set(false); this.visibilityMenuIsOpen.set(false);
}, },
toogleVisibilityMenu: function() { toogleVisibilityMenu() {
this.visibilityMenuIsOpen.set(! this.visibilityMenuIsOpen.get()); this.visibilityMenuIsOpen.set(!this.visibilityMenuIsOpen.get());
}, },
onSubmit: function(evt) { onSubmit(evt) {
evt.preventDefault(); evt.preventDefault();
var title = this.find('.js-new-board-title').value; const title = this.find('.js-new-board-title').value;
var visibility = this.visibility.get(); const visibility = this.visibility.get();
var boardId = Boards.insert({ const boardId = Boards.insert({
title: title, title,
permission: visibility permission: visibility,
}); });
Utils.goBoardId(boardId); Utils.goBoardId(boardId);
@ -146,39 +146,39 @@ BlazeComponent.extendComponent({
Meteor.user().toggleBoardStar(boardId); Meteor.user().toggleBoardStar(boardId);
}, },
events: function() { events() {
return [{ return [{
'click .js-select-visibility': function() { 'click .js-select-visibility'() {
this.setVisibility(this.currentData()); this.setVisibility(this.currentData());
}, },
'click .js-change-visibility': this.toogleVisibilityMenu, 'click .js-change-visibility': this.toogleVisibilityMenu,
submit: this.onSubmit submit: this.onSubmit,
}]; }];
} },
}).register('createBoardPopup'); }).register('createBoardPopup');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'boardChangeVisibilityPopup'; return 'boardChangeVisibilityPopup';
}, },
visibilityCheck: function() { visibilityCheck() {
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
return this.currentData() === currentBoard.permission; return this.currentData() === currentBoard.permission;
}, },
selectBoardVisibility: function() { selectBoardVisibility() {
Boards.update(Session.get('currentBoard'), { Boards.update(Session.get('currentBoard'), {
$set: { $set: {
permission: this.currentData() permission: this.currentData(),
} },
}); });
Popup.close(); Popup.close();
}, },
events: function() { events() {
return [{ return [{
'click .js-select-visibility': this.selectBoardVisibility 'click .js-select-visibility': this.selectBoardVisibility,
}]; }];
} },
}).register('boardChangeVisibilityPopup'); }).register('boardChangeVisibilityPopup');

View file

@ -1,30 +1,30 @@
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'boardList'; return 'boardList';
}, },
boards: function() { boards() {
return Boards.find({ return Boards.find({
archived: false, archived: false,
'members.userId': Meteor.userId() 'members.userId': Meteor.userId(),
}, { }, {
sort: ['title'] sort: ['title'],
}); });
}, },
isStarred: function() { isStarred() {
var user = Meteor.user(); const user = Meteor.user();
return user && user.hasStarred(this.currentData()._id); return user && user.hasStarred(this.currentData()._id);
}, },
events: function() { events() {
return [{ return [{
'click .js-add-board': Popup.open('createBoard'), 'click .js-add-board': Popup.open('createBoard'),
'click .js-star-board': function(evt) { 'click .js-star-board'(evt) {
var boardId = this.currentData()._id; const boardId = this.currentData()._id;
Meteor.user().toggleBoardStar(boardId); Meteor.user().toggleBoardStar(boardId);
evt.preventDefault(); evt.preventDefault();
} },
}]; }];
} },
}).register('boardList'); }).register('boardList');

View file

@ -1,32 +1,32 @@
Template.attachmentsGalery.events({ Template.attachmentsGalery.events({
'click .js-add-attachment': Popup.open('cardAttachments'), 'click .js-add-attachment': Popup.open('cardAttachments'),
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete', 'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
function() { () => {
Attachments.remove(this._id); Attachments.remove(this._id);
Popup.close(); Popup.close();
} }
), ),
// If we let this event bubble, FlowRouter will handle it and empty the page // If we let this event bubble, FlowRouter will handle it and empty the page
// content, see #101. // content, see #101.
'click .js-download': function(event) { 'click .js-download'(event) {
event.stopPropagation(); event.stopPropagation();
}, },
'click .js-open-viewer': function() { 'click .js-open-viewer'() {
// XXX Not implemented! // XXX Not implemented!
}, },
'click .js-add-cover': function() { 'click .js-add-cover'() {
Cards.update(this.cardId, { $set: { coverId: this._id } }); Cards.update(this.cardId, { $set: { coverId: this._id } });
}, },
'click .js-remove-cover': function() { 'click .js-remove-cover'() {
Cards.update(this.cardId, { $unset: { coverId: '' } }); Cards.update(this.cardId, { $unset: { coverId: '' } });
} },
}); });
Template.cardAttachmentsPopup.events({ Template.cardAttachmentsPopup.events({
'change .js-attach-file': function(evt) { 'change .js-attach-file'(evt) {
var card = this; const card = this;
FS.Utility.eachFile(evt, function(f) { FS.Utility.eachFile(evt, (f) => {
var file = new FS.File(f); const file = new FS.File(f);
file.boardId = card.boardId; file.boardId = card.boardId;
file.cardId = card._id; file.cardId = card._id;
@ -34,8 +34,8 @@ Template.cardAttachmentsPopup.events({
Popup.close(); Popup.close();
}); });
}, },
'click .js-computer-upload': function(evt, tpl) { 'click .js-computer-upload'(evt, tpl) {
tpl.find('.js-attach-file').click(); tpl.find('.js-attach-file').click();
evt.preventDefault(); evt.preventDefault();
} },
}); });

View file

@ -1,39 +1,39 @@
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'cardDetails'; return 'cardDetails';
}, },
mixins: function() { mixins() {
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar]; return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
}, },
calculateNextPeak: function() { calculateNextPeak() {
var altitude = this.find('.js-card-details').scrollHeight; const altitude = this.find('.js-card-details').scrollHeight;
this.callFirstWith(this, 'setNextPeak', altitude); this.callFirstWith(this, 'setNextPeak', altitude);
}, },
reachNextPeak: function() { reachNextPeak() {
var activitiesComponent = this.componentChildren('activities')[0]; const activitiesComponent = this.componentChildren('activities')[0];
activitiesComponent.loadNextPage(); activitiesComponent.loadNextPage();
}, },
onCreated: function() { onCreated() {
this.isLoaded = new ReactiveVar(false); this.isLoaded = new ReactiveVar(false);
this.componentParent().showOverlay.set(true); this.componentParent().showOverlay.set(true);
this.componentParent().mouseHasEnterCardDetails = false; this.componentParent().mouseHasEnterCardDetails = false;
}, },
scrollParentContainer: function() { scrollParentContainer() {
const cardPanelWidth = 510; const cardPanelWidth = 510;
let bodyBoardComponent = this.componentParent(); const bodyBoardComponent = this.componentParent();
let $cardContainer = bodyBoardComponent.$('.js-lists'); const $cardContainer = bodyBoardComponent.$('.js-lists');
let $cardView = this.$(this.firstNode()); const $cardView = this.$(this.firstNode());
let cardContainerScroll = $cardContainer.scrollLeft(); const cardContainerScroll = $cardContainer.scrollLeft();
let cardContainerWidth = $cardContainer.width(); const cardContainerWidth = $cardContainer.width();
let cardViewStart = $cardView.offset().left; const cardViewStart = $cardView.offset().left;
let cardViewEnd = cardViewStart + cardPanelWidth; const cardViewEnd = cardViewStart + cardPanelWidth;
let offset = false; let offset = false;
if (cardViewStart < 0) { if (cardViewStart < 0) {
@ -47,53 +47,53 @@ BlazeComponent.extendComponent({
} }
}, },
onRendered: function() { onRendered() {
this.scrollParentContainer(); this.scrollParentContainer();
}, },
onDestroyed: function() { onDestroyed() {
this.componentParent().showOverlay.set(false); this.componentParent().showOverlay.set(false);
}, },
updateCard: function(modifier) { updateCard(modifier) {
Cards.update(this.data()._id, { Cards.update(this.data()._id, {
$set: modifier $set: modifier,
}); });
}, },
events: function() { events() {
var events = { const events = {
[CSSEvents.animationend + ' .js-card-details']: function() { [`${CSSEvents.animationend} .js-card-details`]() {
this.isLoaded.set(true); this.isLoaded.set(true);
} },
}; };
return [_.extend(events, { return [_.extend(events, {
'click .js-close-card-details': function() { 'click .js-close-card-details'() {
Utils.goBoardId(this.data().boardId); Utils.goBoardId(this.data().boardId);
}, },
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'), 'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description': function(evt) { 'submit .js-card-description'(evt) {
evt.preventDefault(); evt.preventDefault();
var description = this.currentComponent().getValue(); const description = this.currentComponent().getValue();
this.updateCard({ description: description }); this.updateCard({ description });
}, },
'submit .js-card-details-title': function(evt) { 'submit .js-card-details-title'(evt) {
evt.preventDefault(); evt.preventDefault();
var title = this.currentComponent().getValue(); const title = this.currentComponent().getValue();
if ($.trim(title)) { if ($.trim(title)) {
this.updateCard({ title: title }); this.updateCard({ title });
} }
}, },
'click .js-member': Popup.open('cardMember'), 'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'), 'click .js-add-members': Popup.open('cardMembers'),
'click .js-add-labels': Popup.open('cardLabels'), 'click .js-add-labels': Popup.open('cardLabels'),
'mouseenter .js-card-details': function() { 'mouseenter .js-card-details'() {
this.componentParent().showOverlay.set(true); this.componentParent().showOverlay.set(true);
this.componentParent().mouseHasEnterCardDetails = true; this.componentParent().mouseHasEnterCardDetails = true;
} },
})]; })];
} },
}).register('cardDetails'); }).register('cardDetails');
// We extends the normal InlinedForm component to support UnsavedEdits draft // We extends the normal InlinedForm component to support UnsavedEdits draft
@ -103,12 +103,12 @@ BlazeComponent.extendComponent({
return { return {
fieldName: 'cardDescription', fieldName: 'cardDescription',
docId: Session.get('currentCard'), docId: Session.get('currentCard'),
} };
} }
close(isReset = false) { close(isReset = false) {
if (this.isOpen.get() && ! isReset) { if (this.isOpen.get() && !isReset) {
let draft = $.trim(this.getValue()); const draft = $.trim(this.getValue());
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());
} }
@ -136,45 +136,45 @@ Template.cardDetailsActionsPopup.events({
'click .js-attachments': Popup.open('cardAttachments'), 'click .js-attachments': Popup.open('cardAttachments'),
'click .js-move-card': Popup.open('moveCard'), 'click .js-move-card': Popup.open('moveCard'),
// 'click .js-copy': Popup.open(), // 'click .js-copy': Popup.open(),
'click .js-archive': function(evt) { 'click .js-archive'(evt) {
evt.preventDefault(); evt.preventDefault();
Cards.update(this._id, { Cards.update(this._id, {
$set: { $set: {
archived: true archived: true,
} },
}); });
Popup.close(); Popup.close();
}, },
'click .js-more': Popup.open('cardMore') 'click .js-more': Popup.open('cardMore'),
}); });
Template.moveCardPopup.events({ Template.moveCardPopup.events({
'click .js-select-list': function() { 'click .js-select-list'() {
// XXX We should *not* get the currentCard from the global state, but // XXX We should *not* get the currentCard from the global state, but
// instead from a “component” state. // instead from a “component” state.
var cardId = Session.get('currentCard'); const cardId = Session.get('currentCard');
var newListId = this._id; const newListId = this._id;
Cards.update(cardId, { Cards.update(cardId, {
$set: { $set: {
listId: newListId listId: newListId,
} },
}); });
Popup.close(); Popup.close();
} },
}); });
Template.cardMorePopup.events({ Template.cardMorePopup.events({
'click .js-delete': Popup.afterConfirm('cardDelete', function() { 'click .js-delete': Popup.afterConfirm('cardDelete', () => {
Popup.close(); Popup.close();
Cards.remove(this._id); Cards.remove(this._id);
Utils.goBoardId(this.board()._id); Utils.goBoardId(this.board()._id);
}) }),
}); });
// Close the card details pane by pressing escape // Close the card details pane by pressing escape
EscapeActions.register('detailsPane', EscapeActions.register('detailsPane',
function() { Utils.goBoardId(Session.get('currentBoard')); }, () => { Utils.goBoardId(Session.get('currentBoard')); },
function() { return ! Session.equals('currentCard', null); }, { () => { return !Session.equals('currentCard', null); }, {
noClickEscapeOn: '.js-card-details,.board-sidebar,#header' noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
} }
); );

View file

@ -1,136 +1,136 @@
let labelColors;
var labelColors; Meteor.startup(() => {
Meteor.startup(function() {
labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; labelColors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'formLabel'; return 'formLabel';
}, },
onCreated: function() { onCreated() {
this.currentColor = new ReactiveVar(this.data().color); this.currentColor = new ReactiveVar(this.data().color);
}, },
labels: function() { labels() {
return _.map(labelColors, function(color) { return _.map(labelColors, (color) => {
return { color: color, name: '' }; return { color, name: '' };
}); });
}, },
isSelected: function(color) { isSelected(color) {
return this.currentColor.get() === color; return this.currentColor.get() === color;
}, },
events: function() { events() {
return [{ return [{
'click .js-palette-color': function() { 'click .js-palette-color'() {
this.currentColor.set(this.currentData().color); this.currentColor.set(this.currentData().color);
} },
}]; }];
} },
}).register('formLabel'); }).register('formLabel');
Template.createLabelPopup.helpers({ Template.createLabelPopup.helpers({
// This is the default color for a new label. We search the first color that // This is the default color for a new label. We search the first color that
// is not already used in the board (although it's not a problem if two // is not already used in the board (although it's not a problem if two
// labels have the same color). // labels have the same color).
defaultColor: function() { defaultColor() {
var labels = Boards.findOne(Session.get('currentBoard')).labels; const labels = Boards.findOne(Session.get('currentBoard')).labels;
var usedColors = _.pluck(labels, 'color'); const usedColors = _.pluck(labels, 'color');
var availableColors = _.difference(labelColors, usedColors); const availableColors = _.difference(labelColors, usedColors);
return availableColors.length > 1 ? availableColors[0] : labelColors[0]; return availableColors.length > 1 ? availableColors[0] : labelColors[0];
} },
}); });
Template.cardLabelsPopup.events({ Template.cardLabelsPopup.events({
'click .js-select-label': function(evt) { 'click .js-select-label'(evt) {
var cardId = Template.parentData(2).data._id; const cardId = Template.parentData(2).data._id;
var labelId = this._id; const labelId = this._id;
var operation; let operation;
if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0) if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0)
operation = '$addToSet'; operation = '$addToSet';
else else
operation = '$pull'; operation = '$pull';
var query = {}; Cards.update(cardId, {
query[operation] = { [operation]: {
labelIds: labelId labelIds: labelId,
}; },
Cards.update(cardId, query); });
evt.preventDefault(); evt.preventDefault();
}, },
'click .js-edit-label': Popup.open('editLabel'), 'click .js-edit-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel') 'click .js-add-label': Popup.open('createLabel'),
}); });
Template.formLabel.events({ Template.formLabel.events({
'click .js-palette-color': function(evt) { 'click .js-palette-color'(evt) {
var $this = $(evt.currentTarget); const $this = $(evt.currentTarget);
// hide selected ll colors // hide selected ll colors
$('.js-palette-select').addClass('hide'); $('.js-palette-select').addClass('hide');
// show select color // show select color
$this.find('.js-palette-select').removeClass('hide'); $this.find('.js-palette-select').removeClass('hide');
} },
}); });
Template.createLabelPopup.events({ Template.createLabelPopup.events({
// Create the new label // Create the new label
'submit .create-label': function(evt, tpl) { 'submit .create-label'(evt, tpl) {
var name = tpl.$('#labelName').val().trim(); const name = tpl.$('#labelName').val().trim();
var boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
var color = Blaze.getData(tpl.find('.fa-check')).color; const color = Blaze.getData(tpl.find('.fa-check')).color;
Boards.update(boardId, { Boards.update(boardId, {
$push: { $push: {
labels: { labels: {
name,
color,
_id: Random.id(6), _id: Random.id(6),
name: name, },
color: color },
}
}
}); });
Popup.back(); Popup.back();
evt.preventDefault(); evt.preventDefault();
} },
}); });
Template.editLabelPopup.events({ Template.editLabelPopup.events({
'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() { 'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() {
var boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
Boards.update(boardId, { Boards.update(boardId, {
$pull: { $pull: {
labels: { labels: {
_id: this._id _id: this._id,
} },
} },
}); });
Popup.back(2); Popup.back(2);
}), }),
'submit .edit-label': function(evt, tpl) { 'submit .edit-label'(evt, tpl) {
evt.preventDefault(); evt.preventDefault();
var name = tpl.$('#labelName').val().trim(); const name = tpl.$('#labelName').val().trim();
var boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
var getLabel = Utils.getLabelIndex(boardId, this._id); const getLabel = Utils.getLabelIndex(boardId, this._id);
var color = Blaze.getData(tpl.find('.fa-check')).color; const color = Blaze.getData(tpl.find('.fa-check')).color;
var $set = {}; Boards.update(boardId, {
$set[getLabel.key('name')] = name; $set: {
$set[getLabel.key('color')] = color; [getLabel.key('name')]: name,
[getLabel.key('color')]: color,
Boards.update(boardId, { $set: $set }); },
});
Popup.back(); Popup.back();
} },
}); });
Template.cardLabelsPopup.helpers({ Template.cardLabelsPopup.helpers({
isLabelSelected: function(cardId) { isLabelSelected(cardId) {
return _.contains(Cards.findOne(cardId).labelIds, this._id); return _.contains(Cards.findOne(cardId).labelIds, this._id);
} },
}); });

View file

@ -3,7 +3,7 @@
// }); // });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'minicard'; return 'minicard';
} },
}).register('minicard'); }).register('minicard');

View file

@ -1,14 +1,16 @@
const { calculateIndex } = Utils;
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'list'; return 'list';
}, },
// Proxies // Proxy
openForm: function(options) { openForm(options) {
this.componentChildren('listBody')[0].openForm(options); this.componentChildren('listBody')[0].openForm(options);
}, },
onCreated: function() { onCreated() {
this.newCardFormIsVisible = new ReactiveVar(true); this.newCardFormIsVisible = new ReactiveVar(true);
}, },
@ -19,28 +21,27 @@ BlazeComponent.extendComponent({
// By calling asking the sortable library to cancel its move on the `stop` // By calling asking the sortable library to cancel its move on the `stop`
// callback, we basically solve all issues related to reactive updates. A // callback, we basically solve all issues related to reactive updates. A
// comment below provides further details. // comment below provides further details.
onRendered: function() { onRendered() {
var self = this; if (!Meteor.user() || !Meteor.user().isBoardMember())
if (! Meteor.user() || ! Meteor.user().isBoardMember())
return; return;
var boardComponent = self.componentParent(); const boardComponent = this.componentParent();
var itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)'; const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
var $cards = self.$('.js-minicards'); const $cards = this.$('.js-minicards');
$cards.sortable({ $cards.sortable({
connectWith: '.js-minicards', connectWith: '.js-minicards',
tolerance: 'pointer', tolerance: 'pointer',
appendTo: 'body', appendTo: 'body',
helper: function(evt, item) { helper(evt, item) {
var helper = item.clone(); const helper = item.clone();
if (MultiSelection.isActive()) { if (MultiSelection.isActive()) {
var andNOthers = $cards.find('.js-minicard.is-checked').length - 1; const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) { if (andNOthers > 0) {
helper.append($(Blaze.toHTML(HTML.DIV( helper.append($(Blaze.toHTML(HTML.DIV(
// XXX Super bad class name // XXX Super bad class name
{'class': 'and-n-other'}, {'class': 'and-n-other'},
// XXX Need to translate // XXX Need to translate
'and ' + andNOthers + ' other cards.' `and ${andNOthers} other cards.`
)))); ))));
} }
} }
@ -50,19 +51,19 @@ BlazeComponent.extendComponent({
items: itemsSelector, items: itemsSelector,
scroll: false, scroll: false,
placeholder: 'minicard-wrapper placeholder', placeholder: 'minicard-wrapper placeholder',
start: function(evt, ui) { start(evt, ui) {
ui.placeholder.height(ui.helper.height()); ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup'); EscapeActions.executeUpTo('popup');
boardComponent.setIsDragging(true); boardComponent.setIsDragging(true);
}, },
stop: function(evt, ui) { stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element // To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any. // of the previous and the following card -- if any.
var prevCardDom = ui.item.prev('.js-minicard').get(0); const prevCardDom = ui.item.prev('.js-minicard').get(0);
var nextCardDom = ui.item.next('.js-minicard').get(0); const nextCardDom = ui.item.next('.js-minicard').get(0);
var nCards = MultiSelection.isActive() ? MultiSelection.count() : 1; const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
var sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards); const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
var listId = Blaze.getData(ui.item.parents('.list').get(0))._id; const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
// Normally the jquery-ui sortable library moves the dragged DOM element // Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism // to its new position, which disrupts Blaze reactive updates mechanism
@ -74,53 +75,53 @@ BlazeComponent.extendComponent({
$cards.sortable('cancel'); $cards.sortable('cancel');
if (MultiSelection.isActive()) { if (MultiSelection.isActive()) {
Cards.find(MultiSelection.getMongoSelector()).forEach(function(c, i) { Cards.find(MultiSelection.getMongoSelector()).forEach((c, i) => {
Cards.update(c._id, { Cards.update(c._id, {
$set: { $set: {
listId: listId, listId,
sort: sortIndex.base + i * sortIndex.increment sort: sortIndex.base + i * sortIndex.increment,
} },
}); });
}); });
} else { } else {
var cardDomElement = ui.item.get(0); const cardDomElement = ui.item.get(0);
var cardId = Blaze.getData(cardDomElement)._id; const cardId = Blaze.getData(cardDomElement)._id;
Cards.update(cardId, { Cards.update(cardId, {
$set: { $set: {
listId: listId, listId,
sort: sortIndex.base sort: sortIndex.base,
} },
}); });
} }
boardComponent.setIsDragging(false); boardComponent.setIsDragging(false);
} },
}); });
// We want to re-run this function any time a card is added. // We want to re-run this function any time a card is added.
self.autorun(function() { this.autorun(() => {
var currentBoardId = Tracker.nonreactive(function() { const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard'); return Session.get('currentBoard');
}); });
Cards.find({ boardId: currentBoardId }).fetch(); Cards.find({ boardId: currentBoardId }).fetch();
Tracker.afterFlush(function() { Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({ $cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card', hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label', accept: '.js-member,.js-label',
drop: function(event, ui) { drop(event, ui) {
var cardId = Blaze.getData(this)._id; const cardId = Blaze.getData(this)._id;
var addToSet; let addToSet;
if (ui.draggable.hasClass('js-member')) { if (ui.draggable.hasClass('js-member')) {
var memberId = Blaze.getData(ui.draggable.get(0)).userId; const memberId = Blaze.getData(ui.draggable.get(0)).userId;
addToSet = { members: memberId }; addToSet = { members: memberId };
} else { } else {
var labelId = Blaze.getData(ui.draggable.get(0))._id; const labelId = Blaze.getData(ui.draggable.get(0))._id;
addToSet = { labelIds: labelId }; addToSet = { labelIds: labelId };
} }
Cards.update(cardId, { $addToSet: addToSet }); Cards.update(cardId, { $addToSet: addToSet });
} },
}); });
}); });
}); });
} },
}).register('list'); }).register('list');

View file

@ -1,46 +1,46 @@
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'listBody'; return 'listBody';
}, },
mixins: function() { mixins() {
return [Mixins.PerfectScrollbar]; return [Mixins.PerfectScrollbar];
}, },
openForm: function(options) { openForm(options) {
options = options || {}; options = options || {};
options.position = options.position || 'top'; options.position = options.position || 'top';
var forms = this.componentChildren('inlinedForm'); const forms = this.componentChildren('inlinedForm');
var form = _.find(forms, function(component) { let form = _.find(forms, (component) => {
return component.data().position === options.position; return component.data().position === options.position;
}); });
if (! form && forms.length > 0) { if (!form && forms.length > 0) {
form = forms[0]; form = forms[0];
} }
form.open(); form.open();
}, },
addCard: function(evt) { addCard(evt) {
evt.preventDefault(); evt.preventDefault();
var textarea = $(evt.currentTarget).find('textarea'); const firstCardDom = this.find('.js-minicard:first');
var title = textarea.val(); const lastCardDom = this.find('.js-minicard:last');
var position = Blaze.getData(evt.currentTarget).position; const textarea = $(evt.currentTarget).find('textarea');
var sortIndex; const title = textarea.val();
var firstCard = this.find('.js-minicard:first'); const position = Blaze.getData(evt.currentTarget).position;
var lastCard = this.find('.js-minicard:last'); let sortIndex;
if (position === 'top') { if (position === 'top') {
sortIndex = Utils.calculateIndex(null, firstCard).base; sortIndex = Utils.calculateIndex(null, firstCardDom).base;
} else if (position === 'bottom') { } else if (position === 'bottom') {
sortIndex = Utils.calculateIndex(lastCard, null).base; sortIndex = Utils.calculateIndex(lastCardDom, null).base;
} }
if ($.trim(title)) { if ($.trim(title)) {
var _id = Cards.insert({ const _id = Cards.insert({
title: title, title,
listId: this.data()._id, listId: this.data()._id,
boardId: this.data().board()._id, boardId: this.data().board()._id,
sort: sortIndex sort: sortIndex,
}); });
// In case the filter is active we need to add the newly inserted card in // 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 // the list of exceptions -- cards that are not filtered. Otherwise the
@ -56,18 +56,18 @@ BlazeComponent.extendComponent({
} }
}, },
scrollToBottom: function() { scrollToBottom() {
var container = this.firstNode(); const container = this.firstNode();
$(container).animate({ $(container).animate({
scrollTop: container.scrollHeight scrollTop: container.scrollHeight,
}); });
}, },
clickOnMiniCard: function(evt) { clickOnMiniCard(evt) {
if (MultiSelection.isActive() || evt.shiftKey) { if (MultiSelection.isActive() || evt.shiftKey) {
evt.stopImmediatePropagation(); evt.stopImmediatePropagation();
evt.preventDefault(); evt.preventDefault();
var methodName = evt.shiftKey ? 'toogleRange' : 'toogle'; const methodName = evt.shiftKey ? 'toogleRange' : 'toogle';
MultiSelection[methodName](this.currentData()._id); MultiSelection[methodName](this.currentData()._id);
// If the card is already selected, we want to de-select it. // If the card is already selected, we want to de-select it.
@ -80,36 +80,36 @@ BlazeComponent.extendComponent({
} }
}, },
cardIsSelected: function() { cardIsSelected() {
return Session.equals('currentCard', this.currentData()._id); return Session.equals('currentCard', this.currentData()._id);
}, },
toggleMultiSelection: function(evt) { toggleMultiSelection(evt) {
evt.stopPropagation(); evt.stopPropagation();
evt.preventDefault(); evt.preventDefault();
MultiSelection.toogle(this.currentData()._id); MultiSelection.toogle(this.currentData()._id);
}, },
events: function() { events() {
return [{ return [{
'click .js-minicard': this.clickOnMiniCard, 'click .js-minicard': this.clickOnMiniCard,
'click .js-toggle-multi-selection': this.toggleMultiSelection, 'click .js-toggle-multi-selection': this.toggleMultiSelection,
'click .open-minicard-composer': this.scrollToBottom, 'click .open-minicard-composer': this.scrollToBottom,
submit: this.addCard submit: this.addCard,
}]; }];
} },
}).register('listBody'); }).register('listBody');
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'addCardForm'; return 'addCardForm';
}, },
pressKey: function(evt) { pressKey(evt) {
// Pressing Enter should submit the card // Pressing Enter should submit the card
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
evt.preventDefault(); evt.preventDefault();
var $form = $(evt.currentTarget).closest('form'); const $form = $(evt.currentTarget).closest('form');
// XXX For some reason $form.submit() does not work (it's probably a bug // XXX For some reason $form.submit() does not work (it's probably a bug
// of blaze-component related to the fact that the submit event is non- // of blaze-component related to the fact that the submit event is non-
// bubbling). This is why we click on the submit button instead -- which // bubbling). This is why we click on the submit button instead -- which
@ -120,24 +120,24 @@ BlazeComponent.extendComponent({
// in the reverse order // in the reverse order
} else if (evt.keyCode === 9) { } else if (evt.keyCode === 9) {
evt.preventDefault(); evt.preventDefault();
var isReverse = evt.shiftKey; const isReverse = evt.shiftKey;
var list = $('#js-list-' + this.data().listId); const list = $(`#js-list-${this.data().listId}`);
var listSelector = '.js-list:not(.js-list-composer)'; const listSelector = '.js-list:not(.js-list-composer)';
var nextList = list[isReverse ? 'prev' : 'next'](listSelector).get(0); let nextList = list[isReverse ? 'prev' : 'next'](listSelector).get(0);
// If there is no next list, loop back to the beginning. // If there is no next list, loop back to the beginning.
if (! nextList) { if (!nextList) {
nextList = $(listSelector + (isReverse ? ':last' : ':first')).get(0); nextList = $(listSelector + (isReverse ? ':last' : ':first')).get(0);
} }
BlazeComponent.getComponentForElement(nextList).openForm({ BlazeComponent.getComponentForElement(nextList).openForm({
position:this.data().position position:this.data().position,
}); });
} }
}, },
events: function() { events() {
return [{ return [{
keydown: this.pressKey keydown: this.pressKey,
}]; }];
} },
}).register('addCardForm'); }).register('addCardForm');

View file

@ -1,78 +1,78 @@
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'listHeader'; return 'listHeader';
}, },
editTitle: function(evt) { editTitle(evt) {
evt.preventDefault(); evt.preventDefault();
var form = this.componentChildren('inlinedForm')[0]; const form = this.componentChildren('inlinedForm')[0];
var newTitle = form.getValue(); const newTitle = form.getValue();
if ($.trim(newTitle)) { if ($.trim(newTitle)) {
Lists.update(this.currentData()._id, { Lists.update(this.currentData()._id, {
$set: { $set: {
title: newTitle title: newTitle,
} },
}); });
} }
}, },
events: function() { events() {
return [{ return [{
'click .js-open-list-menu': Popup.open('listAction'), 'click .js-open-list-menu': Popup.open('listAction'),
submit: this.editTitle submit: this.editTitle,
}]; }];
} },
}).register('listHeader'); }).register('listHeader');
Template.listActionPopup.events({ Template.listActionPopup.events({
'click .js-add-card': function() { 'click .js-add-card'() {
var listDom = document.getElementById('js-list-' + this._id); const listDom = document.getElementById(`js-list-${this._id}`);
var listComponent = BlazeComponent.getComponentForElement(listDom); const listComponent = BlazeComponent.getComponentForElement(listDom);
listComponent.openForm({ position: 'top' }); listComponent.openForm({ position: 'top' });
Popup.close(); Popup.close();
}, },
'click .js-list-subscribe': function() {}, 'click .js-list-subscribe'() {},
'click .js-select-cards': function() { 'click .js-select-cards'() {
var cardIds = Cards.find( const cardIds = Cards.find(
{listId: this._id}, {listId: this._id},
{fields: { _id: 1 }} {fields: { _id: 1 }}
).map(function(card) { return card._id; }); ).map((card) => card._id);
MultiSelection.add(cardIds); MultiSelection.add(cardIds);
Popup.close(); Popup.close();
}, },
'click .js-move-cards': Popup.open('listMoveCards'), 'click .js-move-cards': Popup.open('listMoveCards'),
'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() { 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', () => {
Cards.find({listId: this._id}).forEach(function(card) { Cards.find({listId: this._id}).forEach((card) => {
Cards.update(card._id, { Cards.update(card._id, {
$set: { $set: {
archived: true archived: true,
} },
}); });
}); });
Popup.close(); Popup.close();
}), }),
'click .js-close-list': function(evt) { 'click .js-close-list'(evt) {
evt.preventDefault(); evt.preventDefault();
Lists.update(this._id, { Lists.update(this._id, {
$set: { $set: {
archived: true archived: true,
} },
}); });
Popup.close(); Popup.close();
} },
}); });
Template.listMoveCardsPopup.events({ Template.listMoveCardsPopup.events({
'click .js-select-list': function() { 'click .js-select-list'() {
var fromList = Template.parentData(2).data._id; const fromList = Template.parentData(2).data._id;
var toList = this._id; const toList = this._id;
Cards.find({listId: fromList}).forEach(function(card) { Cards.find({ listId: fromList }).forEach((card) => {
Cards.update(card._id, { Cards.update(card._id, {
$set: { $set: {
listId: toList listId: toList,
} },
}); });
}); });
Popup.close(); Popup.close();
} },
}); });

View file

@ -1,7 +1,7 @@
var dropdownMenuIsOpened = false; let dropdownMenuIsOpened = false;
Template.editor.onRendered(function() { Template.editor.onRendered(() => {
var $textarea = this.$('textarea'); const $textarea = this.$('textarea');
autosize($textarea); autosize($textarea);
@ -9,39 +9,40 @@ Template.editor.onRendered(function() {
// Emojies // Emojies
{ {
match: /\B:([\-+\w]*)$/, match: /\B:([\-+\w]*)$/,
search: function(term, callback) { search(term, callback) {
callback($.map(Emoji.values, function(emoji) { callback($.map(Emoji.values, (emoji) => {
return emoji.indexOf(term) === 0 ? emoji : null; return emoji.indexOf(term) === 0 ? emoji : null;
})); }));
}, },
template: function(value) { template(value) {
var image = '<img src="' + Emoji.baseImagePath + value + '.png"></img>'; const imgSrc = Emoji.baseImagePath + value;
const image = `<img src="${imgSrc}.png" />`;
return image + value; return image + value;
}, },
replace: function(value) { replace(value) {
return ':' + value + ':'; return `:${value}:`;
}, },
index: 1 index: 1,
}, },
// User mentions // User mentions
{ {
match: /\B@(\w*)$/, match: /\B@(\w*)$/,
search: function(term, callback) { search(term, callback) {
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
callback($.map(currentBoard.members, function(member) { callback($.map(currentBoard.members, (member) => {
var username = Users.findOne(member.userId).username; const username = Users.findOne(member.userId).username;
return username.indexOf(term) === 0 ? username : null; return username.indexOf(term) === 0 ? username : null;
})); }));
}, },
template: function(value) { template(value) {
return value; return value;
}, },
replace: function(username) { replace(username) {
return '@' + username + ' '; return `@${username} `;
}, },
index: 1 index: 1,
} },
]); ]);
// Since commit d474017 jquery-textComplete automatically closes a potential // Since commit d474017 jquery-textComplete automatically closes a potential
@ -51,27 +52,27 @@ Template.editor.onRendered(function() {
// 'open' and 'hide' events, and create a ghost escapeAction when the dropdown // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
// is opened (and rely on textComplete to execute the actual action). // is opened (and rely on textComplete to execute the actual action).
$textarea.on({ $textarea.on({
'textComplete:show': function() { 'textComplete:show'() {
dropdownMenuIsOpened = true; dropdownMenuIsOpened = true;
}, },
'textComplete:hide': function() { 'textComplete:hide'() {
Tracker.afterFlush(function() { Tracker.afterFlush(() => {
dropdownMenuIsOpened = false; dropdownMenuIsOpened = false;
}); });
} },
}); });
}); });
EscapeActions.register('textcomplete', EscapeActions.register('textcomplete',
function() {}, () => {},
function() { return dropdownMenuIsOpened; } () => dropdownMenuIsOpened
); );
Template.viewer.events({ Template.viewer.events({
// Viewer sometimes have click-able wrapper around them (for instance to edit // Viewer sometimes have click-able wrapper around them (for instance to edit
// the corresponding text). Clicking a link shouldn't fire these actions, stop // the corresponding text). Clicking a link shouldn't fire these actions, stop
// we stop these event at the viewer component level. // we stop these event at the viewer component level.
'click a': function(evt) { 'click a'(evt) {
evt.stopPropagation(); evt.stopPropagation();
// XXX We hijack the build-in browser action because we currently don't have // XXX We hijack the build-in browser action because we currently don't have
@ -79,7 +80,7 @@ Template.viewer.events({
// handled by a third party package that we can't configure easily. Fix that // handled by a third party package that we can't configure easily. Fix that
// by using directly `_blank` attribute in the rendered HTML. // by using directly `_blank` attribute in the rendered HTML.
evt.preventDefault(); evt.preventDefault();
let href = evt.currentTarget.href; const href = evt.currentTarget.href;
window.open(href, '_blank'); window.open(href, '_blank');
} },
}); });

View file

@ -1,14 +1,14 @@
Template.header.helpers({ Template.header.helpers({
// Reactively set the color of the page from the color of the current board. // Reactively set the color of the page from the color of the current board.
headerTemplate: function() { headerTemplate() {
return 'headerBoard'; return 'headerBoard';
}, },
wrappedHeader: function() { wrappedHeader() {
return ! Session.get('currentBoard'); return !Session.get('currentBoard');
} },
}); });
Template.header.events({ Template.header.events({
'click .js-create-board': Popup.open('createBoard') 'click .js-create-board': Popup.open('createBoard'),
}); });

View file

@ -1,65 +0,0 @@
var Helpers = {
error: function() {
return Session.get('error');
},
toLowerCase: function(text) {
return text && text.toLowerCase();
},
toUpperCase: function(text) {
return text && text.toUpperCase();
},
firstChar: function(text) {
return text && text[0].toUpperCase();
},
session: function(prop) {
return Session.get(prop);
},
getUser: function(userId) {
return Users.findOne(userId);
}
};
// Register all Helpers
_.each(Helpers, function(fn, name) { Blaze.registerHelper(name, fn); });
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown, emojies and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
// compiled version to most users -- who don't need to edit.
// In the meantime, all the transformation are done on the client using the
// Blaze API.
var at = HTML.CharRef({html: '&commat;', str: '@'});
Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
var view = this;
var content = Blaze.toHTML(view.templateContentBlock);
var currentBoard = Session.get('currentBoard');
var knowedUsers = _.map(currentBoard.members, function(member) {
member.username = Users.findOne(member.userId).username;
return member;
});
var mentionRegex = /\B@(\w*)/gi;
var currentMention, knowedUser, href, linkClass, linkValue, link;
while (!! (currentMention = mentionRegex.exec(content))) {
knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] });
if (! knowedUser)
continue;
linkValue = [' ', at, knowedUser.username];
href = Router.url('Profile', { username: knowedUser.username });
linkClass = 'atMention';
if (knowedUser.userId === Meteor.userId())
linkClass += ' me';
link = HTML.A({ href: href, 'class': linkClass }, linkValue);
content = content.replace(currentMention[0], Blaze.toHTML(link));
}
return HTML.Raw(content);
}));

View file

@ -1,13 +1,13 @@
Meteor.subscribe('boards') Meteor.subscribe('boards');
BlazeLayout.setRoot('body') BlazeLayout.setRoot('body');
Template.userFormsLayout.onRendered(function() { Template.userFormsLayout.onRendered(() => {
EscapeActions.executeAll() EscapeActions.executeAll();
}) });
Template.defaultLayout.events({ Template.defaultLayout.events({
'click .js-close-modal': () => { 'click .js-close-modal': () => {
Modal.close() Modal.close();
} },
}) });

View file

@ -1,11 +1,11 @@
Popup.template.events({ Popup.template.events({
'click .js-back-view': function() { 'click .js-back-view'() {
Popup.back(); Popup.back();
}, },
'click .js-close-pop-over': function() { 'click .js-close-pop-over'() {
Popup.close(); Popup.close();
}, },
'click .js-confirm': function() { 'click .js-confirm'() {
this.__afterConfirmAction.call(this); this.__afterConfirmAction.call(this);
}, },
// This handler intends to solve a pretty tricky bug with our popup // This handler intends to solve a pretty tricky bug with our popup
@ -18,22 +18,22 @@ Popup.template.events({
// in moving the whole popup container outside of the popup wrapper. To // in moving the whole popup container outside of the popup wrapper. To
// disable this behavior we have to manually reset the scrollLeft position // disable this behavior we have to manually reset the scrollLeft position
// whenever it is modified. // whenever it is modified.
'scroll .content-wrapper': function(evt) { 'scroll .content-wrapper'(evt) {
evt.currentTarget.scrollLeft = 0; evt.currentTarget.scrollLeft = 0;
} },
}); });
// When a popup content is removed (ie, when the user press the "back" button), // When a popup content is removed (ie, when the user press the "back" button),
// we need to wait for the container translation to end before removing the // we need to wait for the container translation to end before removing the
// actual DOM element. For that purpose we use the undocumented `_uihooks` API. // actual DOM element. For that purpose we use the undocumented `_uihooks` API.
Popup.template.onRendered(function() { Popup.template.onRendered(() => {
var container = this.find('.content-container'); const container = this.find('.content-container');
container._uihooks = { container._uihooks = {
removeElement: function(node) { removeElement(node) {
$(node).addClass('no-height'); $(node).addClass('no-height');
$(container).one(CSSEvents.transitionend, function() { $(container).one(CSSEvents.transitionend, () => {
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
}); });
} },
}; };
}); });

View file

@ -1,37 +1,37 @@
var peakAnticipation = 200; const peakAnticipation = 200;
Mixins.InfiniteScrolling = BlazeComponent.extendComponent({ Mixins.InfiniteScrolling = BlazeComponent.extendComponent({
onCreated: function() { onCreated() {
this._nextPeak = Infinity; this._nextPeak = Infinity;
}, },
setNextPeak: function(v) { setNextPeak(v) {
this._nextPeak = v; this._nextPeak = v;
}, },
getNextPeak: function() { getNextPeak() {
return this._nextPeak; return this._nextPeak;
}, },
resetNextPeak: function() { resetNextPeak() {
this._nextPeak = Infinity; this._nextPeak = Infinity;
}, },
// To be overwritten by consumers of this mixin // To be overwritten by consumers of this mixin
reachNextPeak: function() { reachNextPeak() {
}, },
events: function() { events() {
return [{ return [{
scroll: function(evt) { scroll(evt) {
var domElement = evt.currentTarget; const domElement = evt.currentTarget;
var altitude = domElement.scrollTop + domElement.offsetHeight; let altitude = domElement.scrollTop + domElement.offsetHeight;
altitude += peakAnticipation; altitude += peakAnticipation;
if (altitude >= this.callFirstWith(null, 'getNextPeak')) { if (altitude >= this.callFirstWith(null, 'getNextPeak')) {
this.callFirstWith(null, 'reachNextPeak'); this.callFirstWith(null, 'reachNextPeak');
} }
} },
}]; }];
} },
}); });

View file

@ -1,14 +1,12 @@
Mixins.PerfectScrollbar = BlazeComponent.extendComponent({ Mixins.PerfectScrollbar = BlazeComponent.extendComponent({
onRendered: function() { onRendered() {
var component = this.mixinParent(); const component = this.mixinParent();
var domElement = component.find('.js-perfect-scrollbar'); const domElement = component.find('.js-perfect-scrollbar');
Ps.initialize(domElement); Ps.initialize(domElement);
// XXX We should create an event map to be consistent with other components // XXX We should create an event map to be consistent with other components
// but since BlazeComponent doesn't merge Mixins events transparently I // but since BlazeComponent doesn't merge Mixins events transparently I
// prefered to use a jQuery event (which is what an event map ends up doing) // prefered to use a jQuery event (which is what an event map ends up doing)
component.$(domElement).on('mouseenter', function() { component.$(domElement).on('mouseenter', () => Ps.update(domElement));
Ps.update(domElement); },
});
}
}); });

View file

@ -1,76 +1,76 @@
Sidebar = null; Sidebar = null;
var defaultView = 'home'; const defaultView = 'home';
var viewTitles = { const viewTitles = {
filter: 'filter-cards', filter: 'filter-cards',
multiselection: 'multi-selection', multiselection: 'multi-selection',
archives: 'archives' archives: 'archives',
}; };
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'sidebar'; return 'sidebar';
}, },
mixins: function() { mixins() {
return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar]; return [Mixins.InfiniteScrolling, Mixins.PerfectScrollbar];
}, },
onCreated: function() { onCreated() {
this._isOpen = new ReactiveVar(! Session.get('currentCard')); this._isOpen = new ReactiveVar(!Session.get('currentCard'));
this._view = new ReactiveVar(defaultView); this._view = new ReactiveVar(defaultView);
Sidebar = this; Sidebar = this;
}, },
onDestroyed: function() { onDestroyed() {
Sidebar = null; Sidebar = null;
}, },
isOpen: function() { isOpen() {
return this._isOpen.get(); return this._isOpen.get();
}, },
open: function() { open() {
if (! this._isOpen.get()) { if (!this._isOpen.get()) {
this._isOpen.set(true); this._isOpen.set(true);
EscapeActions.executeUpTo('detailsPane'); EscapeActions.executeUpTo('detailsPane');
} }
}, },
hide: function() { hide() {
if (this._isOpen.get()) { if (this._isOpen.get()) {
this._isOpen.set(false); this._isOpen.set(false);
} }
}, },
toogle: function() { toogle() {
this._isOpen.set(! this._isOpen.get()); this._isOpen.set(!this._isOpen.get());
}, },
calculateNextPeak: function() { calculateNextPeak() {
var altitude = this.find('.js-board-sidebar-content').scrollHeight; const altitude = this.find('.js-board-sidebar-content').scrollHeight;
this.callFirstWith(this, 'setNextPeak', altitude); this.callFirstWith(this, 'setNextPeak', altitude);
}, },
reachNextPeak: function() { reachNextPeak() {
var activitiesComponent = this.componentChildren('activities')[0]; const activitiesComponent = this.componentChildren('activities')[0];
activitiesComponent.loadNextPage(); activitiesComponent.loadNextPage();
}, },
isTongueHidden: function() { isTongueHidden() {
return this.isOpen() && this.getView() !== defaultView; return this.isOpen() && this.getView() !== defaultView;
}, },
scrollTop: function() { scrollTop() {
this.$('.js-board-sidebar-content').scrollTop(0); this.$('.js-board-sidebar-content').scrollTop(0);
}, },
getView: function() { getView() {
return this._view.get(); return this._view.get();
}, },
setView: function(view) { setView(view) {
view = _.isString(view) ? view : defaultView; view = _.isString(view) ? view : defaultView;
if (this._view.get() !== view) { if (this._view.get() !== view) {
this._view.set(view); this._view.set(view);
@ -80,83 +80,84 @@ BlazeComponent.extendComponent({
this.open(); this.open();
}, },
isDefaultView: function() { isDefaultView() {
return this.getView() === defaultView; return this.getView() === defaultView;
}, },
getViewTemplate: function() { getViewTemplate() {
return this.getView() + 'Sidebar'; return `${this.getView()}Sidebar`;
}, },
getViewTitle: function() { getViewTitle() {
return TAPi18n.__(viewTitles[this.getView()]); return TAPi18n.__(viewTitles[this.getView()]);
}, },
events: function() { events() {
// XXX Hacky, we need some kind of `super` // XXX Hacky, we need some kind of `super`
var mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events(); const mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
return mixinEvents.concat([{ return mixinEvents.concat([{
'click .js-toogle-sidebar': this.toogle, 'click .js-toogle-sidebar': this.toogle,
'click .js-back-home': this.setView 'click .js-back-home': this.setView,
}]); }]);
} },
}).register('sidebar'); }).register('sidebar');
Blaze.registerHelper('Sidebar', function() { Blaze.registerHelper('Sidebar', () => Sidebar);
return Sidebar;
});
EscapeActions.register('sidebarView', EscapeActions.register('sidebarView',
function() { Sidebar.setView(defaultView); }, () => { Sidebar.setView(defaultView); },
function() { return Sidebar && Sidebar.getView() !== defaultView; } () => { return Sidebar && Sidebar.getView() !== defaultView; }
); );
var getMemberIndex = function(board, searchId) { function getMemberIndex(board, searchId) {
for (var i = 0; i < board.members.length; i++) { for (let i = 0; i < board.members.length; i++) {
if (board.members[i].userId === searchId) if (board.members[i].userId === searchId)
return i; return i;
} }
throw new Meteor.Error('Member not found'); throw new Meteor.Error('Member not found');
}; }
Template.memberPopup.helpers({ Template.memberPopup.helpers({
user: function() { user() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
}, },
memberType: function() { memberType() {
var type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal'; const type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
return TAPi18n.__(type).toLowerCase(); return TAPi18n.__(type).toLowerCase();
} },
}); });
Template.memberPopup.events({ Template.memberPopup.events({
'click .js-filter-member': function() { 'click .js-filter-member'() {
Filter.members.toogle(this.userId); Filter.members.toogle(this.userId);
Popup.close(); Popup.close();
}, },
'click .js-change-role': Popup.open('changePermissions'), 'click .js-change-role': Popup.open('changePermissions'),
'click .js-remove-member': Popup.afterConfirm('removeMember', function() { 'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
var memberIndex = getMemberIndex(currentBoard, this.userId); const memberIndex = getMemberIndex(currentBoard, this.userId);
var setQuery = {};
setQuery[['members', memberIndex, 'isActive'].join('.')] = false; Boards.update(currentBoard._id, {
Boards.update(currentBoard._id, { $set: setQuery }); $set: {
[`members.${memberIndex}.isActive`]: false,
},
});
Popup.close(); Popup.close();
}), }),
'click .js-leave-member': function() { 'click .js-leave-member'() {
// XXX Not implemented // XXX Not implemented
Popup.close(); Popup.close();
} },
}); });
Template.membersWidget.events({ Template.membersWidget.events({
'click .js-member': Popup.open('member'), 'click .js-member': Popup.open('member'),
'click .js-manage-board-members': Popup.open('addMember') 'click .js-manage-board-members': Popup.open('addMember'),
}); });
Template.labelsWidget.events({ Template.labelsWidget.events({
'click .js-label': Popup.open('editLabel'), 'click .js-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel') 'click .js-add-label': Popup.open('createLabel'),
}); });
// Board members can assign people or labels by drag-dropping elements from the // Board members can assign people or labels by drag-dropping elements from the
@ -164,99 +165,102 @@ Template.labelsWidget.events({
// plugin any time a draggable member or label is modified or removed we use a // plugin any time a draggable member or label is modified or removed we use a
// autorun function and register a dependency on the both members and labels // autorun function and register a dependency on the both members and labels
// fields of the current board document. // fields of the current board document.
var draggableMembersLabelsWidgets = function() { function draggableMembersLabelsWidgets() {
var self = this; if (!Meteor.user() || !Meteor.user().isBoardMember())
if (! Meteor.user() || ! Meteor.user().isBoardMember())
return; return;
self.autorun(function() { this.autorun(() => {
var currentBoardId = Tracker.nonreactive(function() { const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard'); return Session.get('currentBoard');
}); });
Boards.findOne(currentBoardId, { Boards.findOne(currentBoardId, {
fields: { fields: {
members: 1, members: 1,
labels: 1 labels: 1,
} },
}); });
Tracker.afterFlush(function() { Tracker.afterFlush(() => {
self.$('.js-member,.js-label').draggable({ this.$('.js-member,.js-label').draggable({
appendTo: 'body', appendTo: 'body',
helper: 'clone', helper: 'clone',
revert: 'invalid', revert: 'invalid',
revertDuration: 150, revertDuration: 150,
snap: false, snap: false,
snapMode: 'both', snapMode: 'both',
start: function() { start() {
EscapeActions.executeUpTo('popup-back'); EscapeActions.executeUpTo('popup-back');
} },
}); });
}); });
}); });
}; }
Template.membersWidget.onRendered(draggableMembersLabelsWidgets); Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets); Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
Template.addMemberPopup.helpers({ Template.addMemberPopup.helpers({
isBoardMember: function() { isBoardMember() {
var user = Users.findOne(this._id); const user = Users.findOne(this._id);
return user && user.isBoardMember(); return user && user.isBoardMember();
} },
}); });
Template.addMemberPopup.events({ Template.addMemberPopup.events({
'click .pop-over-member-list li:not(.disabled)': function() { 'click .pop-over-member-list li:not(.disabled)'() {
var userId = this._id; const userId = this._id;
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
var currentMembersIds = _.pluck(currentBoard.members, 'userId'); const currentMembersIds = _.pluck(currentBoard.members, 'userId');
if (currentMembersIds.indexOf(userId) === -1) { if (currentMembersIds.indexOf(userId) === -1) {
Boards.update(currentBoard._id, { Boards.update(currentBoard._id, {
$push: { $push: {
members: { members: {
userId: userId, userId,
isAdmin: false, isAdmin: false,
isActive: true isActive: true,
} },
} },
}); });
} else { } else {
var memberIndex = getMemberIndex(currentBoard, userId); const memberIndex = getMemberIndex(currentBoard, userId);
var setQuery = {};
setQuery[['members', memberIndex, 'isActive'].join('.')] = true; Boards.update(currentBoard._id, {
Boards.update(currentBoard._id, { $set: setQuery }); $set: {
[`members.${memberIndex}.isActive`]: true,
},
});
} }
Popup.close(); Popup.close();
} },
}); });
Template.addMemberPopup.onRendered(function() { Template.addMemberPopup.onRendered(() => {
this.find('.js-search-member input').focus(); this.find('.js-search-member input').focus();
}); });
Template.changePermissionsPopup.events({ Template.changePermissionsPopup.events({
'click .js-set-admin, click .js-set-normal': function(event) { 'click .js-set-admin, click .js-set-normal'(event) {
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
var memberIndex = getMemberIndex(currentBoard, this.user._id); const memberIndex = getMemberIndex(currentBoard, this.user._id);
var isAdmin = $(event.currentTarget).hasClass('js-set-admin'); const isAdmin = $(event.currentTarget).hasClass('js-set-admin');
var setQuery = {};
setQuery[['members', memberIndex, 'isAdmin'].join('.')] = isAdmin;
Boards.update(currentBoard._id, { Boards.update(currentBoard._id, {
$set: setQuery $set: {
[`members.${memberIndex}.isAdmin`]: isAdmin,
},
}); });
Popup.back(1); Popup.back(1);
} },
}); });
Template.changePermissionsPopup.helpers({ Template.changePermissionsPopup.helpers({
isAdmin: function() { isAdmin() {
return this.user.isBoardAdmin(); return this.user.isBoardAdmin();
}, },
isLastAdmin: function() { isLastAdmin() {
if (! this.user.isBoardAdmin()) if (!this.user.isBoardAdmin())
return false; return false;
var currentBoard = Boards.findOne(Session.get('currentBoard')); const currentBoard = Boards.findOne(Session.get('currentBoard'));
var nbAdmins = _.where(currentBoard.members, { isAdmin: true }).length; const nbAdmins = _.where(currentBoard.members, { isAdmin: true }).length;
return nbAdmins === 1; return nbAdmins === 1;
} },
}); });

View file

@ -1,46 +1,46 @@
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'archivesSidebar'; return 'archivesSidebar';
}, },
tabs: function() { tabs() {
return [ return [
{ name: TAPi18n.__('cards'), slug: 'cards' }, { name: TAPi18n.__('cards'), slug: 'cards' },
{ name: TAPi18n.__('lists'), slug: 'lists' } { name: TAPi18n.__('lists'), slug: 'lists' },
] ];
}, },
archivedCards: function() { archivedCards() {
return Cards.find({ archived: true }); return Cards.find({ archived: true });
}, },
archivedLists: function() { archivedLists() {
return Lists.find({ archived: true }); return Lists.find({ archived: true });
}, },
cardIsInArchivedList: function() { cardIsInArchivedList() {
return this.currentData().list().archived; return this.currentData().list().archived;
}, },
onRendered: function() { onRendered() {
//XXX We should support dragging a card from the sidebar to the board // XXX We should support dragging a card from the sidebar to the board
}, },
events: function() { events() {
return [{ return [{
'click .js-restore-card': function() { 'click .js-restore-card'() {
var cardId = this.currentData()._id; const cardId = this.currentData()._id;
Cards.update(cardId, {$set: {archived: false}}); Cards.update(cardId, {$set: {archived: false}});
}, },
'click .js-delete-card': Popup.afterConfirm('cardDelete', function() { 'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
var cardId = this._id; const cardId = this._id;
Cards.remove(cardId); Cards.remove(cardId);
Popup.close(); Popup.close();
}), }),
'click .js-restore-list': function() { 'click .js-restore-list'() {
var listId = this.currentData()._id; const listId = this.currentData()._id;
Lists.update(listId, {$set: {archived: false}}); Lists.update(listId, {$set: {archived: false}});
} },
}]; }];
} },
}).register('archivesSidebar'); }).register('archivesSidebar');

View file

@ -1,136 +1,136 @@
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'filterSidebar'; return 'filterSidebar';
}, },
events: function() { events() {
return [{ return [{
'click .js-toggle-label-filter': function(evt) { 'click .js-toggle-label-filter'(evt) {
evt.preventDefault(); evt.preventDefault();
Filter.labelIds.toogle(this.currentData()._id); Filter.labelIds.toogle(this.currentData()._id);
Filter.resetExceptions(); Filter.resetExceptions();
}, },
'click .js-toogle-member-filter': function(evt) { 'click .js-toogle-member-filter'(evt) {
evt.preventDefault(); evt.preventDefault();
Filter.members.toogle(this.currentData()._id); Filter.members.toogle(this.currentData()._id);
Filter.resetExceptions(); Filter.resetExceptions();
}, },
'click .js-clear-all': function(evt) { 'click .js-clear-all'(evt) {
evt.preventDefault(); evt.preventDefault();
Filter.reset(); Filter.reset();
}, },
'click .js-filter-to-selection': function(evt) { 'click .js-filter-to-selection'(evt) {
evt.preventDefault(); evt.preventDefault();
var selectedCards = Cards.find(Filter.mongoSelector()).map(function(c) { const selectedCards = Cards.find(Filter.mongoSelector()).map((c) => {
return c._id; return c._id;
}); });
MultiSelection.add(selectedCards); MultiSelection.add(selectedCards);
} },
}]; }];
} },
}).register('filterSidebar'); }).register('filterSidebar');
var updateSelectedCards = function(query) { function updateSelectedCards(query) {
Cards.find(MultiSelection.getMongoSelector()).forEach(function(card) { Cards.find(MultiSelection.getMongoSelector()).forEach((card) => {
Cards.update(card._id, query); Cards.update(card._id, query);
}); });
}; }
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'multiselectionSidebar'; return 'multiselectionSidebar';
}, },
mapSelection: function(kind, _id) { mapSelection(kind, _id) {
return Cards.find(MultiSelection.getMongoSelector()).map(function(card) { return Cards.find(MultiSelection.getMongoSelector()).map((card) => {
var methodName = kind === 'label' ? 'hasLabel' : 'isAssigned'; const methodName = kind === 'label' ? 'hasLabel' : 'isAssigned';
return card[methodName](_id); return card[methodName](_id);
}); });
}, },
allSelectedElementHave: function(kind, _id) { allSelectedElementHave(kind, _id) {
if (MultiSelection.isEmpty()) if (MultiSelection.isEmpty())
return false; return false;
else else
return _.every(this.mapSelection(kind, _id)); return _.every(this.mapSelection(kind, _id));
}, },
someSelectedElementHave: function(kind, _id) { someSelectedElementHave(kind, _id) {
if (MultiSelection.isEmpty()) if (MultiSelection.isEmpty())
return false; return false;
else else
return _.some(this.mapSelection(kind, _id)); return _.some(this.mapSelection(kind, _id));
}, },
events: function() { events() {
return [{ return [{
'click .js-toggle-label-multiselection': function(evt) { 'click .js-toggle-label-multiselection'(evt) {
var labelId = this.currentData()._id; const labelId = this.currentData()._id;
var mappedSelection = this.mapSelection('label', labelId); const mappedSelection = this.mapSelection('label', labelId);
var operation; let operation;
if (_.every(mappedSelection)) if (_.every(mappedSelection))
operation = '$pull'; operation = '$pull';
else if (_.every(mappedSelection, function(bool) { return ! bool; })) else if (_.every(mappedSelection, (bool) => !bool))
operation = '$addToSet'; operation = '$addToSet';
else { else {
var popup = Popup.open('disambiguateMultiLabel'); const popup = Popup.open('disambiguateMultiLabel');
// XXX We need to have a better integration between the popup and the // XXX We need to have a better integration between the popup and the
// UI components systems. // UI components systems.
return popup.call(this.currentData(), evt); return popup.call(this.currentData(), evt);
} }
var query = {}; updateSelectedCards({
query[operation] = { [operation]: {
labelIds: labelId labelIds: labelId,
}; },
updateSelectedCards(query); });
}, },
'click .js-toogle-member-multiselection': function(evt) { 'click .js-toogle-member-multiselection'(evt) {
var memberId = this.currentData()._id; const memberId = this.currentData()._id;
var mappedSelection = this.mapSelection('member', memberId); const mappedSelection = this.mapSelection('member', memberId);
var operation; let operation;
if (_.every(mappedSelection)) if (_.every(mappedSelection))
operation = '$pull'; operation = '$pull';
else if (_.every(mappedSelection, function(bool) { return ! bool; })) else if (_.every(mappedSelection, (bool) => !bool))
operation = '$addToSet'; operation = '$addToSet';
else { else {
var popup = Popup.open('disambiguateMultiMember'); const popup = Popup.open('disambiguateMultiMember');
// XXX We need to have a better integration between the popup and the // XXX We need to have a better integration between the popup and the
// UI components systems. // UI components systems.
return popup.call(this.currentData(), evt); return popup.call(this.currentData(), evt);
} }
var query = {}; updateSelectedCards({
query[operation] = { [operation]: {
members: memberId members: memberId,
}; },
updateSelectedCards(query); });
}, },
'click .js-archive-selection': function() { 'click .js-archive-selection'() {
updateSelectedCards({$set: {archived: true}}); updateSelectedCards({$set: {archived: true}});
} },
}]; }];
} },
}).register('multiselectionSidebar'); }).register('multiselectionSidebar');
Template.disambiguateMultiLabelPopup.events({ Template.disambiguateMultiLabelPopup.events({
'click .js-remove-label': function() { 'click .js-remove-label'() {
updateSelectedCards({$pull: {labelIds: this._id}}); updateSelectedCards({$pull: {labelIds: this._id}});
Popup.close(); Popup.close();
}, },
'click .js-add-label': function() { 'click .js-add-label'() {
updateSelectedCards({$addToSet: {labelIds: this._id}}); updateSelectedCards({$addToSet: {labelIds: this._id}});
Popup.close(); Popup.close();
} },
}); });
Template.disambiguateMultiMemberPopup.events({ Template.disambiguateMultiMemberPopup.events({
'click .js-unassign-member': function() { 'click .js-unassign-member'() {
updateSelectedCards({$pull: {members: this._id}}); updateSelectedCards({$pull: {members: this._id}});
Popup.close(); Popup.close();
}, },
'click .js-assign-member': function() { 'click .js-assign-member'() {
updateSelectedCards({$addToSet: {members: this._id}}); updateSelectedCards({$addToSet: {members: this._id}});
Popup.close(); Popup.close();
} },
}); });

View file

@ -1,98 +1,98 @@
Meteor.subscribe('my-avatars'); Meteor.subscribe('my-avatars');
Template.userAvatar.helpers({ Template.userAvatar.helpers({
userData: function() { userData() {
return Users.findOne(this.userId, { return Users.findOne(this.userId, {
fields: { fields: {
profile: 1, profile: 1,
username: 1 username: 1,
} },
}); });
}, },
memberType: function() { memberType() {
var user = Users.findOne(this.userId); const user = Users.findOne(this.userId);
return user && user.isBoardAdmin() ? 'admin' : 'normal'; return user && user.isBoardAdmin() ? 'admin' : 'normal';
}, },
presenceStatusClassName: function() { presenceStatusClassName() {
var userPresence = Presences.findOne({ userId: this.userId }); const userPresence = Presences.findOne({ userId: this.userId });
if (! userPresence) if (!userPresence)
return 'disconnected'; return 'disconnected';
else if (Session.equals('currentBoard', userPresence.state.currentBoardId)) else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
return 'active'; return 'active';
else else
return 'idle'; return 'idle';
} },
}); });
Template.userAvatar.events({ Template.userAvatar.events({
'click .js-change-avatar': Popup.open('changeAvatar') 'click .js-change-avatar': Popup.open('changeAvatar'),
}); });
Template.userAvatarInitials.helpers({ Template.userAvatarInitials.helpers({
initials: function() { initials() {
var user = Users.findOne(this.userId); const user = Users.findOne(this.userId);
return user && user.getInitials(); return user && user.getInitials();
}, },
viewPortWidth: function() { viewPortWidth() {
var user = Users.findOne(this.userId); const user = Users.findOne(this.userId);
return (user && user.getInitials().length || 1) * 12; return (user && user.getInitials().length || 1) * 12;
} },
}); });
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
template: function() { template() {
return 'changeAvatarPopup'; return 'changeAvatarPopup';
}, },
onCreated: function() { onCreated() {
this.error = new ReactiveVar(''); this.error = new ReactiveVar('');
}, },
avatarUrlOptions: function() { avatarUrlOptions() {
return { return {
auth: false, auth: false,
brokenIsFine: true brokenIsFine: true,
}; };
}, },
uploadedAvatars: function() { uploadedAvatars() {
return Avatars.find({userId: Meteor.userId()}); return Avatars.find({userId: Meteor.userId()});
}, },
isSelected: function() { isSelected() {
var userProfile = Meteor.user().profile; const userProfile = Meteor.user().profile;
var avatarUrl = userProfile && userProfile.avatarUrl; const avatarUrl = userProfile && userProfile.avatarUrl;
var currentAvatarUrl = this.currentData().url(this.avatarUrlOptions()); const currentAvatarUrl = this.currentData().url(this.avatarUrlOptions());
return avatarUrl === currentAvatarUrl; return avatarUrl === currentAvatarUrl;
}, },
noAvatarUrl: function() { noAvatarUrl() {
var userProfile = Meteor.user().profile; const userProfile = Meteor.user().profile;
var avatarUrl = userProfile && userProfile.avatarUrl; const avatarUrl = userProfile && userProfile.avatarUrl;
return ! avatarUrl; return !avatarUrl;
}, },
setAvatar: function(avatarUrl) { setAvatar(avatarUrl) {
Meteor.users.update(Meteor.userId(), { Meteor.users.update(Meteor.userId(), {
$set: { $set: {
'profile.avatarUrl': avatarUrl 'profile.avatarUrl': avatarUrl,
} },
}); });
}, },
setError: function(error) { setError(error) {
this.error.set(error); this.error.set(error);
}, },
events: function() { events() {
return [{ return [{
'click .js-upload-avatar': function() { 'click .js-upload-avatar'() {
this.$('.js-upload-avatar-input').click(); this.$('.js-upload-avatar-input').click();
}, },
'change .js-upload-avatar-input': function(evt) { 'change .js-upload-avatar-input'(evt) {
let file, fileUrl; let file, fileUrl;
FS.Utility.eachFile(evt, (f) => { FS.Utility.eachFile(evt, (f) => {
@ -106,71 +106,71 @@ BlazeComponent.extendComponent({
if (fileUrl) { if (fileUrl) {
this.setError(''); this.setError('');
let fetchAvatarInterval = window.setInterval(() => { const fetchAvatarInterval = window.setInterval(() => {
$.ajax({ $.ajax({
url: fileUrl, url: fileUrl,
success: () => { success: () => {
this.setAvatar(file.url(this.avatarUrlOptions())); this.setAvatar(file.url(this.avatarUrlOptions()));
window.clearInterval(fetchAvatarInterval); window.clearInterval(fetchAvatarInterval);
} },
}); });
}, 100); }, 100);
} }
}, },
'click .js-select-avatar': function() { 'click .js-select-avatar'() {
var avatarUrl = this.currentData().url(this.avatarUrlOptions()); const avatarUrl = this.currentData().url(this.avatarUrlOptions());
this.setAvatar(avatarUrl); this.setAvatar(avatarUrl);
}, },
'click .js-select-initials': function() { 'click .js-select-initials'() {
this.setAvatar(''); this.setAvatar('');
}, },
'click .js-delete-avatar': function() { 'click .js-delete-avatar'() {
Avatars.remove(this.currentData()._id); Avatars.remove(this.currentData()._id);
} },
}]; }];
} },
}).register('changeAvatarPopup'); }).register('changeAvatarPopup');
Template.cardMembersPopup.helpers({ Template.cardMembersPopup.helpers({
isCardMember: function() { isCardMember() {
var cardId = Template.parentData()._id; const cardId = Template.parentData()._id;
var cardMembers = Cards.findOne(cardId).members || []; const cardMembers = Cards.findOne(cardId).members || [];
return _.contains(cardMembers, this.userId); return _.contains(cardMembers, this.userId);
}, },
user: function() { user() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
} },
}); });
Template.cardMembersPopup.events({ Template.cardMembersPopup.events({
'click .js-select-member': function(evt) { 'click .js-select-member'(evt) {
var cardId = Template.parentData(2).data._id; const cardId = Template.parentData(2).data._id;
var memberId = this.userId; const memberId = this.userId;
var operation; let operation;
if (Cards.find({ _id: cardId, members: memberId}).count() === 0) if (Cards.find({ _id: cardId, members: memberId}).count() === 0)
operation = '$addToSet'; operation = '$addToSet';
else else
operation = '$pull'; operation = '$pull';
var query = {}; Cards.update(cardId, {
query[operation] = { [operation]: {
members: memberId members: memberId,
}; },
Cards.update(cardId, query); });
evt.preventDefault(); evt.preventDefault();
} },
}); });
Template.cardMemberPopup.helpers({ Template.cardMemberPopup.helpers({
user: function() { user() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
} },
}); });
Template.cardMemberPopup.events({ Template.cardMemberPopup.events({
'click .js-remove-member': function() { 'click .js-remove-member'() {
Cards.update(this.cardId, {$pull: {members: this.userId}}); Cards.update(this.cardId, {$pull: {members: this.userId}});
Popup.close(); Popup.close();
}, },
'click .js-edit-profile': Popup.open('editProfile') 'click .js-edit-profile': Popup.open('editProfile'),
}); });

View file

@ -1,6 +1,6 @@
Template.headerUserBar.events({ Template.headerUserBar.events({
'click .js-open-header-member-menu': Popup.open('memberMenu'), 'click .js-open-header-member-menu': Popup.open('memberMenu'),
'click .js-change-avatar': Popup.open('changeAvatar') 'click .js-change-avatar': Popup.open('changeAvatar'),
}); });
Template.memberMenuPopup.events({ Template.memberMenuPopup.events({
@ -8,58 +8,57 @@ Template.memberMenuPopup.events({
'click .js-change-avatar': Popup.open('changeAvatar'), 'click .js-change-avatar': Popup.open('changeAvatar'),
'click .js-change-password': Popup.open('changePassword'), 'click .js-change-password': Popup.open('changePassword'),
'click .js-change-language': Popup.open('changeLanguage'), 'click .js-change-language': Popup.open('changeLanguage'),
'click .js-logout': function(evt) { 'click .js-logout'(evt) {
evt.preventDefault(); evt.preventDefault();
AccountsTemplates.logout(); AccountsTemplates.logout();
} },
}); });
Template.editProfilePopup.events({ Template.editProfilePopup.events({
submit: function(evt, tpl) { submit(evt, tpl) {
evt.preventDefault(); evt.preventDefault();
var fullname = $.trim(tpl.find('.js-profile-fullname').value); const fullname = $.trim(tpl.find('.js-profile-fullname').value);
var username = $.trim(tpl.find('.js-profile-username').value); const username = $.trim(tpl.find('.js-profile-username').value);
var initials = $.trim(tpl.find('.js-profile-initials').value); const initials = $.trim(tpl.find('.js-profile-initials').value);
Users.update(Meteor.userId(), {$set: { Users.update(Meteor.userId(), {$set: {
'profile.fullname': fullname, 'profile.fullname': fullname,
'profile.initials': initials 'profile.initials': initials,
}}); }});
// XXX We should report the error to the user. // XXX We should report the error to the user.
if (username !== Meteor.user().username) { if (username !== Meteor.user().username) {
Meteor.call('setUsername', username); Meteor.call('setUsername', username);
} }
Popup.back(); Popup.back();
} },
}); });
// XXX For some reason the useraccounts autofocus isnt working in this case. // XXX For some reason the useraccounts autofocus isnt working in this case.
// See https://github.com/meteor-useraccounts/core/issues/384 // See https://github.com/meteor-useraccounts/core/issues/384
Template.changePasswordPopup.onRendered(function() { Template.changePasswordPopup.onRendered(() => {
this.find('#at-field-current_password').focus(); this.find('#at-field-current_password').focus();
}); });
Template.changeLanguagePopup.helpers({ Template.changeLanguagePopup.helpers({
languages: function() { languages() {
return _.map(TAPi18n.getLanguages(), function(lang, tag) { return _.map(TAPi18n.getLanguages(), (lang, tag) => {
return { const name = lang.name;
tag: tag, return { tag, name };
name: lang.name
};
}); });
}, },
isCurrentLanguage: function() {
isCurrentLanguage() {
return this.tag === TAPi18n.getLanguage(); return this.tag === TAPi18n.getLanguage();
} },
}); });
Template.changeLanguagePopup.events({ Template.changeLanguagePopup.events({
'click .js-set-language': function(evt) { 'click .js-set-language'(evt) {
Users.update(Meteor.userId(), { Users.update(Meteor.userId(), {
$set: { $set: {
'profile.language': this.tag 'profile.language': this.tag,
} },
}); });
evt.preventDefault(); evt.preventDefault();
} },
}); });

View file

@ -1,11 +1,11 @@
var passwordField = AccountsTemplates.removeField('password'); const passwordField = AccountsTemplates.removeField('password');
var emailField = AccountsTemplates.removeField('email'); const emailField = AccountsTemplates.removeField('email');
AccountsTemplates.addFields([{ AccountsTemplates.addFields([{
_id: 'username', _id: 'username',
type: 'text', type: 'text',
displayName: 'username', displayName: 'username',
required: true, required: true,
minLength: 2 minLength: 2,
}, emailField, passwordField]); }, emailField, passwordField]);
AccountsTemplates.configure({ AccountsTemplates.configure({
@ -15,36 +15,34 @@ AccountsTemplates.configure({
enablePasswordChange: true, enablePasswordChange: true,
sendVerificationEmail: true, sendVerificationEmail: true,
showForgotPasswordLink: true, showForgotPasswordLink: true,
onLogoutHook: function() { onLogoutHook() {
var homePage = 'home'; const homePage = 'home';
if (FlowRouter.getRouteName() === homePage) { if (FlowRouter.getRouteName() === homePage) {
FlowRouter.reload(); FlowRouter.reload();
} else { } else {
FlowRouter.go(homePage); FlowRouter.go(homePage);
} }
} },
}); });
_.each(['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'], _.each(['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'],
function(routeName) { (routeName) => AccountsTemplates.configureRoute(routeName));
AccountsTemplates.configureRoute(routeName);
});
// We display the form to change the password in a popup window that already // We display the form to change the password in a popup window that already
// have a title, so we unset the title automatically displayed by useraccounts. // have a title, so we unset the title automatically displayed by useraccounts.
AccountsTemplates.configure({ AccountsTemplates.configure({
texts: { texts: {
title: { title: {
changePwd: '' changePwd: '',
} },
} },
}); });
AccountsTemplates.configureRoute('changePwd', { AccountsTemplates.configureRoute('changePwd', {
redirect: function() { redirect() {
// XXX We should emit a notification once we have a notification system. // XXX We should emit a notification once we have a notification system.
// Currently the user has no indication that his modification has been // Currently the user has no indication that his modification has been
// applied. // applied.
Popup.back(); Popup.back();
} },
}); });

View file

@ -1,13 +1,53 @@
Blaze.registerHelper('currentBoard', function() { Blaze.registerHelper('currentBoard', () => {
var boardId = Session.get('currentBoard'); const boardId = Session.get('currentBoard');
if (boardId) { if (boardId) {
return Boards.findOne(boardId); return Boards.findOne(boardId);
} }
}); });
Blaze.registerHelper('currentCard', function() { Blaze.registerHelper('currentCard', () => {
var cardId = Session.get('currentCard'); const cardId = Session.get('currentCard');
if (cardId) { if (cardId) {
return Cards.findOne(cardId); return Cards.findOne(cardId);
} }
}); });
Blaze.registerHelper('getUser', (userId) => Users.findOne(userId));
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown, emojies and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
// compiled version to most users -- who don't need to edit.
// In the meantime, all the transformation are done on the client using the
// Blaze API.
const at = HTML.CharRef({html: '&commat;', str: '@'});
Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
const view = this;
const currentBoard = Session.get('currentBoard');
const knowedUsers = _.map(currentBoard.members, (member) => {
member.username = Users.findOne(member.userId).username;
return member;
});
const mentionRegex = /\B@(\w*)/gi;
let content = Blaze.toHTML(view.templateContentBlock);
let currentMention, knowedUser, href, linkClass, linkValue, link;
while (Boolean(currentMention = mentionRegex.exec(content))) {
knowedUser = _.findWhere(knowedUsers, { username: currentMention[1] });
if (!knowedUser)
continue;
linkValue = [' ', at, knowedUser.username];
// XXX We need to convert to flow router
href = Router.url('Profile', { username: knowedUser.username });
linkClass = 'atMention';
if (knowedUser.userId === Meteor.userId())
linkClass += ' me';
link = HTML.A({ href, 'class': linkClass }, linkValue);
content = content.replace(currentMention[0], Blaze.toHTML(link));
}
return HTML.Raw(content);
}));

View file

@ -1,5 +1,4 @@
// XXX Since Blaze doesn't have a clean high component API, component API are // XXX Since Blaze doesn't have a clean high component API, component API are
// also tweaky to use. I guess React would be a solution. // also tweaky to use. I guess React would be a solution.
ReactiveTabs.createInterface({ const template = 'basicTabs';
template: 'basicTabs' ReactiveTabs.createInterface({ template });
});

View file

@ -6,7 +6,7 @@ FlowRouter.triggers.exit([({path}) => {
FlowRouter.route('/', { FlowRouter.route('/', {
name: 'home', name: 'home',
triggersEnter: [AccountsTemplates.ensureSignedIn], triggersEnter: [AccountsTemplates.ensureSignedIn],
action: function() { action() {
Session.set('currentBoard', null); Session.set('currentBoard', null);
Session.set('currentCard', null); Session.set('currentCard', null);
@ -14,14 +14,14 @@ FlowRouter.route('/', {
EscapeActions.executeAll(); EscapeActions.executeAll();
BlazeLayout.render('defaultLayout', { content: 'boardList' }); BlazeLayout.render('defaultLayout', { content: 'boardList' });
} },
}); });
FlowRouter.route('/b/:id/:slug', { FlowRouter.route('/b/:id/:slug', {
name: 'board', name: 'board',
action: function(params) { action(params) {
let currentBoard = params.id; const currentBoard = params.id;
let previousBoard = Session.get('currentBoard'); const previousBoard = Session.get('currentBoard');
Session.set('currentBoard', currentBoard); Session.set('currentBoard', currentBoard);
Session.set('currentCard', null); Session.set('currentCard', null);
@ -32,57 +32,57 @@ FlowRouter.route('/b/:id/:slug', {
} }
BlazeLayout.render('defaultLayout', { content: 'board' }); BlazeLayout.render('defaultLayout', { content: 'board' });
} },
}); });
FlowRouter.route('/b/:boardId/:slug/:cardId', { FlowRouter.route('/b/:boardId/:slug/:cardId', {
name: 'card', name: 'card',
action: function(params) { action(params) {
Session.set('currentBoard', params.boardId); Session.set('currentBoard', params.boardId);
Session.set('currentCard', params.cardId); Session.set('currentCard', params.cardId);
EscapeActions.executeUpTo('inlinedForm'); EscapeActions.executeUpTo('inlinedForm');
BlazeLayout.render('defaultLayout', { content: 'board' }); BlazeLayout.render('defaultLayout', { content: 'board' });
} },
}); });
FlowRouter.route('/shortcuts', { FlowRouter.route('/shortcuts', {
name: 'shortcuts', name: 'shortcuts',
action: function(params) { action() {
const shortcutsTemplate = 'keyboardShortcuts'; const shortcutsTemplate = 'keyboardShortcuts';
EscapeActions.executeUpTo('popup-close'); EscapeActions.executeUpTo('popup-close');
if (previousPath) { if (previousPath) {
Modal.open(shortcutsTemplate, { Modal.open(shortcutsTemplate, {
onCloseGoTo: previousPath onCloseGoTo: previousPath,
}); });
} else { } else {
// XXX There is currently no way to escape this page on Sandstorm // XXX There is currently no way to escape this page on Sandstorm
BlazeLayout.render('defaultLayout', { content: shortcutsTemplate }); BlazeLayout.render('defaultLayout', { content: shortcutsTemplate });
} }
} },
}); });
FlowRouter.notFound = { FlowRouter.notFound = {
action: function() { action() {
BlazeLayout.render('defaultLayout', { content: 'notFound' }); BlazeLayout.render('defaultLayout', { content: 'notFound' });
} },
} };
// We maintain a list of redirections to ensure that we don't break old URLs // We maintain a list of redirections to ensure that we don't break old URLs
// when we change our routing scheme. // when we change our routing scheme.
var redirections = { const redirections = {
'/boards': '/', '/boards': '/',
'/boards/:id/:slug': '/b/:id/:slug', '/boards/:id/:slug': '/b/:id/:slug',
'/boards/:id/:slug/:cardId': '/b/:id/:slug/:cardId' '/boards/:id/:slug/:cardId': '/b/:id/:slug/:cardId',
}; };
_.each(redirections, function(newPath, oldPath) { _.each(redirections, (newPath, oldPath) => {
FlowRouter.route(oldPath, { FlowRouter.route(oldPath, {
triggersEnter: [function(context, redirect) { triggersEnter: [(context, redirect) => {
redirect(FlowRouter.path(newPath, context.params)); redirect(FlowRouter.path(newPath, context.params));
}] }],
}); });
}); });

View file

@ -1,42 +1,40 @@
// XXX Should we use something like Moderniz instead of our custom detector? // XXX Should we use something like Moderniz instead of our custom detector?
var whichTransitionEvent = function() { function whichTransitionEvent() {
var t; const el = document.createElement('fakeelement');
var el = document.createElement('fakeelement'); const transitions = {
var transitions = {
transition:'transitionend', transition:'transitionend',
OTransition:'oTransitionEnd', OTransition:'oTransitionEnd',
MSTransition:'msTransitionEnd', MSTransition:'msTransitionEnd',
MozTransition:'transitionend', MozTransition:'transitionend',
WebkitTransition:'webkitTransitionEnd' WebkitTransition:'webkitTransitionEnd',
}; };
for (t in transitions) { for (const t in transitions) {
if (el.style[t] !== undefined) { if (el.style[t] !== undefined) {
return transitions[t]; return transitions[t];
} }
} }
}; }
var whichAnimationEvent = function() { function whichAnimationEvent() {
var t; const el = document.createElement('fakeelement');
var el = document.createElement('fakeelement'); const transitions = {
var transitions = {
animation:'animationend', animation:'animationend',
OAnimation:'oAnimationEnd', OAnimation:'oAnimationEnd',
MSTransition:'msAnimationEnd', MSTransition:'msAnimationEnd',
MozAnimation:'animationend', MozAnimation:'animationend',
WebkitAnimation:'webkitAnimationEnd' WebkitAnimation:'webkitAnimationEnd',
}; };
for (t in transitions) { for (const t in transitions) {
if (el.style[t] !== undefined) { if (el.style[t] !== undefined) {
return transitions[t]; return transitions[t];
} }
} }
}; }
CSSEvents = { CSSEvents = {
transitionend: whichTransitionEvent(), transitionend: whichTransitionEvent(),
animationend: whichAnimationEvent() animationend: whichAnimationEvent(),
}; };

View file

@ -31,7 +31,7 @@ EscapeActions = {
enabledOnClick = true; enabledOnClick = true;
} }
let noClickEscapeOn = options.noClickEscapeOn; const noClickEscapeOn = options.noClickEscapeOn;
this._actions = _.sortBy([...this._actions, { this._actions = _.sortBy([...this._actions, {
priority, priority,
@ -44,20 +44,20 @@ EscapeActions = {
executeLowest() { executeLowest() {
return this._execute({ return this._execute({
multipleAction: false multipleAction: false,
}); });
}, },
executeAll() { executeAll() {
return this._execute({ return this._execute({
multipleActions: true multipleActions: true,
}); });
}, },
executeUpTo(maxLabel) { executeUpTo(maxLabel) {
return this._execute({ return this._execute({
maxLabel: maxLabel, maxLabel,
multipleActions: true multipleActions: true,
}); });
}, },
@ -66,10 +66,10 @@ EscapeActions = {
this._nextclickPrevented = false; this._nextclickPrevented = false;
} else { } else {
return this._execute({ return this._execute({
maxLabel: maxLabel, maxLabel,
multipleActions: false, multipleActions: false,
isClick: true, isClick: true,
clickTarget: target clickTarget: target,
}); });
} }
}, },
@ -79,7 +79,7 @@ EscapeActions = {
}, },
_stopClick(action, clickTarget) { _stopClick(action, clickTarget) {
if (! _.isString(action.noClickEscapeOn)) if (!_.isString(action.noClickEscapeOn))
return false; return false;
else else
return $(clickTarget).closest(action.noClickEscapeOn).length > 0; return $(clickTarget).closest(action.noClickEscapeOn).length > 0;
@ -88,86 +88,46 @@ EscapeActions = {
_execute(options) { _execute(options) {
const maxLabel = options.maxLabel; const maxLabel = options.maxLabel;
const multipleActions = options.multipleActions; const multipleActions = options.multipleActions;
const isClick = !! options.isClick; const isClick = Boolean(options.isClick);
const clickTarget = options.clickTarget; const clickTarget = options.clickTarget;
let executedAtLeastOne = false; let executedAtLeastOne = false;
let maxPriority; let maxPriority;
if (! maxLabel) if (!maxLabel)
maxPriority = Infinity; maxPriority = Infinity;
else else
maxPriority = this.hierarchy.indexOf(maxLabel); maxPriority = this.hierarchy.indexOf(maxLabel);
for (let currentAction of this._actions) { for (const currentAction of this._actions) {
if (currentAction.priority > maxPriority) if (currentAction.priority > maxPriority)
return executedAtLeastOne; return executedAtLeastOne;
if (isClick && this._stopClick(currentAction, clickTarget)) if (isClick && this._stopClick(currentAction, clickTarget))
return executedAtLeastOne; return executedAtLeastOne;
let isEnabled = currentAction.enabledOnClick || ! isClick; const isEnabled = currentAction.enabledOnClick || !isClick;
if (isEnabled && currentAction.condition()) { if (isEnabled && currentAction.condition()) {
currentAction.action(); currentAction.action();
executedAtLeastOne = true; executedAtLeastOne = true;
if (! multipleActions) if (!multipleActions)
return executedAtLeastOne; return executedAtLeastOne;
} }
} }
return executedAtLeastOne; return executedAtLeastOne;
} },
};
// MouseTrap plugin bindGlobal plugin. Adds a bindGlobal method to Mousetrap
// that allows you to bind specific keyboard shortcuts that will still work
// inside a text input field.
//
// usage:
// Mousetrap.bindGlobal('ctrl+s', _saveChanges);
//
// source:
// https://github.com/ccampbell/mousetrap/tree/master/plugins/global-bind
var _globalCallbacks = {};
var _originalStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = function(e, element, combo, sequence) {
var self = this;
if (self.paused) {
return true;
}
if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
return false;
}
return _originalStopCallback.call(self, e, element, combo);
};
Mousetrap.bindGlobal = function(keys, callback, action) {
var self = this;
self.bind(keys, callback, action);
if (keys instanceof Array) {
for (var i = 0; i < keys.length; i++) {
_globalCallbacks[keys[i]] = true;
}
return;
}
_globalCallbacks[keys] = true;
}; };
// Pressing escape to execute one escape action. We use `bindGloabal` vecause // Pressing escape to execute one escape action. We use `bindGloabal` vecause
// the shortcut sould work on textarea and inputs as well. // the shortcut sould work on textarea and inputs as well.
Mousetrap.bindGlobal('esc', function() { Mousetrap.bindGlobal('esc', () => {
EscapeActions.executeLowest(); EscapeActions.executeLowest();
}); });
// On a left click on the document, we try to exectute one escape action (eg, // On a left click on the document, we try to exectute one escape action (eg,
// close the popup). We don't execute any action if the user has clicked on a // close the popup). We don't execute any action if the user has clicked on a
// link or a button. // link or a button.
$(document).on('click', function(evt) { $(document).on('click', (evt) => {
if (evt.button === 0 && if (evt.button === 0 &&
$(evt.target).closest('a,button,.is-editable').length === 0) { $(evt.target).closest('a,button,.is-editable').length === 0) {
EscapeActions.clickExecute(evt.target, 'multiselection'); EscapeActions.clickExecute(evt.target, 'multiselection');

View file

@ -4,66 +4,66 @@
// goal is to filter complete documents by using the local filters for each // goal is to filter complete documents by using the local filters for each
// fields. // fields.
var showFilterSidebar = function() { function showFilterSidebar() {
Sidebar.setView('filter'); Sidebar.setView('filter');
}; }
// Use a "set" filter for a field that is a set of documents uniquely // Use a "set" filter for a field that is a set of documents uniquely
// identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`. // identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
var SetFilter = function() { class SetFilter {
this._dep = new Tracker.Dependency(); constructor() {
this._selectedElements = []; this._dep = new Tracker.Dependency();
}; this._selectedElements = [];
}
_.extend(SetFilter.prototype, { isSelected(val) {
isSelected: function(val) {
this._dep.depend(); this._dep.depend();
return this._selectedElements.indexOf(val) > -1; return this._selectedElements.indexOf(val) > -1;
}, }
add: function(val) { add(val) {
if (this._indexOfVal(val) === -1) { if (this._indexOfVal(val) === -1) {
this._selectedElements.push(val); this._selectedElements.push(val);
this._dep.changed(); this._dep.changed();
showFilterSidebar(); showFilterSidebar();
} }
}, }
remove: function(val) { remove(val) {
var indexOfVal = this._indexOfVal(val); const indexOfVal = this._indexOfVal(val);
if (this._indexOfVal(val) !== -1) { if (this._indexOfVal(val) !== -1) {
this._selectedElements.splice(indexOfVal, 1); this._selectedElements.splice(indexOfVal, 1);
this._dep.changed(); this._dep.changed();
} }
}, }
toogle: function(val) { toogle(val) {
if (this._indexOfVal(val) === -1) { if (this._indexOfVal(val) === -1) {
this.add(val); this.add(val);
} else { } else {
this.remove(val); this.remove(val);
} }
}, }
reset: function() { reset() {
this._selectedElements = []; this._selectedElements = [];
this._dep.changed(); this._dep.changed();
}, }
_indexOfVal: function(val) { _indexOfVal(val) {
return this._selectedElements.indexOf(val); return this._selectedElements.indexOf(val);
}, }
_isActive: function() { _isActive() {
this._dep.depend(); this._dep.depend();
return this._selectedElements.length !== 0; return this._selectedElements.length !== 0;
}, }
_getMongoSelector: function() { _getMongoSelector() {
this._dep.depend(); this._dep.depend();
return { $in: this._selectedElements }; return { $in: this._selectedElements };
} }
}); }
// The global Filter object. // The global Filter object.
// XXX It would be possible to re-write this object more elegantly, and removing // XXX It would be possible to re-write this object more elegantly, and removing
@ -84,50 +84,46 @@ Filter = {
_exceptions: [], _exceptions: [],
_exceptionsDep: new Tracker.Dependency(), _exceptionsDep: new Tracker.Dependency(),
isActive: function() { isActive() {
var self = this; return _.any(this._fields, (fieldName) => {
return _.any(self._fields, function(fieldName) { return this[fieldName]._isActive();
return self[fieldName]._isActive();
}); });
}, },
_getMongoSelector: function() { _getMongoSelector() {
var self = this; if (!this.isActive())
if (! self.isActive())
return {}; return {};
var filterSelector = {}; const filterSelector = {};
_.forEach(self._fields, function(fieldName) { _.forEach(this._fields, (fieldName) => {
var filter = self[fieldName]; const filter = this[fieldName];
if (filter._isActive()) if (filter._isActive())
filterSelector[fieldName] = filter._getMongoSelector(); filterSelector[fieldName] = filter._getMongoSelector();
}); });
var exceptionsSelector = {_id: {$in: this._exceptions}}; const exceptionsSelector = {_id: {$in: this._exceptions}};
this._exceptionsDep.depend(); this._exceptionsDep.depend();
return {$or: [filterSelector, exceptionsSelector]}; return {$or: [filterSelector, exceptionsSelector]};
}, },
mongoSelector: function(additionalSelector) { mongoSelector(additionalSelector) {
var filterSelector = this._getMongoSelector(); const filterSelector = this._getMongoSelector();
if (_.isUndefined(additionalSelector)) if (_.isUndefined(additionalSelector))
return filterSelector; return filterSelector;
else else
return {$and: [filterSelector, additionalSelector]}; return {$and: [filterSelector, additionalSelector]};
}, },
reset: function() { reset() {
var self = this; _.forEach(this._fields, (fieldName) => {
_.forEach(self._fields, function(fieldName) { const filter = this[fieldName];
var filter = self[fieldName];
filter.reset(); filter.reset();
}); });
self.resetExceptions(); this.resetExceptions();
}, },
addException: function(_id) { addException(_id) {
if (this.isActive()) { if (this.isActive()) {
this._exceptions.push(_id); this._exceptions.push(_id);
this._exceptionsDep.changed(); this._exceptionsDep.changed();
@ -135,10 +131,10 @@ Filter = {
} }
}, },
resetExceptions: function() { resetExceptions() {
this._exceptions = []; this._exceptions = [];
this._exceptionsDep.changed(); this._exceptionsDep.changed();
} },
}; };
Blaze.registerHelper('Filter', Filter); Blaze.registerHelper('Filter', Filter);

View file

@ -2,9 +2,9 @@
// the language reactively. If the user is not connected we use the language // the language reactively. If the user is not connected we use the language
// information provided by the browser, and default to english. // information provided by the browser, and default to english.
Tracker.autorun(function() { Tracker.autorun(() => {
var language; const currentUser = Meteor.user();
var currentUser = Meteor.user(); let language;
if (currentUser) { if (currentUser) {
language = currentUser.profile && currentUser.profile.language; language = currentUser.profile && currentUser.profile.language;
} else { } else {
@ -12,11 +12,10 @@ Tracker.autorun(function() {
} }
if (language) { if (language) {
TAPi18n.setLanguage(language); TAPi18n.setLanguage(language);
// XXX // XXX
var shortLanguage = language.split('-')[0]; const shortLanguage = language.split('-')[0];
T9n.setLanguage(shortLanguage); T9n.setLanguage(shortLanguage);
} }
}); });

View file

@ -13,66 +13,66 @@
// // the content when the form is close (optional) // // the content when the form is close (optional)
// We can only have one inlined form element opened at a time // We can only have one inlined form element opened at a time
currentlyOpenedForm = new ReactiveVar(null); const currentlyOpenedForm = new ReactiveVar(null);
InlinedForm = BlazeComponent.extendComponent({ InlinedForm = BlazeComponent.extendComponent({
template: function() { template() {
return 'inlinedForm'; return 'inlinedForm';
}, },
onCreated: function() { onCreated() {
this.isOpen = new ReactiveVar(false); this.isOpen = new ReactiveVar(false);
}, },
onDestroyed: function() { onDestroyed() {
currentlyOpenedForm.set(null); currentlyOpenedForm.set(null);
}, },
open: function() { open() {
// Close currently opened form, if any // Close currently opened form, if any
EscapeActions.executeUpTo('inlinedForm'); EscapeActions.executeUpTo('inlinedForm');
this.isOpen.set(true); this.isOpen.set(true);
currentlyOpenedForm.set(this); currentlyOpenedForm.set(this);
}, },
close: function() { close() {
this.isOpen.set(false); this.isOpen.set(false);
currentlyOpenedForm.set(null); currentlyOpenedForm.set(null);
}, },
getValue: function() { getValue() {
var input = this.find('textarea,input[type=text]'); const input = this.find('textarea,input[type=text]');
return this.isOpen.get() && input && input.value; return this.isOpen.get() && input && input.value;
}, },
events: function() { events() {
return [{ return [{
'click .js-close-inlined-form': this.close, 'click .js-close-inlined-form': this.close,
'click .js-open-inlined-form': this.open, 'click .js-open-inlined-form': this.open,
// Pressing Ctrl+Enter should submit the form // Pressing Ctrl+Enter should submit the form
'keydown form textarea': function(evt) { 'keydown form textarea'(evt) {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) { if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
this.find('button[type=submit]').click(); this.find('button[type=submit]').click();
} }
}, },
// Close the inlined form when after its submission // Close the inlined form when after its submission
submit: function() { submit() {
if (this.currentData().autoclose !== false) { if (this.currentData().autoclose !== false) {
Tracker.afterFlush(() => { Tracker.afterFlush(() => {
this.close(); this.close();
}); });
} }
} },
}]; }];
} },
}).register('inlinedForm'); }).register('inlinedForm');
// Press escape to close the currently opened inlinedForm // Press escape to close the currently opened inlinedForm
EscapeActions.register('inlinedForm', EscapeActions.register('inlinedForm',
function() { currentlyOpenedForm.get().close(); }, () => { currentlyOpenedForm.get().close(); },
function() { return currentlyOpenedForm.get() !== null; }, { () => { return currentlyOpenedForm.get() !== null; }, {
noClickEscapeOn: '.js-inlined-form' noClickEscapeOn: '.js-inlined-form',
} }
); );

View file

@ -24,7 +24,7 @@ Mousetrap.bind('x', () => {
}); });
Mousetrap.bind(['down', 'up'], (evt, key) => { Mousetrap.bind(['down', 'up'], (evt, key) => {
if (! Session.get('currentCard')) { if (!Session.get('currentCard')) {
return; return;
} }
@ -39,24 +39,24 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
Template.keyboardShortcuts.helpers({ Template.keyboardShortcuts.helpers({
mapping: [{ mapping: [{
keys: ['W'], keys: ['W'],
action: 'shortcut-toogle-sidebar' action: 'shortcut-toogle-sidebar',
}, { }, {
keys: ['Q'], keys: ['Q'],
action: 'shortcut-filter-my-cards' action: 'shortcut-filter-my-cards',
}, { }, {
keys: ['X'], keys: ['X'],
action: 'shortcut-clear-filters' action: 'shortcut-clear-filters',
}, { }, {
keys: ['?'], keys: ['?'],
action: 'shortcut-show-shortcuts' action: 'shortcut-show-shortcuts',
}, { }, {
keys: ['ESC'], keys: ['ESC'],
action: 'shortcut-close-dialog' action: 'shortcut-close-dialog',
}, { }, {
keys: ['@'], keys: ['@'],
action: 'shortcut-autocomplete-members' action: 'shortcut-autocomplete-members',
}, { }, {
keys: [':'], keys: [':'],
action: 'shortcut-autocomplete-emojies' action: 'shortcut-autocomplete-emojies',
}] }],
}); });

View file

@ -1,4 +1,4 @@
const closedValue = null const closedValue = null;
window.Modal = new class { window.Modal = new class {
constructor() { constructor() {

View file

@ -1,53 +1,53 @@
var getCardsBetween = function(idA, idB) { function getCardsBetween(idA, idB) {
var pluckId = function(doc) { function pluckId(doc) {
return doc._id; return doc._id;
}; }
var getListsStrictlyBetween = function(id1, id2) { function getListsStrictlyBetween(id1, id2) {
return Lists.find({ return Lists.find({
$and: [ $and: [
{ sort: { $gt: Lists.findOne(id1).sort } }, { sort: { $gt: Lists.findOne(id1).sort } },
{ sort: { $lt: Lists.findOne(id2).sort } } { sort: { $lt: Lists.findOne(id2).sort } },
], ],
archived: false archived: false,
}).map(pluckId); }).map(pluckId);
}; }
var cards = _.sortBy([Cards.findOne(idA), Cards.findOne(idB)], function(c) { const cards = _.sortBy([Cards.findOne(idA), Cards.findOne(idB)], (c) => {
return c.sort; return c.sort;
}); });
var selector; let selector;
if (cards[0].listId === cards[1].listId) { if (cards[0].listId === cards[1].listId) {
selector = { selector = {
listId: cards[0].listId, listId: cards[0].listId,
sort: { sort: {
$gte: cards[0].sort, $gte: cards[0].sort,
$lte: cards[1].sort $lte: cards[1].sort,
}, },
archived: false archived: false,
}; };
} else { } else {
selector = { selector = {
$or: [{ $or: [{
listId: cards[0].listId, listId: cards[0].listId,
sort: { $lte: cards[0].sort } sort: { $lte: cards[0].sort },
}, { }, {
listId: { listId: {
$in: getListsStrictlyBetween(cards[0].listId, cards[1].listId) $in: getListsStrictlyBetween(cards[0].listId, cards[1].listId),
} },
}, { }, {
listId: cards[1].listId, listId: cards[1].listId,
sort: { $gte: cards[1].sort } sort: { $gte: cards[1].sort },
}], }],
archived: false archived: false,
}; };
} }
return Cards.find(Filter.mongoSelector(selector)).map(pluckId); return Cards.find(Filter.mongoSelector(selector)).map(pluckId);
}; }
MultiSelection = { MultiSelection = {
sidebarView: 'multiselection', sidebarView: 'multiselection',
@ -58,30 +58,30 @@ MultiSelection = {
startRangeCardId: null, startRangeCardId: null,
reset: function() { reset() {
this._selectedCards.set([]); this._selectedCards.set([]);
}, },
getMongoSelector: function() { getMongoSelector() {
return Filter.mongoSelector({ return Filter.mongoSelector({
_id: { $in: this._selectedCards.get() } _id: { $in: this._selectedCards.get() },
}); });
}, },
isActive: function() { isActive() {
return this._isActive.get(); return this._isActive.get();
}, },
count: function() { count() {
return Cards.find(this.getMongoSelector()).count(); return Cards.find(this.getMongoSelector()).count();
}, },
isEmpty: function() { isEmpty() {
return this.count() === 0; return this.count() === 0;
}, },
activate: function() { activate() {
if (! this.isActive()) { if (!this.isActive()) {
EscapeActions.executeUpTo('detailsPane'); EscapeActions.executeUpTo('detailsPane');
this._isActive.set(true); this._isActive.set(true);
Tracker.flush(); Tracker.flush();
@ -89,7 +89,7 @@ MultiSelection = {
Sidebar.setView(this.sidebarView); Sidebar.setView(this.sidebarView);
}, },
disable: function() { disable() {
if (this.isActive()) { if (this.isActive()) {
this._isActive.set(false); this._isActive.set(false);
if (Sidebar && Sidebar.getView() === this.sidebarView) { if (Sidebar && Sidebar.getView() === this.sidebarView) {
@ -99,19 +99,19 @@ MultiSelection = {
} }
}, },
add: function(cardIds) { add(cardIds) {
return this.toogle(cardIds, { add: true, remove: false }); return this.toogle(cardIds, { add: true, remove: false });
}, },
remove: function(cardIds) { remove(cardIds) {
return this.toogle(cardIds, { add: false, remove: true }); return this.toogle(cardIds, { add: false, remove: true });
}, },
toogleRange: function(cardId) { toogleRange(cardId) {
var selectedCards = this._selectedCards.get(); const selectedCards = this._selectedCards.get();
var startRange; let startRange;
this.reset(); this.reset();
if (! this.isActive() || selectedCards.length === 0) { if (!this.isActive() || selectedCards.length === 0) {
this.toogle(cardId); this.toogle(cardId);
} else { } else {
startRange = selectedCards[selectedCards.length - 1]; startRange = selectedCards[selectedCards.length - 1];
@ -119,23 +119,22 @@ MultiSelection = {
} }
}, },
toogle: function(cardIds, options) { toogle(cardIds, options) {
var self = this;
cardIds = _.isString(cardIds) ? [cardIds] : cardIds; cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
options = _.extend({ options = _.extend({
add: true, add: true,
remove: true remove: true,
}, options || {}); }, options || {});
if (! self.isActive()) { if (!this.isActive()) {
self.reset(); this.reset();
self.activate(); this.activate();
} }
var selectedCards = self._selectedCards.get(); const selectedCards = this._selectedCards.get();
_.each(cardIds, function(cardId) { _.each(cardIds, (cardId) => {
var indexOfCard = selectedCards.indexOf(cardId); const indexOfCard = selectedCards.indexOf(cardId);
if (options.remove && indexOfCard > -1) if (options.remove && indexOfCard > -1)
selectedCards.splice(indexOfCard, 1); selectedCards.splice(indexOfCard, 1);
@ -144,19 +143,19 @@ MultiSelection = {
selectedCards.push(cardId); selectedCards.push(cardId);
}); });
self._selectedCards.set(selectedCards); this._selectedCards.set(selectedCards);
}, },
isSelected: function(cardId) { isSelected(cardId) {
return this._selectedCards.get().indexOf(cardId) > -1; return this._selectedCards.get().indexOf(cardId) > -1;
} },
}; };
Blaze.registerHelper('MultiSelection', MultiSelection); Blaze.registerHelper('MultiSelection', MultiSelection);
EscapeActions.register('multiselection', EscapeActions.register('multiselection',
function() { MultiSelection.disable(); }, () => { MultiSelection.disable(); },
function() { return MultiSelection.isActive(); }, { () => { return MultiSelection.isActive(); }, {
noClickEscapeOn: '.js-minicard,.js-board-sidebar-content' noClickEscapeOn: '.js-minicard,.js-board-sidebar-content',
} }
); );

View file

@ -1,55 +1,53 @@
// A simple tracker dependency that we invalidate every time the window is // A simple tracker dependency that we invalidate every time the window is
// resized. This is used to reactively re-calculate the popup position in case // resized. This is used to reactively re-calculate the popup position in case
// of a window resize. This is the equivalent of a "Signal" in some other // of a window resize. This is the equivalent of a "Signal" in some other
// programming environments. // programming environments (eg, elm).
let windowResizeDep = new Tracker.Dependency() const windowResizeDep = new Tracker.Dependency();
$(window).on('resize', () => windowResizeDep.changed()) $(window).on('resize', () => windowResizeDep.changed());
window.Popup = new class { window.Popup = new class {
constructor() { constructor() {
// The template we use to render popups // The template we use to render popups
this.template = Template.popup this.template = Template.popup;
// We only want to display one popup at a time and we keep the view object // We only want to display one popup at a time and we keep the view object
// in this `Popup._current` variable. If there is no popup currently opened // in this `Popup._current` variable. If there is no popup currently opened
// the value is `null`. // the value is `null`.
this._current = null this._current = null;
// It's possible to open a sub-popup B from a popup A. In that case we keep // It's possible to open a sub-popup B from a popup A. In that case we keep
// the data of popup A so we can return back to it. Every time we open a new // the data of popup A so we can return back to it. Every time we open a new
// popup the stack grows, every time we go back the stack decrease, and if // popup the stack grows, every time we go back the stack decrease, and if
// we close the popup the stack is reseted to the empty stack []. // we close the popup the stack is reseted to the empty stack [].
this._stack = [] this._stack = [];
// We invalidate this internal dependency every time the top of the stack // We invalidate this internal dependency every time the top of the stack
// has changed and we want to re-render a popup with the new top-stack data. // has changed and we want to re-render a popup with the new top-stack data.
this._dep = new Tracker.Dependency() this._dep = new Tracker.Dependency();
} }
/// This function returns a callback that can be used in an event map: /// This function returns a callback that can be used in an event map:
///
/// Template.tplName.events({ /// Template.tplName.events({
/// 'click .elementClass': Popup.open("popupName") /// 'click .elementClass': Popup.open("popupName"),
/// }) /// });
///
/// The popup inherit the data context of its parent. /// The popup inherit the data context of its parent.
open(name) { open(name) {
let self = this const self = this;
const popupName = `${name}Popup` const popupName = `${name}Popup`;
function clickFromPopup(evt) { function clickFromPopup(evt) {
return $(evt.target).closest('.js-pop-over').length !== 0 return $(evt.target).closest('.js-pop-over').length !== 0;
} }
return function(evt) { return function(evt) {
// If a popup is already opened, clicking again on the opener element // If a popup is already opened, clicking again on the opener element
// should close it -- and interrupt the current `open` function. // should close it -- and interrupt the current `open` function.
if (self.isOpen()) { if (self.isOpen()) {
let previousOpenerElement = self._getTopStack().openerElement const previousOpenerElement = self._getTopStack().openerElement;
if (previousOpenerElement === evt.currentTarget) { if (previousOpenerElement === evt.currentTarget) {
return self.close() return self.close();
} else { } else {
$(previousOpenerElement).removeClass('is-active') $(previousOpenerElement).removeClass('is-active');
} }
} }
@ -58,16 +56,16 @@ window.Popup = new class {
// if the popup has no parent, or from the parent `openerElement` if it // if the popup has no parent, or from the parent `openerElement` if it
// has one. This allows us to position a sub-popup exactly at the same // has one. This allows us to position a sub-popup exactly at the same
// position than its parent. // position than its parent.
let openerElement let openerElement;
if (clickFromPopup(evt)) { if (clickFromPopup(evt)) {
openerElement = self._getTopStack().openerElement openerElement = self._getTopStack().openerElement;
} else { } else {
self._stack = [] self._stack = [];
openerElement = evt.currentTarget openerElement = evt.currentTarget;
} }
$(openerElement).addClass('is-active') $(openerElement).addClass('is-active');
evt.preventDefault() evt.preventDefault();
// We push our popup data to the stack. The top of the stack is always // We push our popup data to the stack. The top of the stack is always
// used as the data source for our current popup. // used as the data source for our current popup.
@ -79,7 +77,7 @@ window.Popup = new class {
depth: self._stack.length, depth: self._stack.length,
offset: self._getOffset(openerElement), offset: self._getOffset(openerElement),
dataContext: this.currentData && this.currentData() || this, dataContext: this.currentData && this.currentData() || this,
}) });
// If there are no popup currently opened we use the Blaze API to render // If there are no popup currently opened we use the Blaze API to render
// one into the DOM. We use a reactive function as the data parameter that // one into the DOM. We use a reactive function as the data parameter that
@ -90,39 +88,38 @@ window.Popup = new class {
// Otherwise if there is already a popup open we just need to invalidate // Otherwise if there is already a popup open we just need to invalidate
// our internal dependency, and since we just changed the top element of // our internal dependency, and since we just changed the top element of
// our internal stack, the popup will be updated with the new data. // our internal stack, the popup will be updated with the new data.
if (! self.isOpen()) { if (!self.isOpen()) {
self.current = Blaze.renderWithData(self.template, () => { self.current = Blaze.renderWithData(self.template, () => {
self._dep.depend() self._dep.depend();
return _.extend(self._getTopStack(), { stack: self._stack }) return _.extend(self._getTopStack(), { stack: self._stack });
}, document.body) }, document.body);
} else { } else {
self._dep.changed() self._dep.changed();
} }
} };
} }
/// This function returns a callback that can be used in an event map: /// This function returns a callback that can be used in an event map:
///
/// Template.tplName.events({ /// Template.tplName.events({
/// 'click .elementClass': Popup.afterConfirm("popupName", function() { /// 'click .elementClass': Popup.afterConfirm("popupName", function() {
/// // What to do after the user has confirmed the action /// // What to do after the user has confirmed the action
/// }) /// }),
/// }) /// });
afterConfirm(name, action) { afterConfirm(name, action) {
let self = this const self = this;
return function(evt, tpl) { return function(evt, tpl) {
let context = this.currentData && this.currentData() || this const context = this.currentData && this.currentData() || this;
context.__afterConfirmAction = action context.__afterConfirmAction = action;
self.open(name).call(context, evt, tpl) self.open(name).call(context, evt, tpl);
} };
} }
/// The public reactive state of the popup. /// The public reactive state of the popup.
isOpen() { isOpen() {
this._dep.changed() this._dep.changed();
return !! this.current return Boolean(this.current);
} }
/// In case the popup was opened from a parent popup we can get back to it /// In case the popup was opened from a parent popup we can get back to it
@ -132,45 +129,45 @@ window.Popup = new class {
/// steps back is greater than the popup stack size, the popup will be closed. /// steps back is greater than the popup stack size, the popup will be closed.
back(n = 1) { back(n = 1) {
if (this._stack.length > n) { if (this._stack.length > n) {
_.times(n, () => this._stack.pop()) _.times(n, () => this._stack.pop());
this._dep.changed() this._dep.changed();
} else { } else {
this.close() this.close();
} }
} }
/// Close the current opened popup. /// Close the current opened popup.
close() { close() {
if (this.isOpen()) { if (this.isOpen()) {
Blaze.remove(this.current) Blaze.remove(this.current);
this.current = null this.current = null;
let openerElement = this._getTopStack().openerElement const openerElement = this._getTopStack().openerElement;
$(openerElement).removeClass('is-active') $(openerElement).removeClass('is-active');
this._stack = [] this._stack = [];
} }
} }
// An utility fonction that returns the top element of the internal stack // An utility fonction that returns the top element of the internal stack
_getTopStack() { _getTopStack() {
return this._stack[this._stack.length - 1] return this._stack[this._stack.length - 1];
} }
// We automatically calculate the popup offset from the reference element // We automatically calculate the popup offset from the reference element
// position and dimensions. We also reactively use the window dimensions to // position and dimensions. We also reactively use the window dimensions to
// ensure that the popup is always visible on the screen. // ensure that the popup is always visible on the screen.
_getOffset(element) { _getOffset(element) {
let $element = $(element) const $element = $(element);
return () => { return () => {
windowResizeDep.depend() windowResizeDep.depend();
const offset = $element.offset() const offset = $element.offset();
const popupWidth = 300 + 15 const popupWidth = 300 + 15;
return { return {
left: Math.min(offset.left, $(window).width() - popupWidth), left: Math.min(offset.left, $(window).width() - popupWidth),
top: offset.top + $element.outerHeight(), top: offset.top + $element.outerHeight(),
} };
} };
} }
// We get the title from the translation files. Instead of returning the // We get the title from the translation files. Instead of returning the
@ -178,22 +175,22 @@ window.Popup = new class {
// is a reactive data source, the title will be changed reactively. // is a reactive data source, the title will be changed reactively.
_getTitle(popupName) { _getTitle(popupName) {
return () => { return () => {
const translationKey = `${popupName}-title` const translationKey = `${popupName}-title`;
// XXX There is no public API to check if there is an available // XXX There is no public API to check if there is an available
// translation for a given key. So we try to translate the key and if the // translation for a given key. So we try to translate the key and if the
// translation output equals the key input we deduce that no translation // translation output equals the key input we deduce that no translation
// was available and returns `false`. There is a (small) risk a false // was available and returns `false`. There is a (small) risk a false
// positives. // positives.
const title = TAPi18n.__(translationKey) const title = TAPi18n.__(translationKey);
return title !== translationKey ? title : false return title !== translationKey ? title : false;
} };
} }
} };
// We close a potential opened popup on any left click on the document, or go // We close a potential opened popup on any left click on the document, or go
// one step back by pressing escape. // one step back by pressing escape.
const escapeActions = ['back', 'close'] const escapeActions = ['back', 'close'];
_.each(escapeActions, (actionName) => { _.each(escapeActions, (actionName) => {
EscapeActions.register(`popup-${actionName}`, EscapeActions.register(`popup-${actionName}`,
() => Popup[actionName](), () => Popup[actionName](),
@ -202,6 +199,6 @@ _.each(escapeActions, (actionName) => {
noClickEscapeOn: '.js-pop-over', noClickEscapeOn: '.js-pop-over',
enabledOnClick: actionName === 'close', enabledOnClick: actionName === 'close',
} }
) );
}) });

View file

@ -27,9 +27,9 @@ UnsavedEdits = {
// _collection: UnsavedEditCollection, // _collection: UnsavedEditCollection,
get({ fieldName, docId }, defaultTo = '') { get({ fieldName, docId }, defaultTo = '') {
let unsavedValue = this._getCollectionDocument(fieldName, docId); const unsavedValue = this._getCollectionDocument(fieldName, docId);
if (unsavedValue) { if (unsavedValue) {
return unsavedValue.value return unsavedValue.value;
} else { } else {
return defaultTo; return defaultTo;
} }
@ -40,13 +40,9 @@ UnsavedEdits = {
}, },
set({ fieldName, docId }, value) { set({ fieldName, docId }, value) {
let currentDoc = this._getCollectionDocument(fieldName, docId); const currentDoc = this._getCollectionDocument(fieldName, docId);
if (currentDoc) { if (currentDoc) {
UnsavedEditCollection.update(currentDoc._id, { UnsavedEditCollection.update(currentDoc._id, { $set: { value }});
$set: {
value: value
}
});
} else { } else {
UnsavedEditCollection.insert({ UnsavedEditCollection.insert({
fieldName, fieldName,
@ -57,7 +53,7 @@ UnsavedEdits = {
}, },
reset({ fieldName, docId }) { reset({ fieldName, docId }) {
let currentDoc = this._getCollectionDocument(fieldName, docId); const currentDoc = this._getCollectionDocument(fieldName, docId);
if (currentDoc) { if (currentDoc) {
UnsavedEditCollection.remove(currentDoc._id); UnsavedEditCollection.remove(currentDoc._id);
} }
@ -65,13 +61,13 @@ UnsavedEdits = {
_getCollectionDocument(fieldName, docId) { _getCollectionDocument(fieldName, docId) {
return UnsavedEditCollection.findOne({fieldName, docId}); return UnsavedEditCollection.findOne({fieldName, docId});
} },
} };
Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => { Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => {
// Workaround some blaze feature that ass a list of keywords arguments as the // Workaround some blaze feature that ass a list of keywords arguments as the
// last parameter (even if the caller didn't specify any). // last parameter (even if the caller didn't specify any).
if (! _.isString(defaultTo)) { if (!_.isString(defaultTo)) {
defaultTo = ''; defaultTo = '';
} }
return UnsavedEdits.get({ fieldName, docId }, defaultTo); return UnsavedEdits.get({ fieldName, docId }, defaultTo);

View file

@ -1,62 +1,70 @@
Utils = { Utils = {
// XXX We should remove these two methods // XXX We should remove these two methods
goBoardId: function(_id) { goBoardId(_id) {
var board = Boards.findOne(_id); const board = Boards.findOne(_id);
return board && FlowRouter.go('board', { return board && FlowRouter.go('board', {
id: board._id, id: board._id,
slug: board.slug slug: board.slug,
}); });
}, },
goCardId: function(_id) { goCardId(_id) {
var card = Cards.findOne(_id); const card = Cards.findOne(_id);
var board = Boards.findOne(card.boardId); const board = Boards.findOne(card.boardId);
return board && FlowRouter.go('card', { return board && FlowRouter.go('card', {
cardId: card._id, cardId: card._id,
boardId: board._id, boardId: board._id,
slug: board.slug slug: board.slug,
}); });
}, },
capitalize: function(string) { capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
}, },
getLabelIndex: function(boardId, labelId) { getLabelIndex(boardId, labelId) {
var board = Boards.findOne(boardId); const board = Boards.findOne(boardId);
var labels = {}; const labels = {};
_.each(board.labels, function(a, b) { _.each(board.labels, (a, b) => {
labels[a._id] = b; labels[a._id] = b;
}); });
return { return {
index: labels[labelId], index: labels[labelId],
key: function(key) { key(key) {
return 'labels.' + labels[labelId] + '.' + key; return `labels.${labels[labelId]}.${key}`;
} },
}; };
}, },
// Determine the new sort index // Determine the new sort index
calculateIndex: function(prevCardDomElement, nextCardDomElement, nCards) { calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
nCards = nCards || 1; let base, increment;
// If we drop the card to an empty column // If we drop the card to an empty column
if (! prevCardDomElement && ! nextCardDomElement) { if (!prevCardDomElement && !nextCardDomElement) {
return {base: 0, increment: 1}; base = 0;
increment = 1;
// If we drop the card in the first position // If we drop the card in the first position
} else if (! prevCardDomElement) { } else if (!prevCardDomElement) {
return {base: Blaze.getData(nextCardDomElement).sort - 1, increment: -1}; base = Blaze.getData(nextCardDomElement).sort - 1;
increment = -1;
// If we drop the card in the last position // If we drop the card in the last position
} else if (! nextCardDomElement) { } else if (!nextCardDomElement) {
return {base: Blaze.getData(prevCardDomElement).sort + 1, increment: 1}; base = Blaze.getData(prevCardDomElement).sort + 1;
increment = 1;
} }
// In the general case take the average of the previous and next element // In the general case take the average of the previous and next element
// sort indexes. // sort indexes.
else { else {
var prevSortIndex = Blaze.getData(prevCardDomElement).sort; const prevSortIndex = Blaze.getData(prevCardDomElement).sort;
var nextSortIndex = Blaze.getData(nextCardDomElement).sort; const nextSortIndex = Blaze.getData(nextCardDomElement).sort;
var increment = (nextSortIndex - prevSortIndex) / (nCards + 1); increment = (nextSortIndex - prevSortIndex) / (nCards + 1);
return {base: prevSortIndex + increment, increment: increment}; base = prevSortIndex + increment;
} }
} // XXX Return a generator that yield values instead of a base with a
// increment number.
return {
base,
increment,
};
},
}; };

View file

@ -11,41 +11,41 @@
Activities = new Mongo.Collection('activities'); Activities = new Mongo.Collection('activities');
Activities.helpers({ Activities.helpers({
board: function() { board() {
return Boards.findOne(this.boardId); return Boards.findOne(this.boardId);
}, },
user: function() { user() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
}, },
member: function() { member() {
return Users.findOne(this.memberId); return Users.findOne(this.memberId);
}, },
list: function() { list() {
return Lists.findOne(this.listId); return Lists.findOne(this.listId);
}, },
oldList: function() { oldList() {
return Lists.findOne(this.oldListId); return Lists.findOne(this.oldListId);
}, },
card: function() { card() {
return Cards.findOne(this.cardId); return Cards.findOne(this.cardId);
}, },
comment: function() { comment() {
return CardComments.findOne(this.commentId); return CardComments.findOne(this.commentId);
}, },
attachment: function() { attachment() {
return Attachments.findOne(this.attachmentId); return Attachments.findOne(this.attachmentId);
} },
}); });
Activities.before.insert(function(userId, doc) { Activities.before.insert((userId, doc) => {
doc.createdAt = new Date(); doc.createdAt = new Date();
}); });
// For efficiency create an index on the date of creation. // For efficiency create an index on the date of creation.
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(function() { Meteor.startup(() => {
Activities._collection._ensureIndex({ Activities._collection._ensureIndex({
createdAt: -1 createdAt: -1,
}); });
}); });
} }

View file

@ -3,19 +3,19 @@ Attachments = new FS.Collection('attachments', {
// 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
// the general board view // the general board view
new FS.Store.GridFS('attachments') new FS.Store.GridFS('attachments'),
] ],
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Attachments.allow({ Attachments.allow({
insert: function(userId, doc) { insert(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
update: function(userId, doc) { update(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
remove: function(userId, doc) { remove(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
// We authorize the attachment download either: // We authorize the attachment download either:
@ -26,24 +26,24 @@ if (Meteor.isServer) {
// //
// https://github.com/CollectionFS/Meteor-CollectionFS/issues/449 // https://github.com/CollectionFS/Meteor-CollectionFS/issues/449
// //
download: function(userId, doc) { download(userId, doc) {
var query = { const query = {
$or: [ $or: [
{ 'members.userId': userId }, { 'members.userId': userId },
{ permission: 'public' } { permission: 'public' },
] ],
}; };
return !! Boards.findOne(doc.boardId, query); return Boolean(Boards.findOne(doc.boardId, query));
}, },
fetch: ['boardId'] fetch: ['boardId'],
}); });
} }
// XXX Enforce a schema for the Attachments CollectionFS // XXX Enforce a schema for the Attachments CollectionFS
Attachments.files.before.insert(function(userId, doc) { Attachments.files.before.insert((userId, doc) => {
var file = new FS.File(doc); const file = new FS.File(doc);
doc.userId = userId; doc.userId = userId;
// If the uploaded document is not an image we need to enforce browser // If the uploaded document is not an image we need to enforce browser
@ -54,26 +54,26 @@ Attachments.files.before.insert(function(userId, doc) {
// See https://github.com/libreboard/libreboard/issues/99 // See https://github.com/libreboard/libreboard/issues/99
// XXX Should we use `beforeWrite` option of CollectionFS instead of // XXX Should we use `beforeWrite` option of CollectionFS instead of
// collection-hooks? // collection-hooks?
if (! file.isImage()) { if (!file.isImage()) {
file.original.type = 'application/octet-stream'; file.original.type = 'application/octet-stream';
} }
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Attachments.files.after.insert(function(userId, doc) { Attachments.files.after.insert((userId, doc) => {
Activities.insert({ Activities.insert({
userId,
type: 'card', type: 'card',
activityType: 'addAttachment', activityType: 'addAttachment',
attachmentId: doc._id, attachmentId: doc._id,
boardId: doc.boardId, boardId: doc.boardId,
cardId: doc.cardId, cardId: doc.cardId,
userId: userId
}); });
}); });
Attachments.files.after.remove(function(userId, doc) { Attachments.files.after.remove((userId, doc) => {
Activities.remove({ Activities.remove({
attachmentId: doc._id attachmentId: doc._id,
}); });
}); });
} }

View file

@ -1,27 +1,27 @@
Avatars = new FS.Collection('avatars', { Avatars = new FS.Collection('avatars', {
stores: [ stores: [
new FS.Store.GridFS('avatars') new FS.Store.GridFS('avatars'),
], ],
filter: { filter: {
maxSize: 72000, maxSize: 72000,
allow: { allow: {
contentTypes: ['image/*'] contentTypes: ['image/*'],
} },
} },
}); });
var isOwner = function(userId, file) { function isOwner(userId, file) {
return userId && userId === file.userId; return userId && userId === file.userId;
}; }
Avatars.allow({ Avatars.allow({
insert: isOwner, insert: isOwner,
update: isOwner, update: isOwner,
remove: isOwner, remove: isOwner,
download: function() { return true; }, download() { return true; },
fetch: ['userId'] fetch: ['userId'],
}); });
Avatars.files.before.insert(function(userId, doc) { Avatars.files.before.insert((userId, doc) => {
doc.userId = userId; doc.userId = userId;
}); });

View file

@ -2,27 +2,27 @@ Boards = new Mongo.Collection('boards');
Boards.attachSchema(new SimpleSchema({ Boards.attachSchema(new SimpleSchema({
title: { title: {
type: String type: String,
}, },
slug: { slug: {
type: String type: String,
}, },
archived: { archived: {
type: Boolean type: Boolean,
}, },
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: true denyUpdate: true,
}, },
// XXX Inconsistent field naming // XXX Inconsistent field naming
modifiedAt: { modifiedAt: {
type: Date, type: Date,
denyInsert: true, denyInsert: true,
optional: true optional: true,
}, },
// De-normalized number of users that have starred this board // De-normalized number of users that have starred this board
stars: { stars: {
type: Number type: Number,
}, },
// De-normalized label system // De-normalized label system
'labels.$._id': { 'labels.$._id': {
@ -31,46 +31,46 @@ Boards.attachSchema(new SimpleSchema({
// always set on the server. // always set on the server.
// XXX Actually if we create a new label, the `_id` is set on the client // XXX Actually if we create a new label, the `_id` is set on the client
// without being overwritten by the server, could it be a problem? // without being overwritten by the server, could it be a problem?
type: String type: String,
}, },
'labels.$.name': { 'labels.$.name': {
type: String, type: String,
optional: true optional: true,
}, },
'labels.$.color': { 'labels.$.color': {
type: String, type: String,
allowedValues: [ allowedValues: [
'green', 'yellow', 'orange', 'red', 'purple', 'green', 'yellow', 'orange', 'red', 'purple',
'blue', 'sky', 'lime', 'pink', 'black' 'blue', 'sky', 'lime', 'pink', 'black',
] ],
}, },
// XXX We might want to maintain more informations under the member sub- // XXX We might want to maintain more informations under the member sub-
// documents like de-normalized meta-data (the date the member joined the // documents like de-normalized meta-data (the date the member joined the
// board, the number of contributions, etc.). // board, the number of contributions, etc.).
'members.$.userId': { 'members.$.userId': {
type: String type: String,
}, },
'members.$.isAdmin': { 'members.$.isAdmin': {
type: Boolean type: Boolean,
}, },
'members.$.isActive': { 'members.$.isActive': {
type: Boolean type: Boolean,
}, },
permission: { permission: {
type: String, type: String,
allowedValues: ['public', 'private'] allowedValues: ['public', 'private'],
}, },
color: { color: {
type: String, type: String,
allowedValues: [ allowedValues: [
'belize', 'belize',
'nephritis', 'nephritis',
'pomegranate', 'pomegranate',
'pumpkin', 'pumpkin',
'wisteria', 'wisteria',
'midnight', 'midnight',
] ],
} },
})); }));
if (Meteor.isServer) { if (Meteor.isServer) {
@ -78,30 +78,30 @@ if (Meteor.isServer) {
insert: Meteor.userId, insert: Meteor.userId,
update: allowIsBoardAdmin, update: allowIsBoardAdmin,
remove: allowIsBoardAdmin, remove: allowIsBoardAdmin,
fetch: ['members'] fetch: ['members'],
}); });
// The number of users that have starred this board is managed by trusted code // The number of users that have starred this board is managed by trusted code
// and the user is not allowed to update it // and the user is not allowed to update it
Boards.deny({ Boards.deny({
update: function(userId, board, fieldNames) { update(userId, board, fieldNames) {
return _.contains(fieldNames, 'stars'); return _.contains(fieldNames, 'stars');
}, },
fetch: [] fetch: [],
}); });
// We can't remove a member if it is the last administrator // We can't remove a member if it is the last administrator
Boards.deny({ Boards.deny({
update: function(userId, doc, fieldNames, modifier) { update(userId, doc, fieldNames, modifier) {
if (! _.contains(fieldNames, 'members')) if (!_.contains(fieldNames, 'members'))
return false; return false;
// We only care in case of a $pull operation, ie remove a member // We only care in case of a $pull operation, ie remove a member
if (! _.isObject(modifier.$pull && modifier.$pull.members)) if (!_.isObject(modifier.$pull && modifier.$pull.members))
return false; return false;
// If there is more than one admin, it's ok to remove anyone // If there is more than one admin, it's ok to remove anyone
var nbAdmins = _.filter(doc.members, function(member) { const nbAdmins = _.filter(doc.members, (member) => {
return member.isAdmin; return member.isAdmin;
}).length; }).length;
if (nbAdmins > 1) if (nbAdmins > 1)
@ -109,36 +109,36 @@ if (Meteor.isServer) {
// If all the previous conditions were verified, we can't remove // If all the previous conditions were verified, we can't remove
// a user if it's an admin // a user if it's an admin
var removedMemberId = modifier.$pull.members.userId; const removedMemberId = modifier.$pull.members.userId;
return !! _.findWhere(doc.members, { return Boolean(_.findWhere(doc.members, {
userId: removedMemberId, userId: removedMemberId,
isAdmin: true isAdmin: true,
}); }));
}, },
fetch: ['members'] fetch: ['members'],
}); });
} }
Boards.helpers({ Boards.helpers({
isPublic: function() { isPublic() {
return this.permission === 'public'; return this.permission === 'public';
}, },
lists: function() { lists() {
return Lists.find({ boardId: this._id, archived: false }, return Lists.find({ boardId: this._id, archived: false },
{ sort: { sort: 1 }}); { sort: { sort: 1 }});
}, },
activities: function() { activities() {
return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }}); return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }});
}, },
absoluteUrl: function() { absoluteUrl() {
return FlowRouter.path('board', { id: this._id, slug: this.slug }); return FlowRouter.path('board', { id: this._id, slug: this.slug });
}, },
colorClass: function() { colorClass() {
return 'board-color-' + this.color; return `board-color-${this.color}`;
} },
}); });
Boards.before.insert(function(userId, doc) { Boards.before.insert((userId, doc) => {
// XXX We need to improve slug management. Only the id should be necessary // XXX We need to improve slug management. Only the id should be necessary
// to identify a board in the code. // to identify a board in the code.
// XXX If the board title is updated, the slug should also be updated. // XXX If the board title is updated, the slug should also be updated.
@ -149,87 +149,87 @@ Boards.before.insert(function(userId, doc) {
doc.createdAt = new Date(); doc.createdAt = new Date();
doc.archived = false; doc.archived = false;
doc.members = [{ doc.members = [{
userId: userId, userId,
isAdmin: true, isAdmin: true,
isActive: true isActive: true,
}]; }];
doc.stars = 0; doc.stars = 0;
doc.color = Boards.simpleSchema()._schema.color.allowedValues[0]; doc.color = Boards.simpleSchema()._schema.color.allowedValues[0];
// Handle labels // Handle labels
var colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
var defaultLabelsColors = _.clone(colors).splice(0, 6); const defaultLabelsColors = _.clone(colors).splice(0, 6);
doc.labels = _.map(defaultLabelsColors, function(val) { doc.labels = _.map(defaultLabelsColors, (color) => {
return { return {
color,
_id: Random.id(6), _id: Random.id(6),
name: '', name: '',
color: val
}; };
}); });
}); });
Boards.before.update(function(userId, doc, fieldNames, modifier) { Boards.before.update((userId, doc, fieldNames, modifier) => {
modifier.$set = modifier.$set || {}; modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = new Date(); modifier.$set.modifiedAt = new Date();
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
// Let MongoDB ensure that a member is not included twice in the same board // Let MongoDB ensure that a member is not included twice in the same board
Meteor.startup(function() { Meteor.startup(() => {
Boards._collection._ensureIndex({ Boards._collection._ensureIndex({
_id: 1, _id: 1,
'members.userId': 1 'members.userId': 1,
}, { unique: true }); }, { unique: true });
}); });
// Genesis: the first activity of the newly created board // Genesis: the first activity of the newly created board
Boards.after.insert(function(userId, doc) { Boards.after.insert((userId, doc) => {
Activities.insert({ Activities.insert({
userId,
type: 'board', type: 'board',
activityTypeId: doc._id, activityTypeId: doc._id,
activityType: 'createBoard', activityType: 'createBoard',
boardId: doc._id, boardId: doc._id,
userId: userId
}); });
}); });
// If the user remove one label from a board, we cant to remove reference of // If the user remove one label from a board, we cant to remove reference of
// this label in any card of this board. // this label in any card of this board.
Boards.after.update(function(userId, doc, fieldNames, modifier) { Boards.after.update((userId, doc, fieldNames, modifier) => {
if (! _.contains(fieldNames, 'labels') || if (!_.contains(fieldNames, 'labels') ||
! modifier.$pull || !modifier.$pull ||
! modifier.$pull.labels || !modifier.$pull.labels ||
! modifier.$pull.labels._id) !modifier.$pull.labels._id)
return; return;
var removedLabelId = modifier.$pull.labels._id; const removedLabelId = modifier.$pull.labels._id;
Cards.update( Cards.update(
{ boardId: doc._id }, { boardId: doc._id },
{ {
$pull: { $pull: {
labels: removedLabelId labels: removedLabelId,
} },
}, },
{ multi: true } { multi: true }
); );
}); });
// Add a new activity if we add or remove a member to the board // Add a new activity if we add or remove a member to the board
Boards.after.update(function(userId, doc, fieldNames, modifier) { Boards.after.update((userId, doc, fieldNames, modifier) => {
if (! _.contains(fieldNames, 'members')) if (!_.contains(fieldNames, 'members'))
return; return;
var memberId; let memberId;
// Say hello to the new member // Say hello to the new member
if (modifier.$push && modifier.$push.members) { if (modifier.$push && modifier.$push.members) {
memberId = modifier.$push.members.userId; memberId = modifier.$push.members.userId;
Activities.insert({ Activities.insert({
userId,
memberId,
type: 'member', type: 'member',
activityType: 'addBoardMember', activityType: 'addBoardMember',
boardId: doc._id, boardId: doc._id,
userId: userId,
memberId: memberId
}); });
} }
@ -237,11 +237,11 @@ if (Meteor.isServer) {
if (modifier.$pull && modifier.$pull.members) { if (modifier.$pull && modifier.$pull.members) {
memberId = modifier.$pull.members.userId; memberId = modifier.$pull.members.userId;
Activities.insert({ Activities.insert({
userId,
memberId,
type: 'member', type: 'member',
activityType: 'removeBoardMember', activityType: 'removeBoardMember',
boardId: doc._id, boardId: doc._id,
userId: userId,
memberId: memberId
}); });
} }
}); });

View file

@ -6,162 +6,161 @@ CardComments = new Mongo.Collection('card_comments');
// of comments just to display the number of them in the board view. // of comments just to display the number of them in the board view.
Cards.attachSchema(new SimpleSchema({ Cards.attachSchema(new SimpleSchema({
title: { title: {
type: String type: String,
}, },
archived: { archived: {
type: Boolean type: Boolean,
}, },
listId: { listId: {
type: String type: String,
}, },
// The system could work without this `boardId` information (we could deduce // The system could work without this `boardId` information (we could deduce
// the board identifier from the card), but it would make the system more // the board identifier from the card), but it would make the system more
// difficult to manage and less efficient. // difficult to manage and less efficient.
boardId: { boardId: {
type: String type: String,
}, },
coverId: { coverId: {
type: String, type: String,
optional: true optional: true,
}, },
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: true denyUpdate: true,
}, },
dateLastActivity: { dateLastActivity: {
type: Date type: Date,
}, },
description: { description: {
type: String, type: String,
optional: true optional: true,
}, },
labelIds: { labelIds: {
type: [String], type: [String],
optional: true optional: true,
}, },
members: { members: {
type: [String], type: [String],
optional: true optional: true,
}, },
// XXX Should probably be called `authorId`. Is it even needed since we have // XXX Should probably be called `authorId`. Is it even needed since we have
// the `members` field? // the `members` field?
userId: { userId: {
type: String type: String,
}, },
sort: { sort: {
type: Number, type: Number,
decimal: true decimal: true,
} },
})); }));
CardComments.attachSchema(new SimpleSchema({ CardComments.attachSchema(new SimpleSchema({
boardId: { boardId: {
type: String type: String,
}, },
cardId: { cardId: {
type: String type: String,
}, },
// XXX Rename in `content`? `text` is a bit vague... // XXX Rename in `content`? `text` is a bit vague...
text: { text: {
type: String type: String,
}, },
// XXX We probably don't need this information here, since we already have it // XXX We probably don't need this information here, since we already have it
// in the associated comment creation activity // in the associated comment creation activity
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: false denyUpdate: false,
}, },
// XXX Should probably be called `authorId` // XXX Should probably be called `authorId`
userId: { userId: {
type: String type: String,
} },
})); }));
if (Meteor.isServer) { if (Meteor.isServer) {
Cards.allow({ Cards.allow({
insert: function(userId, doc) { insert(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
update: function(userId, doc) { update(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
remove: function(userId, doc) { remove(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
fetch: ['boardId'] fetch: ['boardId'],
}); });
CardComments.allow({ CardComments.allow({
insert: function(userId, doc) { insert(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
update: function(userId, doc) { update(userId, doc) {
return userId === doc.userId; return userId === doc.userId;
}, },
remove: function(userId, doc) { remove(userId, doc) {
return userId === doc.userId; return userId === doc.userId;
}, },
fetch: ['userId', 'boardId'] fetch: ['userId', 'boardId'],
}); });
} }
Cards.helpers({ Cards.helpers({
list: function() { list() {
return Lists.findOne(this.listId); return Lists.findOne(this.listId);
}, },
board: function() { board() {
return Boards.findOne(this.boardId); return Boards.findOne(this.boardId);
}, },
labels: function() { labels() {
var self = this; const boardLabels = this.board().labels;
var boardLabels = self.board().labels; const cardLabels = _.filter(boardLabels, (label) => {
var cardLabels = _.filter(boardLabels, function(label) { return _.contains(this.labelIds, label._id);
return _.contains(self.labelIds, label._id);
}); });
return cardLabels; return cardLabels;
}, },
hasLabel: function(labelId) { hasLabel(labelId) {
return _.contains(this.labelIds, labelId); return _.contains(this.labelIds, labelId);
}, },
user: function() { user() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
}, },
isAssigned: function(memberId) { isAssigned(memberId) {
return _.contains(this.members, memberId); return _.contains(this.members, memberId);
}, },
activities: function() { activities() {
return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }}); return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }});
}, },
comments: function() { comments() {
return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }}); return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }});
}, },
attachments: function() { attachments() {
return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }}); return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }});
}, },
cover: function() { cover() {
return Attachments.findOne(this.coverId); return Attachments.findOne(this.coverId);
}, },
absoluteUrl: function() { absoluteUrl() {
var board = this.board(); const board = this.board();
return FlowRouter.path('card', { return FlowRouter.path('card', {
boardId: board._id, boardId: board._id,
slug: board.slug, slug: board.slug,
cardId: this._id cardId: this._id,
}); });
}, },
rootUrl: function() { rootUrl() {
return Meteor.absoluteUrl(this.absoluteUrl().replace('/', '')); return Meteor.absoluteUrl(this.absoluteUrl().replace('/', ''));
} },
}); });
CardComments.helpers({ CardComments.helpers({
user: function() { user() {
return Users.findOne(this.userId); return Users.findOne(this.userId);
} },
}); });
CardComments.hookOptions.after.update = { fetchPrevious: false }; CardComments.hookOptions.after.update = { fetchPrevious: false };
Cards.before.insert(function(userId, doc) { Cards.before.insert((userId, doc) => {
doc.createdAt = new Date(); doc.createdAt = new Date();
doc.dateLastActivity = new Date(); doc.dateLastActivity = new Date();
@ -169,44 +168,44 @@ Cards.before.insert(function(userId, doc) {
doc.archived = false; doc.archived = false;
// userId native set. // userId native set.
if (! doc.userId) if (!doc.userId)
doc.userId = userId; doc.userId = userId;
}); });
CardComments.before.insert(function(userId, doc) { CardComments.before.insert((userId, doc) => {
doc.createdAt = new Date(); doc.createdAt = new Date();
doc.userId = userId; doc.userId = userId;
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Cards.after.insert(function(userId, doc) { Cards.after.insert((userId, doc) => {
Activities.insert({ Activities.insert({
userId,
activityType: 'createCard', activityType: 'createCard',
boardId: doc.boardId, boardId: doc.boardId,
listId: doc.listId, listId: doc.listId,
cardId: doc._id, cardId: doc._id,
userId: userId
}); });
}); });
// New activity for card (un)archivage // New activity for card (un)archivage
Cards.after.update(function(userId, doc, fieldNames) { Cards.after.update((userId, doc, fieldNames) => {
if (_.contains(fieldNames, 'archived')) { if (_.contains(fieldNames, 'archived')) {
if (doc.archived) { if (doc.archived) {
Activities.insert({ Activities.insert({
userId,
activityType: 'archivedCard', activityType: 'archivedCard',
boardId: doc.boardId, boardId: doc.boardId,
listId: doc.listId, listId: doc.listId,
cardId: doc._id, cardId: doc._id,
userId: userId
}); });
} else { } else {
Activities.insert({ Activities.insert({
userId,
activityType: 'restoredCard', activityType: 'restoredCard',
boardId: doc.boardId, boardId: doc.boardId,
listId: doc.listId, listId: doc.listId,
cardId: doc._id, cardId: doc._id,
userId: userId
}); });
} }
} }
@ -214,34 +213,34 @@ if (Meteor.isServer) {
// New activity for card moves // New activity for card moves
Cards.after.update(function(userId, doc, fieldNames) { Cards.after.update(function(userId, doc, fieldNames) {
var oldListId = this.previous.listId; const oldListId = this.previous.listId;
if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) { if (_.contains(fieldNames, 'listId') && doc.listId !== oldListId) {
Activities.insert({ Activities.insert({
userId,
oldListId,
activityType: 'moveCard', activityType: 'moveCard',
listId: doc.listId, listId: doc.listId,
oldListId: oldListId,
boardId: doc.boardId, boardId: doc.boardId,
cardId: doc._id, cardId: doc._id,
userId: userId
}); });
} }
}); });
// Add a new activity if we add or remove a member to the card // Add a new activity if we add or remove a member to the card
Cards.before.update(function(userId, doc, fieldNames, modifier) { Cards.before.update((userId, doc, fieldNames, modifier) => {
if (! _.contains(fieldNames, 'members')) if (!_.contains(fieldNames, 'members'))
return; return;
var memberId; let memberId;
// Say hello to the new member // Say hello to the new member
if (modifier.$addToSet && modifier.$addToSet.members) { if (modifier.$addToSet && modifier.$addToSet.members) {
memberId = modifier.$addToSet.members; memberId = modifier.$addToSet.members;
if (! _.contains(doc.members, memberId)) { if (!_.contains(doc.members, memberId)) {
Activities.insert({ Activities.insert({
userId,
memberId,
activityType: 'joinMember', activityType: 'joinMember',
boardId: doc.boardId, boardId: doc.boardId,
cardId: doc._id, cardId: doc._id,
userId: userId,
memberId: memberId
}); });
} }
} }
@ -250,34 +249,34 @@ if (Meteor.isServer) {
if (modifier.$pull && modifier.$pull.members) { if (modifier.$pull && modifier.$pull.members) {
memberId = modifier.$pull.members; memberId = modifier.$pull.members;
Activities.insert({ Activities.insert({
userId,
memberId,
activityType: 'unjoinMember', activityType: 'unjoinMember',
boardId: doc.boardId, boardId: doc.boardId,
cardId: doc._id, cardId: doc._id,
userId: userId,
memberId: memberId
}); });
} }
}); });
// Remove all activities associated with a card if we remove the card // Remove all activities associated with a card if we remove the card
Cards.after.remove(function(userId, doc) { Cards.after.remove((userId, doc) => {
Activities.remove({ Activities.remove({
cardId: doc._id cardId: doc._id,
}); });
}); });
CardComments.after.insert(function(userId, doc) { CardComments.after.insert((userId, doc) => {
Activities.insert({ Activities.insert({
userId,
activityType: 'addComment', activityType: 'addComment',
boardId: doc.boardId, boardId: doc.boardId,
cardId: doc.cardId, cardId: doc.cardId,
commentId: doc._id, commentId: doc._id,
userId: userId
}); });
}); });
CardComments.after.remove(function(userId, doc) { CardComments.after.remove((userId, doc) => {
var activity = Activities.findOne({ commentId: doc._id }); const activity = Activities.findOne({ commentId: doc._id });
if (activity) { if (activity) {
Activities.remove(activity._id); Activities.remove(activity._id);
} }

View file

@ -2,92 +2,92 @@ Lists = new Mongo.Collection('lists');
Lists.attachSchema(new SimpleSchema({ Lists.attachSchema(new SimpleSchema({
title: { title: {
type: String type: String,
}, },
archived: { archived: {
type: Boolean type: Boolean,
}, },
boardId: { boardId: {
type: String type: String,
}, },
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: true denyUpdate: true,
}, },
sort: { sort: {
type: Number, type: Number,
decimal: true, decimal: true,
// XXX We should probably provide a default // XXX We should probably provide a default
optional: true optional: true,
}, },
updatedAt: { updatedAt: {
type: Date, type: Date,
denyInsert: true, denyInsert: true,
optional: true optional: true,
} },
})); }));
if (Meteor.isServer) { if (Meteor.isServer) {
Lists.allow({ Lists.allow({
insert: function(userId, doc) { insert(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
update: function(userId, doc) { update(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
remove: function(userId, doc) { remove(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId)); return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
}, },
fetch: ['boardId'] fetch: ['boardId'],
}); });
} }
Lists.helpers({ Lists.helpers({
cards: function() { cards() {
return Cards.find(Filter.mongoSelector({ return Cards.find(Filter.mongoSelector({
listId: this._id, listId: this._id,
archived: false archived: false,
}), { sort: ['sort'] }); }), { sort: ['sort'] });
}, },
board: function() { board() {
return Boards.findOne(this.boardId); return Boards.findOne(this.boardId);
} },
}); });
// HOOKS // HOOKS
Lists.hookOptions.after.update = { fetchPrevious: false }; Lists.hookOptions.after.update = { fetchPrevious: false };
Lists.before.insert(function(userId, doc) { Lists.before.insert((userId, doc) => {
doc.createdAt = new Date(); doc.createdAt = new Date();
doc.archived = false; doc.archived = false;
if (! doc.userId) if (!doc.userId)
doc.userId = userId; doc.userId = userId;
}); });
Lists.before.update(function(userId, doc, fieldNames, modifier) { Lists.before.update((userId, doc, fieldNames, modifier) => {
modifier.$set = modifier.$set || {}; modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = new Date(); modifier.$set.modifiedAt = new Date();
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Lists.after.insert(function(userId, doc) { Lists.after.insert((userId, doc) => {
Activities.insert({ Activities.insert({
userId,
type: 'list', type: 'list',
activityType: 'createList', activityType: 'createList',
boardId: doc.boardId, boardId: doc.boardId,
listId: doc._id, listId: doc._id,
userId: userId
}); });
}); });
Lists.after.update(function(userId, doc) { Lists.after.update((userId, doc) => {
if (doc.archived) { if (doc.archived) {
Activities.insert({ Activities.insert({
userId,
type: 'list', type: 'list',
activityType: 'archivedList', activityType: 'archivedList',
listId: doc._id, listId: doc._id,
boardId: doc.boardId, boardId: doc.boardId,
userId: userId
}); });
} }
}); });

View file

@ -4,16 +4,16 @@ UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
UnsavedEditCollection.attachSchema(new SimpleSchema({ UnsavedEditCollection.attachSchema(new SimpleSchema({
fieldName: { fieldName: {
type: String type: String,
}, },
docId: { docId: {
type: String type: String,
}, },
value: { value: {
type: String type: String,
}, },
userId: { userId: {
type: String type: String,
}, },
})); }));
@ -25,10 +25,10 @@ if (Meteor.isServer) {
insert: isAuthor, insert: isAuthor,
update: isAuthor, update: isAuthor,
remove: isAuthor, remove: isAuthor,
fetch: ['userId'] fetch: ['userId'],
}); });
} }
UnsavedEditCollection.before.insert(function(userId, doc) { UnsavedEditCollection.before.insert((userId, doc) => {
doc.userId = userId; doc.userId = userId;
}); });

View file

@ -2,42 +2,42 @@ Users = Meteor.users;
// 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.
var searchInFields = ['username', 'profile.name']; const searchInFields = ['username', 'profile.name'];
Users.initEasySearch(searchInFields, { Users.initEasySearch(searchInFields, {
use: 'mongo-db', use: 'mongo-db',
returnFields: searchInFields returnFields: searchInFields,
}); });
Users.helpers({ Users.helpers({
boards: function() { boards() {
return Boards.find({ userId: this._id }); return Boards.find({ userId: this._id });
}, },
starredBoards: function() { starredBoards() {
var starredBoardIds = this.profile.starredBoards || []; const starredBoardIds = this.profile.starredBoards || [];
return Boards.find({archived: false, _id: {$in: starredBoardIds}}); return Boards.find({archived: false, _id: {$in: starredBoardIds}});
}, },
hasStarred: function(boardId) { hasStarred(boardId) {
var starredBoardIds = this.profile.starredBoards || []; const starredBoardIds = this.profile.starredBoards || [];
return _.contains(starredBoardIds, boardId); return _.contains(starredBoardIds, boardId);
}, },
isBoardMember: function() { isBoardMember() {
var board = Boards.findOne(Session.get('currentBoard')); const board = Boards.findOne(Session.get('currentBoard'));
return board && _.contains(_.pluck(board.members, 'userId'), this._id) && return board && _.contains(_.pluck(board.members, 'userId'), this._id) &&
_.where(board.members, {userId: this._id})[0].isActive; _.where(board.members, {userId: this._id})[0].isActive;
}, },
isBoardAdmin: function() { isBoardAdmin() {
var board = Boards.findOne(Session.get('currentBoard')); const board = Boards.findOne(Session.get('currentBoard'));
if (this.isBoardMember(board)) if (this.isBoardMember(board))
return _.where(board.members, {userId: this._id})[0].isAdmin; return _.where(board.members, {userId: this._id})[0].isAdmin;
}, },
getInitials: function() { getInitials() {
var profile = this.profile || {}; const profile = this.profile || {};
if (profile.initials) if (profile.initials)
return profile.initials; return profile.initials;
else if (profile.fullname) { else if (profile.fullname) {
return _.reduce(profile.fullname.split(/\s+/), function(memo, word) { return _.reduce(profile.fullname.split(/\s+/), (memo, word) => {
return memo + word[0]; return memo + word[0];
}, '').toUpperCase(); }, '').toUpperCase();
@ -46,43 +46,41 @@ Users.helpers({
} }
}, },
toggleBoardStar: function(boardId) { toggleBoardStar(boardId) {
var queryType = this.hasStarred(boardId) ? '$pull' : '$addToSet'; const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
var query = {}; Meteor.users.update(this._id, {
query[queryType] = { [queryKind]: {
'profile.starredBoards': boardId 'profile.starredBoards': boardId,
}; },
Meteor.users.update(this._id, query); });
} },
}); });
Meteor.methods({ Meteor.methods({
setUsername: function(username) { setUsername(username) {
check(username, String); check(username, String);
var nUsersWithUsername = Users.find({username: username}).count(); const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) { if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken'); throw new Meteor.Error('username-already-taken');
} else { } else {
Users.update(this.userId, {$set: { Users.update(this.userId, {$set: { username }});
username: username
}});
} }
} },
}); });
Users.before.insert(function(userId, doc) { Users.before.insert((userId, doc) => {
doc.profile = doc.profile || {}; doc.profile = doc.profile || {};
if (! doc.username && doc.profile.name) { if (!doc.username && doc.profile.name) {
doc.username = doc.profile.name.toLowerCase().replace(/\s/g, ''); doc.username = doc.profile.name.toLowerCase().replace(/\s/g, '');
} }
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
// Let mongoDB ensure username unicity // Let mongoDB ensure username unicity
Meteor.startup(function() { Meteor.startup(() => {
Users._collection._ensureIndex({ Users._collection._ensureIndex({
username: 1 username: 1,
}, { unique: true }); }, { unique: true });
}); });
@ -94,44 +92,44 @@ if (Meteor.isServer) {
Users.after.update(function(userId, user, fieldNames) { Users.after.update(function(userId, user, fieldNames) {
// The `starredBoards` list is hosted on the `profile` field. If this // The `starredBoards` list is hosted on the `profile` field. If this
// field hasn't been modificated we don't need to run this hook. // field hasn't been modificated we don't need to run this hook.
if (! _.contains(fieldNames, 'profile')) if (!_.contains(fieldNames, 'profile'))
return; return;
// To calculate a diff of board starred ids, we get both the previous // To calculate a diff of board starred ids, we get both the previous
// and the newly board ids list // and the newly board ids list
var getStarredBoardsIds = function(doc) { function getStarredBoardsIds(doc) {
return doc.profile && doc.profile.starredBoards; return doc.profile && doc.profile.starredBoards;
}; }
var oldIds = getStarredBoardsIds(this.previous); const oldIds = getStarredBoardsIds(this.previous);
var newIds = getStarredBoardsIds(user); const newIds = getStarredBoardsIds(user);
// The _.difference(a, b) method returns the values from a that are not in // The _.difference(a, b) method returns the values from a that are not in
// b. We use it to find deleted and newly inserted ids by using it in one // b. We use it to find deleted and newly inserted ids by using it in one
// direction and then in the other. // direction and then in the other.
var incrementBoards = function(boardsIds, inc) { function incrementBoards(boardsIds, inc) {
_.forEach(boardsIds, function(boardId) { _.forEach(boardsIds, (boardId) => {
Boards.update(boardId, {$inc: {stars: inc}}); Boards.update(boardId, {$inc: {stars: inc}});
}); });
}; }
incrementBoards(_.difference(oldIds, newIds), -1); incrementBoards(_.difference(oldIds, newIds), -1);
incrementBoards(_.difference(newIds, oldIds), +1); incrementBoards(_.difference(newIds, oldIds), +1);
}); });
// XXX i18n // XXX i18n
Users.after.insert(function(userId, doc) { Users.after.insert((userId, doc) => {
var ExampleBoard = { const ExampleBoard = {
title: 'Welcome Board', title: 'Welcome Board',
userId: doc._id, userId: doc._id,
permission: 'private' permission: 'private',
}; };
// Insert the Welcome Board // Insert the Welcome Board
Boards.insert(ExampleBoard, function(err, boardId) { Boards.insert(ExampleBoard, (err, boardId) => {
_.forEach(['Basics', 'Advanced'], function(title) { _.forEach(['Basics', 'Advanced'], (title) => {
var list = { const list = {
title: title, title,
boardId: boardId, boardId,
userId: ExampleBoard.userId, userId: ExampleBoard.userId,
// XXX Not certain this is a bug, but we except these fields get // XXX Not certain this is a bug, but we except these fields get
@ -139,7 +137,7 @@ if (Meteor.isServer) {
// hook is not called in this case, we have to dublicate the logic and // hook is not called in this case, we have to dublicate the logic and
// set them here. // set them here.
archived: false, archived: false,
createdAt: new Date() createdAt: new Date(),
}; };
Lists.insert(list); Lists.insert(list);
@ -150,9 +148,7 @@ if (Meteor.isServer) {
// Presence indicator // Presence indicator
if (Meteor.isClient) { if (Meteor.isClient) {
Presence.state = function() { Presence.state = () => {
return { return { currentBoardId: Session.get('currentBoard') };
currentBoardId: Session.get('currentBoard')
};
}; };
} }

View file

@ -1,12 +1,12 @@
// Sandstorm context is detected using the METEOR_SETTINGS environment variable // Sandstorm context is detected using the METEOR_SETTINGS environment variable
// in the package definition. // in the package definition.
var isSandstorm = Meteor.settings && Meteor.settings.public && const isSandstorm = Meteor.settings && Meteor.settings.public &&
Meteor.settings.public.sandstorm; Meteor.settings.public.sandstorm;
// In sandstorm we only have one board per sandstorm instance. Since we want to // In sandstorm we only have one board per sandstorm instance. Since we want to
// keep most of our code unchanged, we simply hard-code a board `_id` and // keep most of our code unchanged, we simply hard-code a board `_id` and
// redirect the user to this particular board. // redirect the user to this particular board.
var sandstormBoard = { const sandstormBoard = {
_id: 'sandstorm', _id: 'sandstorm',
// XXX Should be shared with the grain instance name. // XXX Should be shared with the grain instance name.
@ -16,15 +16,15 @@ var sandstormBoard = {
// Board access security is handled by sandstorm, so in our point of view we // Board access security is handled by sandstorm, so in our point of view we
// can alway assume that the board is public (unauthorized users wont be able // can alway assume that the board is public (unauthorized users wont be able
// to access it anyway). // to access it anyway).
permission: 'public' permission: 'public',
}; };
// The list of permissions a user have is provided by sandstorm accounts // The list of permissions a user have is provided by sandstorm accounts
// package. // package.
var userHasPermission = function(user, permission) { function userHasPermission(user, permission) {
var userPermissions = user.services.sandstorm.permissions; const userPermissions = user.services.sandstorm.permissions;
return userPermissions.indexOf(permission) > -1; return userPermissions.indexOf(permission) > -1;
}; }
if (isSandstorm && Meteor.isServer) { if (isSandstorm && Meteor.isServer) {
// Redirect the user to the hard-coded board. On the first launch the user // Redirect the user to the hard-coded board. On the first launch the user
@ -35,15 +35,15 @@ if (isSandstorm && Meteor.isServer) {
// browser, a server-side redirection solves both of these issues. // browser, a server-side redirection solves both of these issues.
// //
// XXX Maybe sandstorm manifest could provide some kind of "home url"? // XXX Maybe sandstorm manifest could provide some kind of "home url"?
Picker.route('/', function(params, request, response) { Picker.route('/', (params, request, response) => {
var base = request.headers['x-sandstorm-base-path']; const base = request.headers['x-sandstorm-base-path'];
// XXX If this routing scheme changes, this will break. We should generation // XXX If this routing scheme changes, this will break. We should generation
// the location url using the router, but at the time of writting, the // the location url using the router, but at the time of writting, the
// router is only accessible on the client. // router is only accessible on the client.
var path = '/boards/' + sandstormBoard._id + '/' + sandstormBoard.slug; const path = `/boards/${sandstormBoard._id}/${sandstormBoard.slug}`;
response.writeHead(301, { response.writeHead(301, {
Location: base + path Location: base + path,
}); });
response.end(); response.end();
}); });
@ -53,8 +53,8 @@ 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.
Users.after.insert(function(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});
Boards.update(sandstormBoard._id, { Boards.update(sandstormBoard._id, {
$set: { $set: {
@ -62,14 +62,14 @@ if (isSandstorm && Meteor.isServer) {
'members.0': { 'members.0': {
userId: doc._id, userId: doc._id,
isActive: true, isActive: true,
isAdmin: true isAdmin: true,
} },
} },
}); });
Activities.update( Activities.update(
{ activityTypeId: sandstormBoard._id }, { { activityTypeId: sandstormBoard._id },
$set: { userId: doc._id } { $set: { userId: doc._id }}
}); );
} }
// If the hard-coded board already exists and we are inserting a new user, // If the hard-coded board already exists and we are inserting a new user,
@ -77,15 +77,15 @@ if (isSandstorm && Meteor.isServer) {
else if (userHasPermission(doc, 'participate')) { else if (userHasPermission(doc, 'participate')) {
Boards.update({ Boards.update({
_id: sandstormBoard._id, _id: sandstormBoard._id,
permission: 'public' permission: 'public',
}, { }, {
$push: { $push: {
members: { members: {
userId: doc._id, userId: doc._id,
isActive: true, isActive: true,
isAdmin: userHasPermission(doc, 'configure') isAdmin: userHasPermission(doc, 'configure'),
} },
} },
}); });
} }
}); });
@ -96,10 +96,10 @@ if (isSandstorm && Meteor.isClient) {
// session has a different URL whereas Meteor computes absoluteUrl based on // session has a different URL whereas Meteor computes absoluteUrl based on
// the ROOT_URL environment variable. So we overwrite this function on a // the ROOT_URL environment variable. So we overwrite this function on a
// sandstorm client to return relative paths instead of absolutes. // sandstorm client to return relative paths instead of absolutes.
var _absoluteUrl = Meteor.absoluteUrl; const _absoluteUrl = Meteor.absoluteUrl;
var _defaultOptions = Meteor.absoluteUrl.defaultOptions; const _defaultOptions = Meteor.absoluteUrl.defaultOptions;
Meteor.absoluteUrl = function(path, options) { Meteor.absoluteUrl = (path, options) => {
var 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}/, '');
}; };
Meteor.absoluteUrl.defaultOptions = _defaultOptions; Meteor.absoluteUrl.defaultOptions = _defaultOptions;
@ -108,6 +108,4 @@ if (isSandstorm && Meteor.isClient) {
// We use this blaze helper in the UI to hide some templates that does not make // We use this blaze helper in the UI to hide some templates that does not make
// sense in the context of sandstorm, like board staring, board archiving, user // sense in the context of sandstorm, like board staring, board archiving, user
// name edition, etc. // name edition, etc.
Blaze.registerHelper('isSandstorm', function() { Blaze.registerHelper('isSandstorm', () => isSandstorm);
return isSandstorm;
});

View file

@ -1,5 +1,5 @@
allowIsBoardAdmin = function(userId, board) { allowIsBoardAdmin = function(userId, board) {
var admins = _.pluck(_.where(board.members, {isAdmin: true}), 'userId'); const admins = _.pluck(_.where(board.members, {isAdmin: true}), 'userId');
return _.contains(admins, userId); return _.contains(admins, userId);
}; };

View file

@ -11,35 +11,35 @@
// //
// To prevent this bug we always have to disable the schema validation and // To prevent this bug we always have to disable the schema validation and
// argument transformations. We generally use the shorthandlers defined below. // argument transformations. We generally use the shorthandlers defined below.
var noValidate = { const noValidate = {
validate: false, validate: false,
filter: false, filter: false,
autoConvert: false, autoConvert: false,
removeEmptyStrings: false, removeEmptyStrings: false,
getAutoValues: false getAutoValues: false,
}; };
var noValidateMulti = _.extend(noValidate, { multi: true }); const noValidateMulti = { ...noValidate, multi: true };
Migrations.add('board-background-color', function() { Migrations.add('board-background-color', () => {
var defaultColor = '#16A085'; const defaultColor = '#16A085';
Boards.update({ Boards.update({
background: { background: {
$exists: false $exists: false,
} },
}, { }, {
$set: { $set: {
background: { background: {
type: 'color', type: 'color',
color: defaultColor color: defaultColor,
} },
} },
}, noValidateMulti); }, noValidateMulti);
}); });
Migrations.add('lowercase-board-permission', function() { Migrations.add('lowercase-board-permission', () => {
_.forEach(['Public', 'Private'], function(permission) { _.forEach(['Public', 'Private'], (permission) => {
Boards.update( Boards.update(
{ permission: permission }, { permission },
{ $set: { permission: permission.toLowerCase() } }, { $set: { permission: permission.toLowerCase() } },
noValidateMulti noValidateMulti
); );
@ -47,23 +47,23 @@ Migrations.add('lowercase-board-permission', function() {
}); });
// Security migration: see https://github.com/wekan/wekan/issues/99 // Security migration: see https://github.com/wekan/wekan/issues/99
Migrations.add('change-attachments-type-for-non-images', function() { Migrations.add('change-attachments-type-for-non-images', () => {
var newTypeForNonImage = 'application/octet-stream'; const newTypeForNonImage = 'application/octet-stream';
Attachments.find().forEach(function(file) { Attachments.find().forEach((file) => {
if (! file.isImage()) { if (!file.isImage()) {
Attachments.update(file._id, { Attachments.update(file._id, {
$set: { $set: {
'original.type': newTypeForNonImage, 'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage 'copies.attachments.type': newTypeForNonImage,
} },
}, noValidate); }, noValidate);
} }
}); });
}); });
Migrations.add('card-covers', function() { Migrations.add('card-covers', () => {
Cards.find().forEach(function(card) { Cards.find().forEach((card) => {
var cover = Attachments.findOne({ cardId: card._id, cover: true }); const cover = Attachments.findOne({ cardId: card._id, cover: true });
if (cover) { if (cover) {
Cards.update(card._id, {$set: {coverId: cover._id}}, noValidate); Cards.update(card._id, {$set: {coverId: cover._id}}, noValidate);
} }
@ -71,54 +71,54 @@ Migrations.add('card-covers', function() {
Attachments.update({}, {$unset: {cover: ''}}, noValidateMulti); Attachments.update({}, {$unset: {cover: ''}}, noValidateMulti);
}); });
Migrations.add('use-css-class-for-boards-colors', function() { Migrations.add('use-css-class-for-boards-colors', () => {
var associationTable = { const associationTable = {
'#27AE60': 'nephritis', '#27AE60': 'nephritis',
'#C0392B': 'pomegranate', '#C0392B': 'pomegranate',
'#2980B9': 'belize', '#2980B9': 'belize',
'#8E44AD': 'wisteria', '#8E44AD': 'wisteria',
'#2C3E50': 'midnight', '#2C3E50': 'midnight',
'#E67E22': 'pumpkin' '#E67E22': 'pumpkin',
}; };
Boards.find().forEach(function(board) { Boards.find().forEach((board) => {
var oldBoardColor = board.background.color; const oldBoardColor = board.background.color;
var newBoardColor = associationTable[oldBoardColor]; const newBoardColor = associationTable[oldBoardColor];
Boards.update(board._id, { Boards.update(board._id, {
$set: { color: newBoardColor }, $set: { color: newBoardColor },
$unset: { background: '' } $unset: { background: '' },
}, noValidate); }, noValidate);
}); });
}); });
Migrations.add('denormalize-star-number-per-board', function() { Migrations.add('denormalize-star-number-per-board', () => {
Boards.find().forEach(function(board) { Boards.find().forEach((board) => {
var nStars = Users.find({'profile.starredBoards': board._id}).count(); const nStars = Users.find({'profile.starredBoards': board._id}).count();
Boards.update(board._id, {$set: {stars: nStars}}, noValidate); Boards.update(board._id, {$set: {stars: nStars}}, noValidate);
}); });
}); });
// We want to keep a trace of former members so we can efficiently publish their // We want to keep a trace of former members so we can efficiently publish their
// infos in the general board publication. // infos in the general board publication.
Migrations.add('add-member-isactive-field', function() { Migrations.add('add-member-isactive-field', () => {
Boards.find({}, {fields: {members: 1}}).forEach(function(board) { Boards.find({}, {fields: {members: 1}}).forEach((board) => {
var allUsersWithSomeActivity = _.chain( const allUsersWithSomeActivity = _.chain(
Activities.find({boardId: board._id}, {fields:{userId:1}}).fetch()) Activities.find({ boardId: board._id }, { fields:{ userId:1 }}).fetch())
.pluck('userId') .pluck('userId')
.uniq() .uniq()
.value(); .value();
var currentUsers = _.pluck(board.members, 'userId'); const currentUsers = _.pluck(board.members, 'userId');
var formerUsers = _.difference(allUsersWithSomeActivity, currentUsers); const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
var newMemberSet = []; const newMemberSet = [];
_.forEach(board.members, function(member) { _.forEach(board.members, (member) => {
member.isActive = true; member.isActive = true;
newMemberSet.push(member); newMemberSet.push(member);
}); });
_.forEach(formerUsers, function(userId) { _.forEach(formerUsers, (userId) => {
newMemberSet.push({ newMemberSet.push({
userId: userId, userId,
isAdmin: false, isAdmin: false,
isActive: false isActive: false,
}); });
}); });
Boards.update(board._id, {$set: {members: newMemberSet}}, noValidate); Boards.update(board._id, {$set: {members: newMemberSet}}, noValidate);

View file

@ -1,24 +1,19 @@
// We use activities fields at three different places: // We use activities fields at two different places:
// 1. The home page that contains // 1. The board sidebar
// 2. The board // 2. The card activity tab
// 3. // We use this publication to paginate for these two publications.
// We use publish paginate for these three publications.
Meteor.publish('activities', function(mode, id, limit) { Meteor.publish('activities', (kind, id, limit) => {
check(mode, Match.Where(function(x) { check(kind, Match.Where((x) => {
return ['board', 'card'].indexOf(x) !== -1; return ['board', 'card'].indexOf(x) !== -1;
})); }));
check(id, String); check(id, String);
check(limit, Number); check(limit, Number);
var selector = {}; return Activities.find({
if (mode === 'board') [`${kind}Id`]: id,
selector.boardId = id; }, {
else if (mode === 'card') limit,
selector.cardId = id;
return Activities.find(selector, {
sort: {createdAt: -1}, sort: {createdAt: -1},
limit: limit
}); });
}); });

View file

@ -5,20 +5,20 @@
Meteor.publish('boards', function() { Meteor.publish('boards', function() {
// Ensure that the user is connected. If it is not, we need to return an empty // Ensure that the user is connected. If it is not, we need to return an empty
// array to tell the client to remove the previously published docs. // array to tell the client to remove the previously published docs.
if (! Match.test(this.userId, String)) if (!Match.test(this.userId, String))
return []; return [];
// Defensive programming to verify that starredBoards has the expected // Defensive programming to verify that starredBoards has the expected
// format -- since the field is in the `profile` a user can modify it. // format -- since the field is in the `profile` a user can modify it.
var starredBoards = Users.findOne(this.userId).profile.starredBoards || []; const starredBoards = Users.findOne(this.userId).profile.starredBoards || [];
check(starredBoards, [String]); check(starredBoards, [String]);
return Boards.find({ return Boards.find({
archived: false, archived: false,
$or: [ $or: [
{ 'members.userId': this.userId }, { 'members.userId': this.userId },
{ _id: { $in: starredBoards } } { _id: { $in: starredBoards } },
] ],
}, { }, {
fields: { fields: {
_id: 1, _id: 1,
@ -27,13 +27,13 @@ Meteor.publish('boards', function() {
title: 1, title: 1,
color: 1, color: 1,
members: 1, members: 1,
permission: 1 permission: 1,
} },
}); });
}); });
Meteor.publish('archivedBoards', function() { Meteor.publish('archivedBoards', function() {
if (! Match.test(this.userId, String)) if (!Match.test(this.userId, String))
return []; return [];
return Boards.find({ return Boards.find({
@ -41,23 +41,23 @@ Meteor.publish('archivedBoards', function() {
members: { members: {
$elemMatch: { $elemMatch: {
userId: this.userId, userId: this.userId,
isAdmin: true isAdmin: true,
} },
} },
}, { }, {
fields: { fields: {
_id: 1, _id: 1,
archived: 1, archived: 1,
slug: 1, slug: 1,
title: 1 title: 1,
} },
}) });
}); });
Meteor.publishComposite('board', function(boardId) { Meteor.publishComposite('board', function(boardId) {
check(boardId, String); check(boardId, String);
return { return {
find: function() { find() {
return Boards.find({ return Boards.find({
_id: boardId, _id: boardId,
archived: false, archived: false,
@ -65,18 +65,18 @@ Meteor.publishComposite('board', function(boardId) {
// it. // it.
$or: [ $or: [
{ permission: 'public' }, { permission: 'public' },
{ 'members.userId': this.userId } { 'members.userId': this.userId },
] ],
}, { limit: 1 }); }, { limit: 1 });
}, },
children: [ children: [
// Lists // Lists
{ {
find: function(board) { find(board) {
return Lists.find({ return Lists.find({
boardId: board._id boardId: board._id,
}); });
} },
}, },
// Cards and cards comments // Cards and cards comments
@ -103,48 +103,48 @@ Meteor.publishComposite('board', function(boardId) {
// And in the meantime our code below works pretty well -- it's not even a // And in the meantime our code below works pretty well -- it's not even a
// hack! // hack!
{ {
find: function(board) { find(board) {
return Cards.find({ return Cards.find({
boardId: board._id boardId: board._id,
}); });
}, },
children: [ children: [
// comments // comments
{ {
find: function(card) { find(card) {
return CardComments.find({ return CardComments.find({
cardId: card._id cardId: card._id,
}); });
} },
}, },
// Attachments // Attachments
{ {
find: function(card) { find(card) {
return Attachments.find({ return Attachments.find({
cardId: card._id cardId: card._id,
}); });
} },
} },
] ],
}, },
// Board members. This publication also includes former board members that // Board members. This publication also includes former board members that
// aren't members anymore but may have some activities attached to them in // aren't members anymore but may have some activities attached to them in
// the history. // the history.
{ {
find: function(board) { find(board) {
return Users.find({ return Users.find({
_id: { $in: _.pluck(board.members, 'userId') } _id: { $in: _.pluck(board.members, 'userId') },
}); });
}, },
// Presence indicators // Presence indicators
children: [{ children: [{
find: function(user) { find(user) {
return Presences.find({userId: user._id}); return Presences.find({userId: user._id});
} },
}] }],
} },
] ],
}; };
}); });

View file

@ -1,4 +1,4 @@
Meteor.publish('card', function(cardId) { Meteor.publish('card', (cardId) => {
check(cardId, String); check(cardId, String);
return Cards.find({ _id: cardId }); return Cards.find({ _id: cardId });
}); });

View file

@ -1,5 +1,5 @@
Meteor.publish('unsaved-edits', function() { Meteor.publish('unsaved-edits', function() {
return UnsavedEditCollection.find({ return UnsavedEditCollection.find({
userId: this.userId userId: this.userId,
}); });
}); });