mirror of
https://github.com/wekan/wekan.git
synced 2025-04-20 12:07:11 -04:00
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:
parent
039cfe7edf
commit
b3851817ec
60 changed files with 1604 additions and 1692 deletions
157
.eslintrc
Normal file
157
.eslintrc
Normal 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
73
.jscsrc
|
@ -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
|
|
||||||
}
|
|
91
.jshintrc
91
.jshintrc
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// });
|
// });
|
||||||
|
|
||||||
BlazeComponent.extendComponent({
|
BlazeComponent.extendComponent({
|
||||||
template: function() {
|
template() {
|
||||||
return 'minicard';
|
return 'minicard';
|
||||||
}
|
},
|
||||||
}).register('minicard');
|
}).register('minicard');
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: '@', 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);
|
|
||||||
}));
|
|
|
@ -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();
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}];
|
}];
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
},
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: '@', 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);
|
||||||
|
}));
|
||||||
|
|
|
@ -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 });
|
||||||
});
|
|
||||||
|
|
|
@ -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));
|
||||||
}]
|
}],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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',
|
||||||
}]
|
}],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const closedValue = null
|
const closedValue = null;
|
||||||
|
|
||||||
window.Modal = new class {
|
window.Modal = new class {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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')
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
58
sandstorm.js
58
sandstorm.js
|
@ -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 won’t be able
|
// can alway assume that the board is public (unauthorized users won’t 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;
|
|
||||||
});
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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});
|
||||||
}
|
},
|
||||||
}]
|
}],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue