Merge branch 'justinr1234-created-modified' into meteor-1.8

This commit is contained in:
Lauri Ojansivu 2019-06-27 15:27:14 -04:00
commit a0a482aa8e
28 changed files with 3403 additions and 2175 deletions

View file

@ -8,3 +8,20 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
[*.{js,html}]
charset = utf-8
end_of_line = lf
indent_brace_style = 1TBS
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
quote_type = auto
spaces_around_operators = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
packages/*

View file

@ -1,9 +1,15 @@
{ {
"extends": "eslint:recommended", "extends": [
"eslint:recommended",
"plugin:meteor/recommended",
"prettier",
"prettier/standard"
],
"env": { "env": {
"es6": true, "es6": true,
"node": true, "node": true,
"browser": true "browser": true,
"meteor": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2017, "ecmaVersion": 2017,
@ -28,11 +34,12 @@
"no-unneeded-ternary": 2, "no-unneeded-ternary": 2,
"radix": 2, "radix": 2,
"semi": [2, "always"], "semi": [2, "always"],
"camelcase": [2, {"properties": "never"}], "camelcase": [2, { "properties": "never" }],
"comma-spacing": 2, "comma-spacing": 2,
"comma-style": 2, "comma-style": 2,
"eol-last": 2, "eol-last": 2,
"linebreak-style": [2, "unix"], "linebreak-style": [2, "unix"],
"meteor/audit-argument-checks": 0,
"new-parens": 2, "new-parens": 2,
"no-lonely-if": 2, "no-lonely-if": 2,
"no-multiple-empty-lines": 2, "no-multiple-empty-lines": 2,
@ -52,8 +59,26 @@
"prefer-const": 2, "prefer-const": 2,
"prefer-spread": 2, "prefer-spread": 2,
"prefer-template": 2, "prefer-template": 2,
"no-unused-vars" : "warn" "no-unused-vars": "warn",
"prettier/prettier": [
"error",
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "all"
}
]
}, },
"settings": {
"import/resolver": {
"meteor": {
"extensions": [".js", ".jsx"]
}
}
},
"plugins": ["prettier", "meteor"],
"globals": { "globals": {
"Meteor": false, "Meteor": false,
"Session": false, "Session": false,
@ -100,7 +125,7 @@
"Attachments": true, "Attachments": true,
"Boards": true, "Boards": true,
"CardComments": true, "CardComments": true,
"DatePicker" : true, "DatePicker": true,
"Cards": true, "Cards": true,
"CustomFields": true, "CustomFields": true,
"Lists": true, "Lists": true,

7
.prettierignore Normal file
View file

@ -0,0 +1,7 @@
packages/
node_modules/
.build/
.meteor/
.vscode/
.tx/
.github/

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}

View file

@ -1,6 +1,7 @@
AccountSettings = new Mongo.Collection('accountSettings'); AccountSettings = new Mongo.Collection('accountSettings');
AccountSettings.attachSchema(new SimpleSchema({ AccountSettings.attachSchema(
new SimpleSchema({
_id: { _id: {
type: String, type: String,
}, },
@ -12,7 +13,32 @@ AccountSettings.attachSchema(new SimpleSchema({
type: Number, type: Number,
decimal: true, decimal: true,
}, },
})); createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
AccountSettings.allow({ AccountSettings.allow({
update(userId) { update(userId) {
@ -21,19 +47,33 @@ AccountSettings.allow({
}, },
}); });
AccountSettings.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, { AccountSettings._collection._ensureIndex({ modifiedAt: -1 });
AccountSettings.upsert(
{ _id: 'accounts-allowEmailChange' },
{
$setOnInsert: { $setOnInsert: {
booleanValue: false, booleanValue: false,
sort: 0, sort: 0,
}, },
}); }
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, { );
AccountSettings.upsert(
{ _id: 'accounts-allowUserNameChange' },
{
$setOnInsert: { $setOnInsert: {
booleanValue: false, booleanValue: false,
sort: 1, sort: 1,
}, },
}); }
);
}); });
} }
export default AccountSettings;

View file

@ -1,3 +1,5 @@
import { Meteor } from 'meteor/meteor';
Actions = new Mongo.Collection('actions'); Actions = new Mongo.Collection('actions');
Actions.allow({ Actions.allow({
@ -17,3 +19,16 @@ Actions.helpers({
return this.desc; return this.desc;
}, },
}); });
Actions.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Actions._collection._ensureIndex({ modifiedAt: -1 });
});
}
export default Actions;

View file

@ -69,7 +69,11 @@ Activities.before.insert((userId, doc) => {
Activities.after.insert((userId, doc) => { Activities.after.insert((userId, doc) => {
const activity = Activities._transform(doc); const activity = Activities._transform(doc);
RulesHelper.executeRules(activity); RulesHelper.executeRules(activity);
});
Activities.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
@ -78,11 +82,21 @@ if (Meteor.isServer) {
// are largely used in the App. See #524. // are largely used in the App. See #524.
Meteor.startup(() => { Meteor.startup(() => {
Activities._collection._ensureIndex({ createdAt: -1 }); Activities._collection._ensureIndex({ createdAt: -1 });
Activities._collection._ensureIndex({ modifiedAt: -1 });
Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 }); Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 }); Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ commentId: 1 }, { partialFilterExpression: { commentId: { $exists: true } } }); Activities._collection._ensureIndex(
Activities._collection._ensureIndex({ attachmentId: 1 }, { partialFilterExpression: { attachmentId: { $exists: true } } }); { commentId: 1 },
Activities._collection._ensureIndex({ customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } }); { partialFilterExpression: { commentId: { $exists: true } } }
);
Activities._collection._ensureIndex(
{ attachmentId: 1 },
{ partialFilterExpression: { attachmentId: { $exists: true } } }
);
Activities._collection._ensureIndex(
{ customFieldId: 1 },
{ partialFilterExpression: { customFieldId: { $exists: true } } }
);
// Label activity did not work yet, unable to edit labels when tried this. // Label activity did not work yet, unable to edit labels when tried this.
//Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 }); //Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 });
//Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } }); //Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
@ -189,18 +203,35 @@ if (Meteor.isServer) {
// params.labelId = activity.labelId; // params.labelId = activity.labelId;
//} //}
if (board) { if (board) {
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId'); const watchingUsers = _.pluck(
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId'); _.where(board.watchers, { level: 'watching' }),
watchers = _.union(watchers, watchingUsers, _.intersection(participants, trackingUsers)); 'userId'
);
const trackingUsers = _.pluck(
_.where(board.watchers, { level: 'tracking' }),
'userId'
);
watchers = _.union(
watchers,
watchingUsers,
_.intersection(participants, trackingUsers)
);
} }
Notifications.getUsers(watchers).forEach((user) => { Notifications.getUsers(watchers).forEach((user) => {
Notifications.notify(user, title, description, params); Notifications.notify(user, title, description, params);
}); });
const integrations = Integrations.find({ boardId: board._id, type: 'outgoing-webhooks', enabled: true, activities: { '$in': [description, 'all'] } }).fetch(); const integrations = Integrations.find({
boardId: board._id,
type: 'outgoing-webhooks',
enabled: true,
activities: { $in: [description, 'all'] },
}).fetch();
if (integrations.length > 0) { if (integrations.length > 0) {
Meteor.call('outgoingWebhooks', integrations, description, params); Meteor.call('outgoingWebhooks', integrations, description, params);
} }
}); });
} }
export default Activities;

View file

@ -1,6 +1,7 @@
Announcements = new Mongo.Collection('announcements'); Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({ Announcements.attachSchema(
new SimpleSchema({
enabled: { enabled: {
type: Boolean, type: Boolean,
defaultValue: false, defaultValue: false,
@ -17,7 +18,32 @@ Announcements.attachSchema(new SimpleSchema({
type: Number, type: Number,
decimal: true, decimal: true,
}, },
})); createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Announcements.allow({ Announcements.allow({
update(userId) { update(userId) {
@ -26,11 +52,19 @@ Announcements.allow({
}, },
}); });
Announcements.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Announcements._collection._ensureIndex({ modifiedAt: -1 });
const announcements = Announcements.findOne({}); const announcements = Announcements.findOne({});
if(!announcements){ if (!announcements) {
Announcements.insert({enabled: false, sort: 0}); Announcements.insert({ enabled: false, sort: 0 });
} }
}); });
} }
export default Announcements;

View file

@ -1,6 +1,5 @@
Attachments = new FS.Collection('attachments', { Attachments = new FS.Collection('attachments', {
stores: [ stores: [
// XXX Add a new store for cover thumbnails so we don't load big images in // XXX Add a new store for cover thumbnails so we don't load big images in
// the general board view // the general board view
new FS.Store.GridFS('attachments', { new FS.Store.GridFS('attachments', {
@ -25,7 +24,6 @@ Attachments = new FS.Collection('attachments', {
], ],
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Attachments.files._ensureIndex({ cardId: 1 }); Attachments.files._ensureIndex({ cardId: 1 });
@ -78,13 +76,16 @@ if (Meteor.isServer) {
} else { } else {
// Don't add activity about adding the attachment as the activity // Don't add activity about adding the attachment as the activity
// be imported and delete source field // be imported and delete source field
Attachments.update({ Attachments.update(
{
_id: doc._id, _id: doc._id,
}, { },
{
$unset: { $unset: {
source: '', source: '',
}, },
}); }
);
} }
}); });
@ -107,3 +108,5 @@ if (Meteor.isServer) {
}); });
}); });
} }
export default Attachments;

View file

@ -1,7 +1,5 @@
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: {
@ -18,10 +16,14 @@ Avatars.allow({
insert: isOwner, insert: isOwner,
update: isOwner, update: isOwner,
remove: isOwner, remove: isOwner,
download() { return true; }, download() {
return true;
},
fetch: ['userId'], fetch: ['userId'],
}); });
Avatars.files.before.insert((userId, doc) => { Avatars.files.before.insert((userId, doc) => {
doc.userId = userId; doc.userId = userId;
}); });
export default Avatars;

View file

@ -3,7 +3,8 @@ Boards = new Mongo.Collection('boards');
/** /**
* This is a Board. * This is a Board.
*/ */
Boards.attachSchema(new SimpleSchema({ Boards.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* The title of the board * The title of the board
@ -15,7 +16,8 @@ Boards.attachSchema(new SimpleSchema({
* The title slugified. * The title slugified.
*/ */
type: String, type: String,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
// 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.
@ -37,7 +39,8 @@ Boards.attachSchema(new SimpleSchema({
* Is the board archived? * Is the board archived?
*/ */
type: Boolean, type: Boolean,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return false; return false;
} }
@ -48,7 +51,8 @@ Boards.attachSchema(new SimpleSchema({
* Creation time of the board * Creation time of the board
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -63,8 +67,9 @@ Boards.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
if (this.isUpdate) { autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date(); return new Date();
} else { } else {
this.unset(); this.unset();
@ -77,21 +82,24 @@ Boards.attachSchema(new SimpleSchema({
* How many stars the board has * How many stars the board has
*/ */
type: Number, type: Number,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return 0; return 0;
} }
}, },
}, },
// De-normalized label system // De-normalized label system
'labels': { labels: {
/** /**
* List of labels attached to a board * List of labels attached to a board
*/ */
type: [Object], type: [Object],
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues; const colors = Boards.simpleSchema()._schema['labels.$.color']
.allowedValues;
const defaultLabelsColors = _.clone(colors).splice(0, 6); const defaultLabelsColors = _.clone(colors).splice(0, 6);
return defaultLabelsColors.map((color) => ({ return defaultLabelsColors.map((color) => ({
color, color,
@ -131,30 +139,52 @@ Boards.attachSchema(new SimpleSchema({
*/ */
type: String, type: String,
allowedValues: [ allowedValues: [
'green', 'yellow', 'orange', 'red', 'purple', 'green',
'blue', 'sky', 'lime', 'pink', 'black', 'yellow',
'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen', 'orange',
'slateblue', 'magenta', 'gold', 'navy', 'gray', 'red',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', 'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'silver',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
], ],
}, },
// 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': { members: {
/** /**
* List of members of a board * List of members of a board
*/ */
type: [Object], type: [Object],
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return [{ return [
{
userId: this.userId, userId: this.userId,
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
isNoComments: false, isNoComments: false,
isCommentOnly: false, isCommentOnly: false,
}]; },
];
} }
}, },
}, },
@ -210,7 +240,8 @@ Boards.attachSchema(new SimpleSchema({
'wisteria', 'wisteria',
'midnight', 'midnight',
], ],
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return Boards.simpleSchema()._schema.color.allowedValues[0]; return Boards.simpleSchema()._schema.color.allowedValues[0];
} }
@ -311,8 +342,8 @@ Boards.attachSchema(new SimpleSchema({
type: String, type: String,
defaultValue: 'board', defaultValue: 'board',
}, },
})); })
);
Boards.helpers({ Boards.helpers({
copy() { copy() {
@ -350,7 +381,9 @@ Boards.helpers({
*/ */
isActiveMember(userId) { isActiveMember(userId) {
if (userId) { if (userId) {
return this.members.find((member) => (member.userId === userId && member.isActive)); return this.members.find(
(member) => member.userId === userId && member.isActive
);
} else { } else {
return false; return false;
} }
@ -361,11 +394,17 @@ Boards.helpers({
}, },
cards() { cards() {
return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } }); return Cards.find(
{ boardId: this._id, archived: false },
{ sort: { title: 1 } }
);
}, },
lists() { lists() {
return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); return Lists.find(
{ boardId: this._id, archived: false },
{ sort: { sort: 1 } }
);
}, },
nullSortLists() { nullSortLists() {
@ -377,18 +416,24 @@ Boards.helpers({
}, },
swimlanes() { swimlanes() {
return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); return Swimlanes.find(
{ boardId: this._id, archived: false },
{ sort: { sort: 1 } }
);
}, },
nextSwimlane(swimlane) { nextSwimlane(swimlane) {
return Swimlanes.findOne({ return Swimlanes.findOne(
{
boardId: this._id, boardId: this._id,
archived: false, archived: false,
sort: { $gte: swimlane.sort }, sort: { $gte: swimlane.sort },
_id: { $ne: swimlane._id }, _id: { $ne: swimlane._id },
}, { },
{
sort: { sort: 1 }, sort: { sort: 1 },
}); }
);
}, },
nullSortSwimlanes() { nullSortSwimlanes() {
@ -399,13 +444,21 @@ Boards.helpers({
}); });
}, },
hasOvertimeCards(){ hasOvertimeCards() {
const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); const card = Cards.findOne({
isOvertime: true,
boardId: this._id,
archived: false,
});
return card !== undefined; return card !== undefined;
}, },
hasSpentTimeCards(){ hasSpentTimeCards() {
const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} ); const card = Cards.findOne({
spentTime: { $gt: 0 },
boardId: this._id,
archived: false,
});
return card !== undefined; return card !== undefined;
}, },
@ -429,7 +482,7 @@ Boards.helpers({
return _.findWhere(this.labels, { name, color }); return _.findWhere(this.labels, { name, color });
}, },
getLabelById(labelId){ getLabelById(labelId) {
return _.findWhere(this.labels, { _id: labelId }); return _.findWhere(this.labels, { _id: labelId });
}, },
@ -446,15 +499,29 @@ Boards.helpers({
}, },
hasAdmin(memberId) { hasAdmin(memberId) {
return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true }); return !!_.findWhere(this.members, {
userId: memberId,
isActive: true,
isAdmin: true,
});
}, },
hasNoComments(memberId) { hasNoComments(memberId) {
return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true }); return !!_.findWhere(this.members, {
userId: memberId,
isActive: true,
isAdmin: false,
isNoComments: true,
});
}, },
hasCommentOnly(memberId) { hasCommentOnly(memberId) {
return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true }); return !!_.findWhere(this.members, {
userId: memberId,
isActive: true,
isAdmin: false,
isCommentOnly: true,
});
}, },
absoluteUrl() { absoluteUrl() {
@ -466,7 +533,10 @@ Boards.helpers({
}, },
customFields() { customFields() {
return CustomFields.find({ boardIds: {$in: [this._id]} }, { sort: { name: 1 } }); return CustomFields.find(
{ boardIds: { $in: [this._id] } },
{ sort: { name: 1 } }
);
}, },
// XXX currently mutations return no value so we have an issue when using addLabel in import // XXX currently mutations return no value so we have an issue when using addLabel in import
@ -489,10 +559,7 @@ Boards.helpers({
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Cards.find(query, projection); return Cards.find(query, projection);
@ -506,17 +573,14 @@ Boards.helpers({
query.type = 'template-swimlane'; query.type = 'template-swimlane';
query.archived = false; query.archived = false;
} else { } else {
query.type = {$nin: ['template-swimlane']}; query.type = { $nin: ['template-swimlane'] };
} }
const projection = { limit: 10, sort: { createdAt: -1 } }; const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Swimlanes.find(query, projection); return Swimlanes.find(query, projection);
@ -530,17 +594,14 @@ Boards.helpers({
query.type = 'template-list'; query.type = 'template-list';
query.archived = false; query.archived = false;
} else { } else {
query.type = {$nin: ['template-list']}; query.type = { $nin: ['template-list'] };
} }
const projection = { limit: 10, sort: { createdAt: -1 } }; const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Lists.find(query, projection); return Lists.find(query, projection);
@ -557,17 +618,14 @@ Boards.helpers({
query.type = 'template-card'; query.type = 'template-card';
query.archived = false; query.archived = false;
} else { } else {
query.type = {$nin: ['template-card']}; query.type = { $nin: ['template-card'] };
} }
const projection = { limit: 10, sort: { createdAt: -1 } }; const projection = { limit: 10, sort: { createdAt: -1 } };
if (term) { if (term) {
const regex = new RegExp(term, 'i'); const regex = new RegExp(term, 'i');
query.$or = [ query.$or = [{ title: regex }, { description: regex }];
{ title: regex },
{ description: regex },
];
} }
return Cards.find(query, projection); return Cards.find(query, projection);
@ -575,22 +633,29 @@ Boards.helpers({
// A board alwasy has another board where it deposits subtasks of thasks // A board alwasy has another board where it deposits subtasks of thasks
// that belong to itself. // that belong to itself.
getDefaultSubtasksBoardId() { getDefaultSubtasksBoardId() {
if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) { if (
this.subtasksDefaultBoardId === null ||
this.subtasksDefaultBoardId === undefined
) {
this.subtasksDefaultBoardId = Boards.insert({ this.subtasksDefaultBoardId = Boards.insert({
title: `^${this.title}^`, title: `^${this.title}^`,
permission: this.permission, permission: this.permission,
members: this.members, members: this.members,
color: this.color, color: this.color,
description: TAPi18n.__('default-subtasks-board', {board: this.title}), description: TAPi18n.__('default-subtasks-board', {
board: this.title,
}),
}); });
Swimlanes.insert({ Swimlanes.insert({
title: TAPi18n.__('default'), title: TAPi18n.__('default'),
boardId: this.subtasksDefaultBoardId, boardId: this.subtasksDefaultBoardId,
}); });
Boards.update(this._id, {$set: { Boards.update(this._id, {
$set: {
subtasksDefaultBoardId: this.subtasksDefaultBoardId, subtasksDefaultBoardId: this.subtasksDefaultBoardId,
}}); },
});
} }
return this.subtasksDefaultBoardId; return this.subtasksDefaultBoardId;
}, },
@ -600,7 +665,10 @@ Boards.helpers({
}, },
getDefaultSubtasksListId() { getDefaultSubtasksListId() {
if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) { if (
this.subtasksDefaultListId === null ||
this.subtasksDefaultListId === undefined
) {
this.subtasksDefaultListId = Lists.insert({ this.subtasksDefaultListId = Lists.insert({
title: TAPi18n.__('queue'), title: TAPi18n.__('queue'),
boardId: this._id, boardId: this._id,
@ -615,13 +683,13 @@ Boards.helpers({
}, },
getDefaultSwimline() { getDefaultSwimline() {
let result = Swimlanes.findOne({boardId: this._id}); let result = Swimlanes.findOne({ boardId: this._id });
if (result === undefined) { if (result === undefined) {
Swimlanes.insert({ Swimlanes.insert({
title: TAPi18n.__('default'), title: TAPi18n.__('default'),
boardId: this._id, boardId: this._id,
}); });
result = Swimlanes.findOne({boardId: this._id}); result = Swimlanes.findOne({ boardId: this._id });
} }
return result; return result;
}, },
@ -633,19 +701,24 @@ Boards.helpers({
{ {
startAt: { startAt: {
$lte: start, $lte: start,
}, endAt: { },
endAt: {
$gte: start, $gte: start,
}, },
}, { },
{
startAt: { startAt: {
$lte: end, $lte: end,
}, endAt: { },
endAt: {
$gte: end, $gte: end,
}, },
}, { },
{
startAt: { startAt: {
$gte: start, $gte: start,
}, endAt: { },
endAt: {
$lte: end, $lte: end,
}, },
}, },
@ -662,7 +735,6 @@ Boards.helpers({
}, },
}); });
Boards.mutations({ Boards.mutations({
archive() { archive() {
return { $set: { archived: true } }; return { $set: { archived: true } };
@ -753,7 +825,8 @@ Boards.mutations({
const memberIndex = this.memberIndex(memberId); const memberIndex = this.memberIndex(memberId);
// we do not allow the only one admin to be removed // we do not allow the only one admin to be removed
const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1); const allowRemove =
!this.members[memberIndex].isAdmin || this.activeAdmins().length > 1;
if (!allowRemove) { if (!allowRemove) {
return { return {
$set: { $set: {
@ -770,7 +843,13 @@ Boards.mutations({
}; };
}, },
setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly, currentUserId = Meteor.userId()) { setMemberPermission(
memberId,
isAdmin,
isNoComments,
isCommentOnly,
currentUserId = Meteor.userId()
) {
const memberIndex = this.memberIndex(memberId); const memberIndex = this.memberIndex(memberId);
// do not allow change permission of self // do not allow change permission of self
if (memberId === currentUserId) { if (memberId === currentUserId) {
@ -804,12 +883,13 @@ Boards.mutations({
}); });
function boardRemover(userId, doc) { function boardRemover(userId, doc) {
[Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach((element) => { [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach(
(element) => {
element.remove({ boardId: doc._id }); element.remove({ boardId: doc._id });
}); }
);
} }
if (Meteor.isServer) { if (Meteor.isServer) {
Boards.allow({ Boards.allow({
insert: Meteor.userId, insert: Meteor.userId,
@ -830,25 +910,25 @@ if (Meteor.isServer) {
// 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(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
const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true }).length; const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true })
if (nbAdmins > 1) .length;
return false; if (nbAdmins > 1) return false;
// 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
const removedMemberId = modifier.$pull.members.userId; const removedMemberId = modifier.$pull.members.userId;
return Boolean(_.findWhere(doc.members, { return Boolean(
_.findWhere(doc.members, {
userId: removedMemberId, userId: removedMemberId,
isAdmin: true, isAdmin: true,
})); })
);
}, },
fetch: ['members'], fetch: ['members'],
}); });
@ -882,16 +962,19 @@ if (Meteor.isServer) {
} else throw new Meteor.Error('error-board-doesNotExist'); } else throw new Meteor.Error('error-board-doesNotExist');
}, },
}); });
} }
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(() => { Meteor.startup(() => {
Boards._collection._ensureIndex({ Boards._collection._ensureIndex({ modifiedAt: -1 });
Boards._collection._ensureIndex(
{
_id: 1, _id: 1,
'members.userId': 1, 'members.userId': 1,
}, { unique: true }); },
{ unique: true }
);
Boards._collection._ensureIndex({ 'members.userId': 1 }); Boards._collection._ensureIndex({ 'members.userId': 1 });
}); });
@ -909,10 +992,12 @@ if (Meteor.isServer) {
// 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((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;
} }
@ -935,12 +1020,21 @@ if (Meteor.isServer) {
} }
const parts = set.split('.'); const parts = set.split('.');
if (parts.length === 3 && parts[0] === 'members' && parts[2] === 'isActive') { if (
parts.length === 3 &&
parts[0] === 'members' &&
parts[2] === 'isActive'
) {
callback(doc.members[parts[1]].userId); callback(doc.members[parts[1]].userId);
} }
}); });
}; };
Boards.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
// Remove a member from all objects of the board before leaving the board // Remove a member from all objects of the board before leaving the board
Boards.before.update((userId, doc, fieldNames, modifier) => { Boards.before.update((userId, doc, fieldNames, modifier) => {
if (!_.contains(fieldNames, 'members')) { if (!_.contains(fieldNames, 'members')) {
@ -976,14 +1070,11 @@ if (Meteor.isServer) {
// Remove board from users starred list // Remove board from users starred list
if (!board.isPublic()) { if (!board.isPublic()) {
Users.update( Users.update(memberId, {
memberId,
{
$pull: { $pull: {
'profile.starredBoards': boardId, 'profile.starredBoards': boardId,
}, },
} });
);
} }
}); });
} }
@ -1044,29 +1135,34 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
title: string}] title: string}]
*/ */
JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) { JsonRoutes.add('GET', '/api/users/:userId/boards', function(req, res) {
try { try {
Authentication.checkLoggedIn(req.userId); Authentication.checkLoggedIn(req.userId);
const paramUserId = req.params.userId; const paramUserId = req.params.userId;
// A normal user should be able to see their own boards, // A normal user should be able to see their own boards,
// admins can access boards of any user // admins can access boards of any user
Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId); Authentication.checkAdminOrCondition(
req.userId,
req.userId === paramUserId
);
const data = Boards.find({ const data = Boards.find(
{
archived: false, archived: false,
'members.userId': paramUserId, 'members.userId': paramUserId,
}, { },
{
sort: ['title'], sort: ['title'],
}).map(function(board) { }
).map(function(board) {
return { return {
_id: board._id, _id: board._id,
title: board.title, title: board.title,
}; };
}); });
JsonRoutes.sendResult(res, {code: 200, data}); JsonRoutes.sendResult(res, { code: 200, data });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1081,20 +1177,19 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
title: string}] title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards', function (req, res) { JsonRoutes.add('GET', '/api/boards', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Boards.find({ permission: 'public' }).map(function (doc) { data: Boards.find({ permission: 'public' }).map(function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
}; };
}), }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1109,7 +1204,7 @@ if (Meteor.isServer) {
* @param {string} boardId the ID of the board to retrieve the data * @param {string} boardId the ID of the board to retrieve the data
* @return_type Boards * @return_type Boards
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId', function(req, res) {
try { try {
const id = req.params.boardId; const id = req.params.boardId;
Authentication.checkBoardAccess(req.userId, id); Authentication.checkBoardAccess(req.userId, id);
@ -1118,8 +1213,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: Boards.findOne({ _id: id }), data: Boards.findOne({ _id: id }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1152,7 +1246,7 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
defaultSwimlaneId: string} defaultSwimlaneId: string}
*/ */
JsonRoutes.add('POST', '/api/boards', function (req, res) { JsonRoutes.add('POST', '/api/boards', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = Boards.insert({ const id = Boards.insert({
@ -1180,8 +1274,7 @@ if (Meteor.isServer) {
defaultSwimlaneId: swimlaneId, defaultSwimlaneId: swimlaneId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1195,19 +1288,18 @@ if (Meteor.isServer) {
* *
* @param {string} boardId the ID of the board * @param {string} boardId the ID of the board
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId', function (req, res) { JsonRoutes.add('DELETE', '/api/boards/:boardId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.boardId; const id = req.params.boardId;
Boards.remove({ _id: id }); Boards.remove({ _id: id });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data:{ data: {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1228,7 +1320,7 @@ if (Meteor.isServer) {
* *
* @return_type string * @return_type string
*/ */
JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function (req, res) { JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function(req, res) {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.boardId; const id = req.params.boardId;
try { try {
@ -1238,7 +1330,10 @@ if (Meteor.isServer) {
const name = req.body.label.name; const name = req.body.label.name;
const labelId = Random.id(6); const labelId = Random.id(6);
if (!board.getLabel(name, color)) { if (!board.getLabel(name, color)) {
Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } }); Boards.direct.update(
{ _id: id },
{ $push: { labels: { _id: labelId, name, color } } }
);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: labelId, data: labelId,
@ -1249,8 +1344,7 @@ if (Meteor.isServer) {
}); });
} }
} }
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
data: error, data: error,
}); });
@ -1268,29 +1362,36 @@ if (Meteor.isServer) {
* @param {boolean} isNoComments NoComments capability * @param {boolean} isNoComments NoComments capability
* @param {boolean} isCommentOnly CommentsOnly capability * @param {boolean} isCommentOnly CommentsOnly capability
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function (req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function(
req,
res
) {
try { try {
const boardId = req.params.boardId; const boardId = req.params.boardId;
const memberId = req.params.memberId; const memberId = req.params.memberId;
const {isAdmin, isNoComments, isCommentOnly} = req.body; const { isAdmin, isNoComments, isCommentOnly } = req.body;
Authentication.checkBoardAccess(req.userId, boardId); Authentication.checkBoardAccess(req.userId, boardId);
const board = Boards.findOne({ _id: boardId }); const board = Boards.findOne({ _id: boardId });
function isTrue(data){ function isTrue(data) {
try { try {
return data.toLowerCase() === 'true'; return data.toLowerCase() === 'true';
} } catch (error) {
catch (error) {
return data; return data;
} }
} }
const query = board.setMemberPermission(memberId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), req.userId); const query = board.setMemberPermission(
memberId,
isTrue(isAdmin),
isTrue(isNoComments),
isTrue(isCommentOnly),
req.userId
);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: query, data: query,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1298,3 +1399,5 @@ if (Meteor.isServer) {
} }
}); });
} }
export default Boards;

View file

@ -3,7 +3,8 @@ CardComments = new Mongo.Collection('card_comments');
/** /**
* A comment on a card * A comment on a card
*/ */
CardComments.attachSchema(new SimpleSchema({ CardComments.attachSchema(
new SimpleSchema({
boardId: { boardId: {
/** /**
* the board ID * the board ID
@ -23,15 +24,14 @@ CardComments.attachSchema(new SimpleSchema({
*/ */
type: String, type: String,
}, },
// XXX We probably don't need this information here, since we already have it
// in the associated comment creation activity
createdAt: { createdAt: {
/** /**
* when was the comment created * when was the comment created
*/ */
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -39,19 +39,33 @@ CardComments.attachSchema(new SimpleSchema({
} }
}, },
}, },
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
// XXX Should probably be called `authorId` // XXX Should probably be called `authorId`
userId: { userId: {
/** /**
* the author ID of the comment * the author ID of the comment
*/ */
type: String, type: String,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return this.userId; return this.userId;
} }
}, },
}, },
})); })
);
CardComments.allow({ CardComments.allow({
insert(userId, doc) { insert(userId, doc) {
@ -80,7 +94,7 @@ CardComments.helpers({
CardComments.hookOptions.after.update = { fetchPrevious: false }; CardComments.hookOptions.after.update = { fetchPrevious: false };
function commentCreation(userId, doc){ function commentCreation(userId, doc) {
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
Activities.insert({ Activities.insert({
userId, userId,
@ -93,10 +107,16 @@ function commentCreation(userId, doc){
}); });
} }
CardComments.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) { if (Meteor.isServer) {
// Comments are often fetched within a card, so we create an index to make these // Comments are often fetched within a card, so we create an index to make these
// queries more efficient. // queries more efficient.
Meteor.startup(() => { Meteor.startup(() => {
CardComments._collection._ensureIndex({ modifiedAt: -1 });
CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 }); CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 });
}); });
@ -152,14 +172,20 @@ if (Meteor.isServer) {
* comment: string, * comment: string,
* authorId: string}] * authorId: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function(
req,
res
) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CardComments.find({ boardId: paramBoardId, cardId: paramCardId}).map(function (doc) { data: CardComments.find({
boardId: paramBoardId,
cardId: paramCardId,
}).map(function(doc) {
return { return {
_id: doc._id, _id: doc._id,
comment: doc.text, comment: doc.text,
@ -167,8 +193,7 @@ if (Meteor.isServer) {
}; };
}), }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -185,24 +210,31 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to retrieve * @param {string} commentId the ID of the comment to retrieve
* @return_type CardComments * @return_type CardComments
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId; const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }), data: CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
}),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation new_comment * @operation new_comment
@ -214,9 +246,12 @@ if (Meteor.isServer) {
* @param {string} text the content of the comment * @param {string} text the content of the comment
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/comments',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const id = CardComments.direct.insert({ const id = CardComments.direct.insert({
@ -233,16 +268,20 @@ if (Meteor.isServer) {
}, },
}); });
const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId }); const cardComment = CardComments.findOne({
_id: id,
cardId: paramCardId,
boardId: paramBoardId,
});
commentCreation(req.body.authorId, cardComment); commentCreation(req.body.authorId, cardComment);
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation delete_comment * @operation delete_comment
@ -253,25 +292,34 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to delete * @param {string} commentId the ID of the comment to delete
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId; const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }); CardComments.remove({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: paramCardId, _id: paramCardId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
} }
export default CardComments;

View file

@ -81,7 +81,8 @@ Cards.attachSchema(new SimpleSchema({
* creation date * creation date
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -89,6 +90,18 @@ Cards.attachSchema(new SimpleSchema({
} }
}, },
}, },
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
customFields: { customFields: {
/** /**
* list of custom fields * list of custom fields
@ -1539,7 +1552,8 @@ if (Meteor.isServer) {
// Cards are often fetched within a board, so we create an index to make these // Cards are often fetched within a board, so we create an index to make these
// queries more efficient. // queries more efficient.
Meteor.startup(() => { Meteor.startup(() => {
Cards._collection._ensureIndex({boardId: 1, createdAt: -1}); Cards._collection._ensureIndex({ modifiedAt: -1 });
Cards._collection._ensureIndex({ boardId: 1, createdAt: -1 });
// https://github.com/wekan/wekan/issues/1863 // https://github.com/wekan/wekan/issues/1863
// Swimlane added a new field in the cards collection of mongodb named parentId. // Swimlane added a new field in the cards collection of mongodb named parentId.
// When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection). // When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection).
@ -1581,6 +1595,11 @@ if (Meteor.isServer) {
cardCustomFields(userId, doc, fieldNames, modifier); cardCustomFields(userId, doc, fieldNames, modifier);
}); });
Cards.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
// Remove all activities associated with a card if we remove the card // Remove all activities associated with a card if we remove the card
// Remove also card_comments / checklists / attachments // Remove also card_comments / checklists / attachments
Cards.before.remove((userId, doc) => { Cards.before.remove((userId, doc) => {
@ -1980,3 +1999,5 @@ if (Meteor.isServer) {
}); });
} }
export default Cards;

View file

@ -3,7 +3,8 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/** /**
* An item in a checklist * An item in a checklist
*/ */
ChecklistItems.attachSchema(new SimpleSchema({ ChecklistItems.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* the text of the item * the text of the item
@ -36,7 +37,32 @@ ChecklistItems.attachSchema(new SimpleSchema({
*/ */
type: String, type: String,
}, },
})); createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
ChecklistItems.allow({ ChecklistItems.allow({
insert(userId, doc) { insert(userId, doc) {
@ -62,10 +88,10 @@ ChecklistItems.mutations({
setTitle(title) { setTitle(title) {
return { $set: { title } }; return { $set: { title } };
}, },
check(){ check() {
return { $set: { isFinished: true } }; return { $set: { isFinished: true } };
}, },
uncheck(){ uncheck() {
return { $set: { isFinished: false } }; return { $set: { isFinished: false } };
}, },
toggleItem() { toggleItem() {
@ -79,7 +105,7 @@ ChecklistItems.mutations({
sort: sortIndex, sort: sortIndex,
}; };
return {$set: mutatedFields}; return { $set: mutatedFields };
}, },
}); });
@ -106,13 +132,13 @@ function itemRemover(userId, doc) {
}); });
} }
function publishCheckActivity(userId, doc){ function publishCheckActivity(userId, doc) {
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
const boardId = card.boardId; const boardId = card.boardId;
let activityType; let activityType;
if(doc.isFinished){ if (doc.isFinished) {
activityType = 'checkedItem'; activityType = 'checkedItem';
}else{ } else {
activityType = 'uncheckedItem'; activityType = 'uncheckedItem';
} }
const act = { const act = {
@ -122,19 +148,19 @@ function publishCheckActivity(userId, doc){
boardId, boardId,
checklistId: doc.checklistId, checklistId: doc.checklistId,
checklistItemId: doc._id, checklistItemId: doc._id,
checklistItemName:doc.title, checklistItemName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}; };
Activities.insert(act); Activities.insert(act);
} }
function publishChekListCompleted(userId, doc){ function publishChekListCompleted(userId, doc) {
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
const boardId = card.boardId; const boardId = card.boardId;
const checklistId = doc.checklistId; const checklistId = doc.checklistId;
const checkList = Checklists.findOne({_id:checklistId}); const checkList = Checklists.findOne({ _id: checklistId });
if(checkList.isFinished()){ if (checkList.isFinished()) {
const act = { const act = {
userId, userId,
activityType: 'completeChecklist', activityType: 'completeChecklist',
@ -149,11 +175,11 @@ function publishChekListCompleted(userId, doc){
} }
} }
function publishChekListUncompleted(userId, doc){ function publishChekListUncompleted(userId, doc) {
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
const boardId = card.boardId; const boardId = card.boardId;
const checklistId = doc.checklistId; const checklistId = doc.checklistId;
const checkList = Checklists.findOne({_id:checklistId}); const checkList = Checklists.findOne({ _id: checklistId });
// BUGS in IFTTT Rules: https://github.com/wekan/wekan/issues/1972 // BUGS in IFTTT Rules: https://github.com/wekan/wekan/issues/1972
// Currently in checklist all are set as uncompleted/not checked, // Currently in checklist all are set as uncompleted/not checked,
// IFTTT Rule does not move card to other list. // IFTTT Rule does not move card to other list.
@ -167,7 +193,7 @@ function publishChekListUncompleted(userId, doc){
// find . | xargs grep 'count' -sl | grep -v .meteor | grep -v node_modules | grep -v .build // find . | xargs grep 'count' -sl | grep -v .meteor | grep -v node_modules | grep -v .build
// Maybe something related here? // Maybe something related here?
// wekan/client/components/rules/triggers/checklistTriggers.js // wekan/client/components/rules/triggers/checklistTriggers.js
if(checkList.isFinished()){ if (checkList.isFinished()) {
const act = { const act = {
userId, userId,
activityType: 'uncompleteChecklist', activityType: 'uncompleteChecklist',
@ -185,6 +211,7 @@ function publishChekListUncompleted(userId, doc){
// Activities // Activities
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
ChecklistItems._collection._ensureIndex({ modifiedAt: -1 });
ChecklistItems._collection._ensureIndex({ checklistId: 1 }); ChecklistItems._collection._ensureIndex({ checklistId: 1 });
ChecklistItems._collection._ensureIndex({ cardId: 1 }); ChecklistItems._collection._ensureIndex({ cardId: 1 });
}); });
@ -198,6 +225,10 @@ if (Meteor.isServer) {
publishChekListUncompleted(userId, doc, fieldNames); publishChekListUncompleted(userId, doc, fieldNames);
}); });
ChecklistItems.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
ChecklistItems.after.insert((userId, doc) => { ChecklistItems.after.insert((userId, doc) => {
itemCreation(userId, doc); itemCreation(userId, doc);
@ -214,7 +245,7 @@ if (Meteor.isServer) {
boardId, boardId,
checklistId: doc.checklistId, checklistId: doc.checklistId,
checklistItemId: doc._id, checklistItemId: doc._id,
checklistItemName:doc.title, checklistItemName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}); });
@ -233,8 +264,11 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item * @param {string} itemId the ID of the item
* @return_type ChecklistItems * @return_type ChecklistItems
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId; const paramItemId = req.params.itemId;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId }); const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) { if (checklistItem) {
@ -247,7 +281,8 @@ if (Meteor.isServer) {
code: 500, code: 500,
}); });
} }
}); }
);
/** /**
* @operation edit_checklist_item * @operation edit_checklist_item
@ -262,16 +297,25 @@ if (Meteor.isServer) {
* @param {string} [title] the new text of the item * @param {string} [title] the new text of the item
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'PUT',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId; const paramItemId = req.params.itemId;
if (req.body.hasOwnProperty('isFinished')) { if (req.body.hasOwnProperty('isFinished')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}}); ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { isFinished: req.body.isFinished } }
);
} }
if (req.body.hasOwnProperty('title')) { if (req.body.hasOwnProperty('title')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}}); ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { title: req.body.title } }
);
} }
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -280,7 +324,8 @@ if (Meteor.isServer) {
_id: paramItemId, _id: paramItemId,
}, },
}); });
}); }
);
/** /**
* @operation delete_checklist_item * @operation delete_checklist_item
@ -295,8 +340,11 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item to be removed * @param {string} itemId the ID of the item to be removed
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId; const paramItemId = req.params.itemId;
ChecklistItems.direct.remove({ _id: paramItemId }); ChecklistItems.direct.remove({ _id: paramItemId });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -305,5 +353,8 @@ if (Meteor.isServer) {
_id: paramItemId, _id: paramItemId,
}, },
}); });
}); }
);
} }
export default ChecklistItems;

View file

@ -3,7 +3,8 @@ Checklists = new Mongo.Collection('checklists');
/** /**
* A Checklist * A Checklist
*/ */
Checklists.attachSchema(new SimpleSchema({ Checklists.attachSchema(
new SimpleSchema({
cardId: { cardId: {
/** /**
* The ID of the card the checklist is in * The ID of the card the checklist is in
@ -30,7 +31,8 @@ Checklists.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -38,6 +40,18 @@ Checklists.attachSchema(new SimpleSchema({
} }
}, },
}, },
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
sort: { sort: {
/** /**
* sorting value of the checklist * sorting value of the checklist
@ -45,7 +59,8 @@ Checklists.attachSchema(new SimpleSchema({
type: Number, type: Number,
decimal: true, decimal: true,
}, },
})); })
);
Checklists.helpers({ Checklists.helpers({
copy(newCardId) { copy(newCardId) {
@ -53,7 +68,7 @@ Checklists.helpers({
this._id = null; this._id = null;
this.cardId = newCardId; this.cardId = newCardId;
const newChecklistId = Checklists.insert(this); const newChecklistId = Checklists.insert(this);
ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => { ChecklistItems.find({ checklistId: oldChecklistId }).forEach((item) => {
item._id = null; item._id = null;
item.checklistId = newChecklistId; item.checklistId = newChecklistId;
item.cardId = newCardId; item.cardId = newCardId;
@ -65,9 +80,12 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count(); return ChecklistItems.find({ checklistId: this._id }).count();
}, },
items() { items() {
return ChecklistItems.find({ return ChecklistItems.find(
{
checklistId: this._id, checklistId: this._id,
}, { sort: ['sort'] }); },
{ sort: ['sort'] }
);
}, },
finishedCount() { finishedCount() {
return ChecklistItems.find({ return ChecklistItems.find({
@ -78,20 +96,20 @@ Checklists.helpers({
isFinished() { isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount(); return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
}, },
checkAllItems(){ checkAllItems() {
const checkItems = ChecklistItems.find({checklistId: this._id}); const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item){ checkItems.forEach(function(item) {
item.check(); item.check();
}); });
}, },
uncheckAllItems(){ uncheckAllItems() {
const checkItems = ChecklistItems.find({checklistId: this._id}); const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item){ checkItems.forEach(function(item) {
item.uncheck(); item.uncheck();
}); });
}, },
itemIndex(itemId) { itemIndex(itemId) {
const items = self.findOne({_id : this._id}).items; const items = self.findOne({ _id: this._id }).items;
return _.pluck(items, '_id').indexOf(itemId); return _.pluck(items, '_id').indexOf(itemId);
}, },
}); });
@ -124,6 +142,7 @@ Checklists.mutations({
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Checklists._collection._ensureIndex({ modifiedAt: -1 });
Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 }); Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 });
}); });
@ -135,12 +154,17 @@ if (Meteor.isServer) {
cardId: doc.cardId, cardId: doc.cardId,
boardId: card.boardId, boardId: card.boardId,
checklistId: doc._id, checklistId: doc._id,
checklistName:doc.title, checklistName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}); });
}); });
Checklists.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Checklists.before.remove((userId, doc) => { Checklists.before.remove((userId, doc) => {
const activities = Activities.find({ checklistId: doc._id }); const activities = Activities.find({ checklistId: doc._id });
const card = Cards.findOne(doc.cardId); const card = Cards.findOne(doc.cardId);
@ -155,7 +179,7 @@ if (Meteor.isServer) {
cardId: doc.cardId, cardId: doc.cardId,
boardId: Cards.findOne(doc.cardId).boardId, boardId: Cards.findOne(doc.cardId).boardId,
checklistId: doc._id, checklistId: doc._id,
checklistName:doc.title, checklistName: doc.title,
listId: card.listId, listId: card.listId,
swimlaneId: card.swimlaneId, swimlaneId: card.swimlaneId,
}); });
@ -172,10 +196,15 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
* title: string}] * title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) { const checklists = Checklists.find({ cardId: paramCardId }).map(function(
doc
) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
@ -191,7 +220,8 @@ if (Meteor.isServer) {
code: 500, code: 500,
}); });
} }
}); }
);
/** /**
* @operation get_checklist * @operation get_checklist
@ -209,13 +239,21 @@ if (Meteor.isServer) {
* title: string, * title: string,
* isFinished: boolean}]} * isFinished: boolean}]}
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId; const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }); const checklist = Checklists.findOne({
_id: paramChecklistId,
cardId: paramCardId,
});
if (checklist) { if (checklist) {
checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) { checklist.items = ChecklistItems.find({
checklistId: checklist._id,
}).map(function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
@ -231,7 +269,8 @@ if (Meteor.isServer) {
code: 500, code: 500,
}); });
} }
}); }
);
/** /**
* @operation new_checklist * @operation new_checklist
@ -242,8 +281,11 @@ if (Meteor.isServer) {
* @param {string} title the title of the new checklist * @param {string} title the title of the new checklist
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'POST',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId; const paramCardId = req.params.cardId;
const id = Checklists.insert({ const id = Checklists.insert({
@ -252,7 +294,7 @@ if (Meteor.isServer) {
sort: 0, sort: 0,
}); });
if (id) { if (id) {
req.body.items.forEach(function (item, idx) { req.body.items.forEach(function(item, idx) {
ChecklistItems.insert({ ChecklistItems.insert({
cardId: paramCardId, cardId: paramCardId,
checklistId: id, checklistId: id,
@ -271,7 +313,8 @@ if (Meteor.isServer) {
code: 400, code: 400,
}); });
} }
}); }
);
/** /**
* @operation delete_checklist * @operation delete_checklist
@ -284,8 +327,11 @@ if (Meteor.isServer) {
* @param {string} checklistId the ID of the checklist to remove * @param {string} checklistId the ID of the checklist to remove
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId; const paramChecklistId = req.params.checklistId;
Checklists.remove({ _id: paramChecklistId }); Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -294,5 +340,8 @@ if (Meteor.isServer) {
_id: paramChecklistId, _id: paramChecklistId,
}, },
}); });
}); }
);
} }
export default Checklists;

View file

@ -3,7 +3,8 @@ CustomFields = new Mongo.Collection('customFields');
/** /**
* A custom field on a card in the board * A custom field on a card in the board
*/ */
CustomFields.attachSchema(new SimpleSchema({ CustomFields.attachSchema(
new SimpleSchema({
boardIds: { boardIds: {
/** /**
* the ID of the board * the ID of the board
@ -70,7 +71,32 @@ CustomFields.attachSchema(new SimpleSchema({
*/ */
type: Boolean, type: Boolean,
}, },
})); createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
CustomFields.mutations({ CustomFields.mutations({
addBoard(boardId) { addBoard(boardId) {
@ -88,19 +114,28 @@ CustomFields.mutations({
CustomFields.allow({ CustomFields.allow({
insert(userId, doc) { insert(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({ return allowIsAnyBoardMember(
_id: {$in: doc.boardIds}, userId,
}).fetch()); Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
}, },
update(userId, doc) { update(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({ return allowIsAnyBoardMember(
_id: {$in: doc.boardIds}, userId,
}).fetch()); Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
}, },
remove(userId, doc) { remove(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({ return allowIsAnyBoardMember(
_id: {$in: doc.boardIds}, userId,
}).fetch()); Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
}, },
fetch: ['userId', 'boardIds'], fetch: ['userId', 'boardIds'],
}); });
@ -108,7 +143,7 @@ CustomFields.allow({
// not sure if we need this? // not sure if we need this?
//CustomFields.hookOptions.after.update = { fetchPrevious: false }; //CustomFields.hookOptions.after.update = { fetchPrevious: false };
function customFieldCreation(userId, doc){ function customFieldCreation(userId, doc) {
Activities.insert({ Activities.insert({
userId, userId,
activityType: 'createCustomField', activityType: 'createCustomField',
@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
CustomFields._collection._ensureIndex({ modifiedAt: -1 });
CustomFields._collection._ensureIndex({ boardIds: 1 }); CustomFields._collection._ensureIndex({ boardIds: 1 });
}); });
@ -149,12 +185,17 @@ if (Meteor.isServer) {
customFieldCreation(userId, doc); customFieldCreation(userId, doc);
}); });
CustomFields.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
CustomFields.before.update((userId, doc, fieldNames, modifier) => { CustomFields.before.update((userId, doc, fieldNames, modifier) => {
if (_.contains(fieldNames, 'boardIds') && modifier.$pull) { if (_.contains(fieldNames, 'boardIds') && modifier.$pull) {
Cards.update( Cards.update(
{boardId: modifier.$pull.boardIds, 'customFields._id': doc._id}, { boardId: modifier.$pull.boardIds, 'customFields._id': doc._id },
{$pull: {'customFields': {'_id': doc._id}}}, { $pull: { customFields: { _id: doc._id } } },
{multi: true} { multi: true }
); );
customFieldEdit(userId, doc); customFieldEdit(userId, doc);
Activities.remove({ Activities.remove({
@ -180,9 +221,9 @@ if (Meteor.isServer) {
}); });
Cards.update( Cards.update(
{boardId: {$in: doc.boardIds}, 'customFields._id': doc._id}, { boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
{$pull: {'customFields': {'_id': doc._id}}}, { $pull: { customFields: { _id: doc._id } } },
{multi: true} { multi: true }
); );
}); });
} }
@ -198,18 +239,23 @@ if (Meteor.isServer) {
* name: string, * name: string,
* type: string}] * type: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function(
Authentication.checkUserId( req.userId); req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CustomFields.find({ boardIds: {$in: [paramBoardId]} }).map(function (cf) { data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
function(cf) {
return { return {
_id: cf._id, _id: cf._id,
name: cf.name, name: cf.name,
type: cf.type, type: cf.type,
}; };
}), }
),
}); });
}); });
@ -221,15 +267,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field * @param {string} customFieldId the ID of the custom field
* @return_type CustomFields * @return_type CustomFields
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'GET',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId; const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }), data: CustomFields.findOne({
}); _id: paramCustomFieldId,
boardIds: { $in: [paramBoardId] },
}),
}); });
}
);
/** /**
* @operation new_custom_field * @operation new_custom_field
@ -244,8 +297,11 @@ if (Meteor.isServer) {
* @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards? * @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function(
Authentication.checkUserId( req.userId); req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const id = CustomFields.direct.insert({ const id = CustomFields.direct.insert({
name: req.body.name, name: req.body.name,
@ -254,10 +310,13 @@ if (Meteor.isServer) {
showOnCard: req.body.showOnCard, showOnCard: req.body.showOnCard,
automaticallyOnCard: req.body.automaticallyOnCard, automaticallyOnCard: req.body.automaticallyOnCard,
showLabelOnMiniCard: req.body.showLabelOnMiniCard, showLabelOnMiniCard: req.body.showLabelOnMiniCard,
boardIds: {$in: [paramBoardId]}, boardIds: { $in: [paramBoardId] },
}); });
const customField = CustomFields.findOne({_id: id, boardIds: {$in: [paramBoardId]} }); const customField = CustomFields.findOne({
_id: id,
boardIds: { $in: [paramBoardId] },
});
customFieldCreation(req.body.authorId, customField); customFieldCreation(req.body.authorId, customField);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -278,16 +337,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field * @param {string} customFieldId the ID of the custom field
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) { JsonRoutes.add(
Authentication.checkUserId( req.userId); 'DELETE',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const id = req.params.customFieldId; const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} }); CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: id, _id: id,
}, },
}); });
}); }
);
} }
export default CustomFields;

View file

@ -3,7 +3,8 @@ Integrations = new Mongo.Collection('integrations');
/** /**
* Integration with third-party applications * Integration with third-party applications
*/ */
Integrations.attachSchema(new SimpleSchema({ Integrations.attachSchema(
new SimpleSchema({
enabled: { enabled: {
/** /**
* is the integration enabled? * is the integration enabled?
@ -32,7 +33,8 @@ Integrations.attachSchema(new SimpleSchema({
type: [String], type: [String],
defaultValue: ['all'], defaultValue: ['all'],
}, },
url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex) url: {
// URL validation regex (https://mathiasbynens.be/demo/url-regex)
/** /**
* URL validation regex (https://mathiasbynens.be/demo/url-regex) * URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/ */
@ -57,7 +59,8 @@ Integrations.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -65,13 +68,31 @@ Integrations.attachSchema(new SimpleSchema({
} }
}, },
}, },
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
userId: { userId: {
/** /**
* user ID who created the interation * user ID who created the interation
*/ */
type: String, type: String,
}, },
})); })
);
Integrations.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Integrations.allow({ Integrations.allow({
insert(userId, doc) { insert(userId, doc) {
@ -89,6 +110,7 @@ Integrations.allow({
//INTEGRATIONS REST API //INTEGRATIONS REST API
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Integrations._collection._ensureIndex({ modifiedAt: -1 });
Integrations._collection._ensureIndex({ boardId: 1 }); Integrations._collection._ensureIndex({ boardId: 1 });
}); });
@ -99,18 +121,23 @@ if (Meteor.isServer) {
* @param {string} boardId the board ID * @param {string} boardId the board ID
* @return_type [Integrations] * @return_type [Integrations]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
const data = Integrations.find({ boardId: paramBoardId }, { fields: { token: 0 } }).map(function(doc) { const data = Integrations.find(
{ boardId: paramBoardId },
{ fields: { token: 0 } }
).map(function(doc) {
return doc; return doc;
}); });
JsonRoutes.sendResult(res, {code: 200, data}); JsonRoutes.sendResult(res, { code: 200, data });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -126,7 +153,10 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID * @param {string} intId the integration ID
* @return_type Integrations * @return_type Integrations
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
@ -134,10 +164,12 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }), data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { token: 0 } }
),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -153,7 +185,10 @@ if (Meteor.isServer) {
* @param {string} url the URL of the integration * @param {string} url the URL of the integration
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
@ -170,8 +205,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -192,7 +226,10 @@ if (Meteor.isServer) {
* @param {string} [activities] new list of activities of the integration * @param {string} [activities] new list of activities of the integration
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res) { JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
@ -200,28 +237,38 @@ if (Meteor.isServer) {
if (req.body.hasOwnProperty('enabled')) { if (req.body.hasOwnProperty('enabled')) {
const newEnabled = req.body.enabled; const newEnabled = req.body.enabled;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {enabled: newEnabled}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { enabled: newEnabled } }
);
} }
if (req.body.hasOwnProperty('title')) { if (req.body.hasOwnProperty('title')) {
const newTitle = req.body.title; const newTitle = req.body.title;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {title: newTitle}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { title: newTitle } }
);
} }
if (req.body.hasOwnProperty('url')) { if (req.body.hasOwnProperty('url')) {
const newUrl = req.body.url; const newUrl = req.body.url;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {url: newUrl}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { url: newUrl } }
);
} }
if (req.body.hasOwnProperty('token')) { if (req.body.hasOwnProperty('token')) {
const newToken = req.body.token; const newToken = req.body.token;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {token: newToken}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { token: newToken } }
);
} }
if (req.body.hasOwnProperty('activities')) { if (req.body.hasOwnProperty('activities')) {
const newActivities = req.body.activities; const newActivities = req.body.activities;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$set: {activities: newActivities}}); { _id: paramIntId, boardId: paramBoardId },
{ $set: { activities: newActivities } }
);
} }
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
@ -230,8 +277,7 @@ if (Meteor.isServer) {
_id: paramIntId, _id: paramIntId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -248,28 +294,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to remove from the integration * @param {string} newActivities the activities to remove from the integration
* @return_type Integrations * @return_type Integrations
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) { JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/integrations/:intId/activities',
function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
const newActivities = req.body.activities; const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$pullAll: {activities: newActivities}}); { _id: paramIntId, boardId: paramBoardId },
{ $pullAll: { activities: newActivities } }
);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { _id: 1, activities: 1 } }
),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation new_integration_activities * @operation new_integration_activities
@ -280,28 +334,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to add to the integration * @param {string} newActivities the activities to add to the integration
* @return_type Integrations * @return_type Integrations
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) { JsonRoutes.add(
'POST',
'/api/boards/:boardId/integrations/:intId/activities',
function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
const newActivities = req.body.activities; const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, Integrations.direct.update(
{$addToSet: {activities: { $each: newActivities}}}); { _id: paramIntId, boardId: paramBoardId },
{ $addToSet: { activities: { $each: newActivities } } }
);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), data: Integrations.findOne(
{ _id: paramIntId, boardId: paramBoardId },
{ fields: { _id: 1, activities: 1 } }
),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation delete_integration * @operation delete_integration
@ -311,21 +373,23 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID * @param {string} intId the integration ID
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res) { JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId; const paramIntId = req.params.intId;
Authentication.checkBoardAccess(req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId}); Integrations.direct.remove({ _id: paramIntId, boardId: paramBoardId });
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: paramIntId, _id: paramIntId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -333,3 +397,5 @@ if (Meteor.isServer) {
} }
}); });
} }
export default Integrations;

View file

@ -1,6 +1,7 @@
InvitationCodes = new Mongo.Collection('invitation_codes'); InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({ InvitationCodes.attachSchema(
new SimpleSchema({
code: { code: {
type: String, type: String,
}, },
@ -12,6 +13,27 @@ InvitationCodes.attachSchema(new SimpleSchema({
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: false, denyUpdate: false,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
}, },
// always be the admin if only one admin // always be the admin if only one admin
authorId: { authorId: {
@ -25,21 +47,32 @@ InvitationCodes.attachSchema(new SimpleSchema({
type: Boolean, type: Boolean,
defaultValue: true, defaultValue: true,
}, },
})); })
);
InvitationCodes.helpers({ InvitationCodes.helpers({
author(){ author() {
return Users.findOne(this.authorId); return Users.findOne(this.authorId);
}, },
}); });
InvitationCodes.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
// InvitationCodes.before.insert((userId, doc) => { // InvitationCodes.before.insert((userId, doc) => {
// doc.createdAt = new Date(); // doc.createdAt = new Date();
// doc.authorId = userId; // doc.authorId = userId;
// }); // });
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => {
InvitationCodes._collection._ensureIndex({ modifiedAt: -1 });
});
Boards.deny({ Boards.deny({
fetch: ['members'], fetch: ['members'],
}); });
} }
export default InvitationCodes;

View file

@ -3,7 +3,8 @@ Lists = new Mongo.Collection('lists');
/** /**
* A list (column) in the Wekan board. * A list (column) in the Wekan board.
*/ */
Lists.attachSchema(new SimpleSchema({ Lists.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* the title of the list * the title of the list
@ -15,7 +16,8 @@ Lists.attachSchema(new SimpleSchema({
* is the list archived * is the list archived
*/ */
type: Boolean, type: Boolean,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return false; return false;
} }
@ -39,7 +41,8 @@ Lists.attachSchema(new SimpleSchema({
* creation date * creation date
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -62,8 +65,21 @@ Lists.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
if (this.isUpdate) { autoValue() {
if (this.isUpdate || this.isUpsert || this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date(); return new Date();
} else { } else {
this.unset(); this.unset();
@ -107,11 +123,30 @@ Lists.attachSchema(new SimpleSchema({
optional: true, optional: true,
// silver is the default, so it is left out // silver is the default, so it is left out
allowedValues: [ allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple', 'white',
'blue', 'sky', 'lime', 'pink', 'black', 'green',
'peachpuff', 'crimson', 'plum', 'darkgreen', 'yellow',
'slateblue', 'magenta', 'gold', 'navy', 'gray', 'orange',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', 'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
], ],
}, },
type: { type: {
@ -121,7 +156,8 @@ Lists.attachSchema(new SimpleSchema({
type: String, type: String,
defaultValue: 'list', defaultValue: 'list',
}, },
})); })
);
Lists.allow({ Lists.allow({
insert(userId, doc) { insert(userId, doc) {
@ -172,10 +208,8 @@ Lists.helpers({
listId: this._id, listId: this._id,
archived: false, archived: false,
}; };
if (swimlaneId) if (swimlaneId) selector.swimlaneId = swimlaneId;
selector.swimlaneId = swimlaneId; return Cards.find(Filter.mongoSelector(selector), { sort: ['sort'] });
return Cards.find(Filter.mongoSelector(selector),
{ sort: ['sort'] });
}, },
cardsUnfiltered(swimlaneId) { cardsUnfiltered(swimlaneId) {
@ -183,10 +217,8 @@ Lists.helpers({
listId: this._id, listId: this._id,
archived: false, archived: false,
}; };
if (swimlaneId) if (swimlaneId) selector.swimlaneId = swimlaneId;
selector.swimlaneId = swimlaneId; return Cards.find(selector, { sort: ['sort'] });
return Cards.find(selector,
{ sort: ['sort'] });
}, },
allCards() { allCards() {
@ -197,11 +229,12 @@ Lists.helpers({
return Boards.findOne(this.boardId); return Boards.findOne(this.boardId);
}, },
getWipLimit(option){ getWipLimit(option) {
const list = Lists.findOne({ _id: this._id }); const list = Lists.findOne({ _id: this._id });
if(!list.wipLimit) { // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set if (!list.wipLimit) {
// Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
return 0; return 0;
} else if(!option) { } else if (!option) {
return list.wipLimit; return list.wipLimit;
} else { } else {
return list.wipLimit[option] ? list.wipLimit[option] : 0; // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set return list.wipLimit[option] ? list.wipLimit[option] : 0; // Necessary check to avoid exceptions for the case where the doc doesn't have the wipLimit field yet set
@ -209,8 +242,7 @@ Lists.helpers({
}, },
colorClass() { colorClass() {
if (this.color) if (this.color) return this.color;
return this.color;
return ''; return '';
}, },
@ -219,7 +251,7 @@ Lists.helpers({
}, },
remove() { remove() {
Lists.remove({ _id: this._id}); Lists.remove({ _id: this._id });
}, },
}); });
@ -271,10 +303,10 @@ Lists.mutations({
}); });
Meteor.methods({ Meteor.methods({
applyWipLimit(listId, limit){ applyWipLimit(listId, limit) {
check(listId, String); check(listId, String);
check(limit, Number); check(limit, Number);
if(limit === 0){ if (limit === 0) {
limit = 1; limit = 1;
} }
Lists.findOne({ _id: listId }).setWipLimit(limit); Lists.findOne({ _id: listId }).setWipLimit(limit);
@ -283,7 +315,7 @@ Meteor.methods({
enableWipLimit(listId) { enableWipLimit(listId) {
check(listId, String); check(listId, String);
const list = Lists.findOne({ _id: listId }); const list = Lists.findOne({ _id: listId });
if(list.getWipLimit('value') === 0){ if (list.getWipLimit('value') === 0) {
list.setWipLimit(1); list.setWipLimit(1);
} }
list.toggleWipLimit(!list.getWipLimit('enabled')); list.toggleWipLimit(!list.getWipLimit('enabled'));
@ -300,6 +332,7 @@ Lists.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Lists._collection._ensureIndex({ modifiedAt: -1 });
Lists._collection._ensureIndex({ boardId: 1 }); Lists._collection._ensureIndex({ boardId: 1 });
}); });
@ -313,6 +346,11 @@ if (Meteor.isServer) {
}); });
}); });
Lists.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Lists.before.remove((userId, doc) => { Lists.before.remove((userId, doc) => {
const cards = Cards.find({ listId: doc._id }); const cards = Cards.find({ listId: doc._id });
if (cards) { if (cards) {
@ -353,22 +391,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
* title: string}] * title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/lists', function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) { data: Lists.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
}; };
}),
});
} }
catch (error) { ),
});
} catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -384,17 +423,23 @@ if (Meteor.isServer) {
* @param {string} listId the List ID * @param {string} listId the List ID
* @return_type Lists * @return_type Lists
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramListId = req.params.listId; const paramListId = req.params.listId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }), data: Lists.findOne({
_id: paramListId,
boardId: paramBoardId,
archived: false,
}),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -410,9 +455,9 @@ if (Meteor.isServer) {
* @param {string} title the title of the List * @param {string} title the title of the List
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/lists', function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId); const board = Boards.findOne(paramBoardId);
const id = Lists.insert({ const id = Lists.insert({
@ -426,8 +471,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -446,9 +490,12 @@ if (Meteor.isServer) {
* @param {string} listId the ID of the list to remove * @param {string} listId the ID of the list to remove
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) { JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function(
req,
res
) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramListId = req.params.listId; const paramListId = req.params.listId;
Lists.remove({ _id: paramListId, boardId: paramBoardId }); Lists.remove({ _id: paramListId, boardId: paramBoardId });
@ -458,13 +505,13 @@ if (Meteor.isServer) {
_id: paramListId, _id: paramListId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); });
} }
export default Lists;

View file

@ -1,6 +1,9 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules'); Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({ Rules.attachSchema(
new SimpleSchema({
title: { title: {
type: String, type: String,
optional: false, optional: false,
@ -17,7 +20,32 @@ Rules.attachSchema(new SimpleSchema({
type: String, type: String,
optional: false, optional: false,
}, },
})); createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
Rules.mutations({ Rules.mutations({
rename(description) { rename(description) {
@ -26,15 +54,14 @@ Rules.mutations({
}); });
Rules.helpers({ Rules.helpers({
getAction(){ getAction() {
return Actions.findOne({_id:this.actionId}); return Actions.findOne({ _id: this.actionId });
}, },
getTrigger(){ getTrigger() {
return Triggers.findOne({_id:this.triggerId}); return Triggers.findOne({ _id: this.triggerId });
}, },
}); });
Rules.allow({ Rules.allow({
insert(userId, doc) { insert(userId, doc) {
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
@ -46,3 +73,16 @@ Rules.allow({
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
}, },
}); });
Rules.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Rules._collection._ensureIndex({ modifiedAt: -1 });
});
}
export default Rules;

View file

@ -1,6 +1,7 @@
Settings = new Mongo.Collection('settings'); Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({ Settings.attachSchema(
new SimpleSchema({
disableRegistration: { disableRegistration: {
type: Boolean, type: Boolean,
}, },
@ -55,13 +56,30 @@ Settings.attachSchema(new SimpleSchema({
createdAt: { createdAt: {
type: Date, type: Date,
denyUpdate: true, denyUpdate: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
}, },
modifiedAt: { modifiedAt: {
type: Date, type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
})); },
})
);
Settings.helpers({ Settings.helpers({
mailUrl () { mailUrl() {
if (!this.mailServer.host) { if (!this.mailServer.host) {
return null; return null;
} }
@ -69,7 +87,9 @@ Settings.helpers({
if (!this.mailServer.username && !this.mailServer.password) { if (!this.mailServer.username && !this.mailServer.password) {
return `${protocol}${this.mailServer.host}:${this.mailServer.port}/`; return `${protocol}${this.mailServer.host}:${this.mailServer.port}/`;
} }
return `${protocol}${this.mailServer.username}:${encodeURIComponent(this.mailServer.password)}@${this.mailServer.host}:${this.mailServer.port}/`; return `${protocol}${this.mailServer.username}:${encodeURIComponent(
this.mailServer.password
)}@${this.mailServer.host}:${this.mailServer.port}/`;
}, },
}); });
Settings.allow({ Settings.allow({
@ -86,50 +106,75 @@ Settings.before.update((userId, doc, fieldNames, modifier) => {
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Settings._collection._ensureIndex({ modifiedAt: -1 });
const setting = Settings.findOne({}); const setting = Settings.findOne({});
if(!setting){ if (!setting) {
const now = new Date(); const now = new Date();
const domain = process.env.ROOT_URL.match(/\/\/(?:www\.)?(.*)?(?:\/)?/)[1]; const domain = process.env.ROOT_URL.match(
/\/\/(?:www\.)?(.*)?(?:\/)?/
)[1];
const from = `Boards Support <support@${domain}>`; const from = `Boards Support <support@${domain}>`;
const defaultSetting = {disableRegistration: false, mailServer: { const defaultSetting = {
username: '', password: '', host: '', port: '', enableTLS: false, from, disableRegistration: false,
}, createdAt: now, modifiedAt: now, displayAuthenticationMethod: true, mailServer: {
defaultAuthenticationMethod: 'password'}; username: '',
password: '',
host: '',
port: '',
enableTLS: false,
from,
},
createdAt: now,
modifiedAt: now,
displayAuthenticationMethod: true,
defaultAuthenticationMethod: 'password',
};
Settings.insert(defaultSetting); Settings.insert(defaultSetting);
} }
const newSetting = Settings.findOne(); const newSetting = Settings.findOne();
if (!process.env.MAIL_URL && newSetting.mailUrl()) if (!process.env.MAIL_URL && newSetting.mailUrl())
process.env.MAIL_URL = newSetting.mailUrl(); process.env.MAIL_URL = newSetting.mailUrl();
Accounts.emailTemplates.from = process.env.MAIL_FROM ? process.env.MAIL_FROM : newSetting.mailServer.from; Accounts.emailTemplates.from = process.env.MAIL_FROM
? process.env.MAIL_FROM
: newSetting.mailServer.from;
}); });
Settings.after.update((userId, doc, fieldNames) => { Settings.after.update((userId, doc, fieldNames) => {
// assign new values to mail-from & MAIL_URL in environment // assign new values to mail-from & MAIL_URL in environment
if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) { if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) {
const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://'; const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://';
if (!doc.mailServer.username && !doc.mailServer.password) { if (!doc.mailServer.username && !doc.mailServer.password) {
process.env.MAIL_URL = `${protocol}${doc.mailServer.host}:${doc.mailServer.port}/`; process.env.MAIL_URL = `${protocol}${doc.mailServer.host}:${
doc.mailServer.port
}/`;
} else { } else {
process.env.MAIL_URL = `${protocol}${doc.mailServer.username}:${encodeURIComponent(doc.mailServer.password)}@${doc.mailServer.host}:${doc.mailServer.port}/`; process.env.MAIL_URL = `${protocol}${
doc.mailServer.username
}:${encodeURIComponent(doc.mailServer.password)}@${
doc.mailServer.host
}:${doc.mailServer.port}/`;
} }
Accounts.emailTemplates.from = doc.mailServer.from; Accounts.emailTemplates.from = doc.mailServer.from;
} }
}); });
function getRandomNum (min, max) { function getRandomNum(min, max) {
const range = max - min; const range = max - min;
const rand = Math.random(); const rand = Math.random();
return (min + Math.round(rand * range)); return min + Math.round(rand * range);
} }
function getEnvVar(name){ function getEnvVar(name) {
const value = process.env[name]; const value = process.env[name];
if (value){ if (value) {
return value; return value;
} }
throw new Meteor.Error(['var-not-exist', `The environment variable ${name} does not exist`]); throw new Meteor.Error([
'var-not-exist',
`The environment variable ${name} does not exist`,
]);
} }
function sendInvitationEmail (_id){ function sendInvitationEmail(_id) {
const icode = InvitationCodes.findOne(_id); const icode = InvitationCodes.findOne(_id);
const author = Users.findOne(Meteor.userId()); const author = Users.findOne(Meteor.userId());
try { try {
@ -172,30 +217,47 @@ if (Meteor.isServer) {
check(boards, [String]); check(boards, [String]);
const user = Users.findOne(Meteor.userId()); const user = Users.findOne(Meteor.userId());
if(!user.isAdmin){ if (!user.isAdmin) {
throw new Meteor.Error('not-allowed'); throw new Meteor.Error('not-allowed');
} }
emails.forEach((email) => { emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email)) { if (email && SimpleSchema.RegEx.Email.test(email)) {
// Checks if the email is already link to an account. // Checks if the email is already link to an account.
const userExist = Users.findOne({email}); const userExist = Users.findOne({ email });
if (userExist){ if (userExist) {
throw new Meteor.Error('user-exist', `The user with the email ${email} has already an account.`); throw new Meteor.Error(
'user-exist',
`The user with the email ${email} has already an account.`
);
} }
// Checks if the email is already link to an invitation. // Checks if the email is already link to an invitation.
const invitation = InvitationCodes.findOne({email}); const invitation = InvitationCodes.findOne({ email });
if (invitation){ if (invitation) {
InvitationCodes.update(invitation, {$set : {boardsToBeInvited: boards}}); InvitationCodes.update(invitation, {
$set: { boardsToBeInvited: boards },
});
sendInvitationEmail(invitation._id); sendInvitationEmail(invitation._id);
}else { } else {
const code = getRandomNum(100000, 999999); const code = getRandomNum(100000, 999999);
InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){ InvitationCodes.insert(
{
code,
email,
boardsToBeInvited: boards,
createdAt: new Date(),
authorId: Meteor.userId(),
},
function(err, _id) {
if (!err && _id) { if (!err && _id) {
sendInvitationEmail(_id); sendInvitationEmail(_id);
} else { } else {
throw new Meteor.Error('invitation-generated-fail', err.message); throw new Meteor.Error(
'invitation-generated-fail',
err.message
);
} }
}); }
);
} }
} }
}); });
@ -215,11 +277,15 @@ if (Meteor.isServer) {
Email.send({ Email.send({
to: user.emails[0].address, to: user.emails[0].address,
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-smtp-test-subject', {lng: lang}), subject: TAPi18n.__('email-smtp-test-subject', { lng: lang }),
text: TAPi18n.__('email-smtp-test-text', {lng: lang}), text: TAPi18n.__('email-smtp-test-text', { lng: lang }),
}); });
} catch ({message}) { } catch ({ message }) {
throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message); throw new Meteor.Error(
'email-fail',
`${TAPi18n.__('email-fail-text', { lng: lang })}: ${message}`,
message
);
} }
return { return {
message: 'email-sent', message: 'email-sent',
@ -227,7 +293,7 @@ if (Meteor.isServer) {
}; };
}, },
getCustomUI(){ getCustomUI() {
const setting = Settings.findOne({}); const setting = Settings.findOne({});
if (!setting.productName) { if (!setting.productName) {
return { return {
@ -240,7 +306,7 @@ if (Meteor.isServer) {
} }
}, },
getMatomoConf(){ getMatomoConf() {
return { return {
address: getEnvVar('MATOMO_ADDRESS'), address: getEnvVar('MATOMO_ADDRESS'),
siteId: getEnvVar('MATOMO_SITE_ID'), siteId: getEnvVar('MATOMO_SITE_ID'),
@ -275,3 +341,5 @@ if (Meteor.isServer) {
}, },
}); });
} }
export default Settings;

View file

@ -3,7 +3,8 @@ Swimlanes = new Mongo.Collection('swimlanes');
/** /**
* A swimlane is an line in the kaban board. * A swimlane is an line in the kaban board.
*/ */
Swimlanes.attachSchema(new SimpleSchema({ Swimlanes.attachSchema(
new SimpleSchema({
title: { title: {
/** /**
* the title of the swimlane * the title of the swimlane
@ -15,7 +16,8 @@ Swimlanes.attachSchema(new SimpleSchema({
* is the swimlane archived? * is the swimlane archived?
*/ */
type: Boolean, type: Boolean,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return false; return false;
} }
@ -32,7 +34,8 @@ Swimlanes.attachSchema(new SimpleSchema({
* creation date of the swimlane * creation date of the swimlane
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -57,11 +60,30 @@ Swimlanes.attachSchema(new SimpleSchema({
optional: true, optional: true,
// silver is the default, so it is left out // silver is the default, so it is left out
allowedValues: [ allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple', 'white',
'blue', 'sky', 'lime', 'pink', 'black', 'green',
'peachpuff', 'crimson', 'plum', 'darkgreen', 'yellow',
'slateblue', 'magenta', 'gold', 'navy', 'gray', 'orange',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', 'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
], ],
}, },
updatedAt: { updatedAt: {
@ -70,8 +92,21 @@ Swimlanes.attachSchema(new SimpleSchema({
*/ */
type: Date, type: Date,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
if (this.isUpdate) { autoValue() {
if (this.isUpdate || this.isUpsert || this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date(); return new Date();
} else { } else {
this.unset(); this.unset();
@ -85,7 +120,8 @@ Swimlanes.attachSchema(new SimpleSchema({
type: String, type: String,
defaultValue: 'swimlane', defaultValue: 'swimlane',
}, },
})); })
);
Swimlanes.allow({ Swimlanes.allow({
insert(userId, doc) { insert(userId, doc) {
@ -109,7 +145,7 @@ Swimlanes.helpers({
const _id = Swimlanes.insert(this); const _id = Swimlanes.insert(this);
const query = { const query = {
swimlaneId: {$in: [oldId, '']}, swimlaneId: { $in: [oldId, ''] },
archived: false, archived: false,
}; };
if (oldBoardId) { if (oldBoardId) {
@ -126,18 +162,24 @@ Swimlanes.helpers({
}, },
cards() { cards() {
return Cards.find(Filter.mongoSelector({ return Cards.find(
Filter.mongoSelector({
swimlaneId: this._id, swimlaneId: this._id,
archived: false, archived: false,
}), { sort: ['sort'] }); }),
{ sort: ['sort'] }
);
}, },
lists() { lists() {
return Lists.find({ return Lists.find(
{
boardId: this.boardId, boardId: this.boardId,
swimlaneId: {$in: [this._id, '']}, swimlaneId: { $in: [this._id, ''] },
archived: false, archived: false,
}, { sort: ['sort'] }); },
{ sort: ['sort'] }
);
}, },
myLists() { myLists() {
@ -153,8 +195,7 @@ Swimlanes.helpers({
}, },
colorClass() { colorClass() {
if (this.color) if (this.color) return this.color;
return this.color;
return ''; return '';
}, },
@ -182,7 +223,7 @@ Swimlanes.helpers({
}, },
remove() { remove() {
Swimlanes.remove({ _id: this._id}); Swimlanes.remove({ _id: this._id });
}, },
}); });
@ -221,10 +262,16 @@ Swimlanes.mutations({
}, },
}); });
Swimlanes.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
Swimlanes.hookOptions.after.update = { fetchPrevious: false }; Swimlanes.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
Swimlanes._collection._ensureIndex({ modifiedAt: -1 });
Swimlanes._collection._ensureIndex({ boardId: 1 }); Swimlanes._collection._ensureIndex({ boardId: 1 });
}); });
@ -239,18 +286,21 @@ if (Meteor.isServer) {
}); });
Swimlanes.before.remove(function(userId, doc) { Swimlanes.before.remove(function(userId, doc) {
const lists = Lists.find({ const lists = Lists.find(
{
boardId: doc.boardId, boardId: doc.boardId,
swimlaneId: {$in: [doc._id, '']}, swimlaneId: { $in: [doc._id, ''] },
archived: false, archived: false,
}, { sort: ['sort'] }); },
{ sort: ['sort'] }
);
if (lists.count() < 2) { if (lists.count() < 2) {
lists.forEach((list) => { lists.forEach((list) => {
list.remove(); list.remove();
}); });
} else { } else {
Cards.remove({swimlaneId: doc._id}); Cards.remove({ swimlaneId: doc._id });
} }
Activities.insert({ Activities.insert({
@ -287,22 +337,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string, * @return_type [{_id: string,
* title: string}] * title: string}]
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) { data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return { return {
_id: doc._id, _id: doc._id,
title: doc.title, title: doc.title,
}; };
}),
});
} }
catch (error) { ),
});
} catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -319,17 +370,23 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane * @param {string} swimlaneId the ID of the swimlane
* @return_type Swimlanes * @return_type Swimlanes
*/ */
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) { JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes/:swimlaneId', function(
req,
res
) {
try { try {
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId; const paramSwimlaneId = req.params.swimlaneId;
Authentication.checkBoardAccess( req.userId, paramBoardId); Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }), data: Swimlanes.findOne({
_id: paramSwimlaneId,
boardId: paramBoardId,
archived: false,
}),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -346,9 +403,9 @@ if (Meteor.isServer) {
* @param {string} title the new title of the swimlane * @param {string} title the new title of the swimlane
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId); const board = Boards.findOne(paramBoardId);
const id = Swimlanes.insert({ const id = Swimlanes.insert({
@ -362,8 +419,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -382,9 +438,12 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane * @param {string} swimlaneId the ID of the swimlane
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) { JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/swimlanes/:swimlaneId',
function(req, res) {
try { try {
Authentication.checkUserId( req.userId); Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId; const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId; const paramSwimlaneId = req.params.swimlaneId;
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId }); Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
@ -394,13 +453,14 @@ if (Meteor.isServer) {
_id: paramSwimlaneId, _id: paramSwimlaneId,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
} }
export default Swimlanes;

View file

@ -1,3 +1,5 @@
import { Meteor } from 'meteor/meteor';
Triggers = new Mongo.Collection('triggers'); Triggers = new Mongo.Collection('triggers');
Triggers.mutations({ Triggers.mutations({
@ -23,7 +25,6 @@ Triggers.allow({
}); });
Triggers.helpers({ Triggers.helpers({
description() { description() {
return this.desc; return this.desc;
}, },
@ -56,3 +57,16 @@ Triggers.helpers({
return cardLabels; return cardLabels;
}, },
}); });
Triggers.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
Meteor.startup(() => {
Triggers._collection._ensureIndex({ modifiedAt: -1 });
});
}
export default Triggers;

View file

@ -2,7 +2,8 @@
// `UnsavedEdits` API on the client. // `UnsavedEdits` API on the client.
UnsavedEditCollection = new Mongo.Collection('unsaved-edits'); UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
UnsavedEditCollection.attachSchema(new SimpleSchema({ UnsavedEditCollection.attachSchema(
new SimpleSchema({
fieldName: { fieldName: {
type: String, type: String,
}, },
@ -14,19 +15,53 @@ UnsavedEditCollection.attachSchema(new SimpleSchema({
}, },
userId: { userId: {
type: String, type: String,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return this.userId; return this.userId;
} }
}, },
}, },
})); createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
})
);
UnsavedEditCollection.before.update(
(userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
}
);
if (Meteor.isServer) { if (Meteor.isServer) {
function isAuthor(userId, doc, fieldNames = []) { function isAuthor(userId, doc, fieldNames = []) {
return userId === doc.userId && fieldNames.indexOf('userId') === -1; return userId === doc.userId && fieldNames.indexOf('userId') === -1;
} }
Meteor.startup(() => { Meteor.startup(() => {
UnsavedEditCollection._collection._ensureIndex({ modifiedAt: -1 });
UnsavedEditCollection._collection._ensureIndex({ userId: 1 }); UnsavedEditCollection._collection._ensureIndex({ userId: 1 });
}); });
UnsavedEditCollection.allow({ UnsavedEditCollection.allow({
@ -36,3 +71,5 @@ if (Meteor.isServer) {
fetch: ['userId'], fetch: ['userId'],
}); });
} }
export default UnsavedEditCollection;

View file

@ -1,20 +1,22 @@
// 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.
const isSandstorm = Meteor.settings && Meteor.settings.public && const isSandstorm =
Meteor.settings.public.sandstorm; Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
Users = Meteor.users; Users = Meteor.users;
/** /**
* A User in wekan * A User in wekan
*/ */
Users.attachSchema(new SimpleSchema({ Users.attachSchema(
new SimpleSchema({
username: { username: {
/** /**
* the username of the user * the username of the user
*/ */
type: String, type: String,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
const name = this.field('profile.fullname'); const name = this.field('profile.fullname');
if (name.isSet) { if (name.isSet) {
@ -48,7 +50,8 @@ Users.attachSchema(new SimpleSchema({
* creation date of the user * creation date of the user
*/ */
type: Date, type: Date,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) { if (this.isInsert) {
return new Date(); return new Date();
} else { } else {
@ -56,13 +59,26 @@ Users.attachSchema(new SimpleSchema({
} }
}, },
}, },
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
profile: { profile: {
/** /**
* profile settings * profile settings
*/ */
type: Object, type: Object,
optional: true, optional: true,
autoValue() { // eslint-disable-line consistent-return // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return { return {
boardView: 'board-view-lists', boardView: 'board-view-lists',
@ -223,7 +239,7 @@ Users.attachSchema(new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
'authenticationMethod': { authenticationMethod: {
/** /**
* authentication method of the user * authentication method of the user
*/ */
@ -231,7 +247,8 @@ Users.attachSchema(new SimpleSchema({
optional: false, optional: false,
defaultValue: 'password', defaultValue: 'password',
}, },
})); })
);
Users.allow({ Users.allow({
update(userId) { update(userId) {
@ -240,7 +257,10 @@ Users.allow({
}, },
remove(userId, doc) { remove(userId, doc) {
const adminsNumber = Users.find({ isAdmin: true }).count(); const adminsNumber = Users.find({ isAdmin: true }).count();
const { isAdmin } = Users.findOne({ _id: userId }, { fields: { 'isAdmin': 1 } }); const { isAdmin } = Users.findOne(
{ _id: userId },
{ fields: { isAdmin: 1 } }
);
// Prevents remove of the only one administrator // Prevents remove of the only one administrator
if (adminsNumber === 1 && isAdmin && userId === doc._id) { if (adminsNumber === 1 && isAdmin && userId === doc._id) {
@ -270,7 +290,9 @@ if (Meteor.isClient) {
isNotNoComments() { isNotNoComments() {
const board = Boards.findOne(Session.get('currentBoard')); const board = Boards.findOne(Session.get('currentBoard'));
return board && board.hasMember(this._id) && !board.hasNoComments(this._id); return (
board && board.hasMember(this._id) && !board.hasNoComments(this._id)
);
}, },
isNoComments() { isNoComments() {
@ -280,7 +302,9 @@ if (Meteor.isClient) {
isNotCommentOnly() { isNotCommentOnly() {
const board = Boards.findOne(Session.get('currentBoard')); const board = Boards.findOne(Session.get('currentBoard'));
return board && board.hasMember(this._id) && !board.hasCommentOnly(this._id); return (
board && board.hasMember(this._id) && !board.hasCommentOnly(this._id)
);
}, },
isCommentOnly() { isCommentOnly() {
@ -301,32 +325,32 @@ Users.helpers({
}, },
starredBoards() { starredBoards() {
const {starredBoards = []} = this.profile || {}; const { starredBoards = [] } = this.profile || {};
return Boards.find({archived: false, _id: {$in: starredBoards}}); return Boards.find({ archived: false, _id: { $in: starredBoards } });
}, },
hasStarred(boardId) { hasStarred(boardId) {
const {starredBoards = []} = this.profile || {}; const { starredBoards = [] } = this.profile || {};
return _.contains(starredBoards, boardId); return _.contains(starredBoards, boardId);
}, },
invitedBoards() { invitedBoards() {
const {invitedBoards = []} = this.profile || {}; const { invitedBoards = [] } = this.profile || {};
return Boards.find({archived: false, _id: {$in: invitedBoards}}); return Boards.find({ archived: false, _id: { $in: invitedBoards } });
}, },
isInvitedTo(boardId) { isInvitedTo(boardId) {
const {invitedBoards = []} = this.profile || {}; const { invitedBoards = [] } = this.profile || {};
return _.contains(invitedBoards, boardId); return _.contains(invitedBoards, boardId);
}, },
hasTag(tag) { hasTag(tag) {
const {tags = []} = this.profile || {}; const { tags = [] } = this.profile || {};
return _.contains(tags, tag); return _.contains(tags, tag);
}, },
hasNotification(activityId) { hasNotification(activityId) {
const {notifications = []} = this.profile || {}; const { notifications = [] } = this.profile || {};
return _.contains(notifications, activityId); return _.contains(notifications, activityId);
}, },
@ -336,20 +360,20 @@ Users.helpers({
}, },
getEmailBuffer() { getEmailBuffer() {
const {emailBuffer = []} = this.profile || {}; const { emailBuffer = [] } = this.profile || {};
return emailBuffer; return emailBuffer;
}, },
getInitials() { getInitials() {
const 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 profile.fullname.split(/\s+/).reduce((memo, word) => { return profile.fullname
.split(/\s+/)
.reduce((memo, word) => {
return memo + word[0]; return memo + word[0];
}, '').toUpperCase(); }, '')
.toUpperCase();
} else { } else {
return this.username[0].toUpperCase(); return this.username[0].toUpperCase();
} }
@ -379,7 +403,7 @@ Users.helpers({
}, },
remove() { remove() {
User.remove({ _id: this._id}); User.remove({ _id: this._id });
}, },
}); });
@ -426,10 +450,8 @@ Users.mutations({
}, },
toggleTag(tag) { toggleTag(tag) {
if (this.hasTag(tag)) if (this.hasTag(tag)) this.removeTag(tag);
this.removeTag(tag); else this.addTag(tag);
else
this.addTag(tag);
}, },
toggleSystem(value = false) { toggleSystem(value = false) {
@ -473,16 +495,16 @@ Users.mutations({
}, },
setAvatarUrl(avatarUrl) { setAvatarUrl(avatarUrl) {
return {$set: {'profile.avatarUrl': avatarUrl}}; return { $set: { 'profile.avatarUrl': avatarUrl } };
}, },
setShowCardsCountAt(limit) { setShowCardsCountAt(limit) {
return {$set: {'profile.showCardsCountAt': limit}}; return { $set: { 'profile.showCardsCountAt': limit } };
}, },
setBoardView(view) { setBoardView(view) {
return { return {
$set : { $set: {
'profile.boardView': view, 'profile.boardView': view,
}, },
}; };
@ -492,11 +514,11 @@ Users.mutations({
Meteor.methods({ Meteor.methods({
setUsername(username, userId) { setUsername(username, userId) {
check(username, String); check(username, String);
const nUsersWithUsername = Users.find({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(userId, {$set: {username}}); Users.update(userId, { $set: { username } });
} }
}, },
toggleSystemMessages() { toggleSystemMessages() {
@ -509,16 +531,21 @@ Meteor.methods({
}, },
setEmail(email, userId) { setEmail(email, userId) {
check(email, String); check(email, String);
const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}}); const existingUser = Users.findOne(
{ 'emails.address': email },
{ fields: { _id: 1 } }
);
if (existingUser) { if (existingUser) {
throw new Meteor.Error('email-already-taken'); throw new Meteor.Error('email-already-taken');
} else { } else {
Users.update(userId, { Users.update(userId, {
$set: { $set: {
emails: [{ emails: [
{
address: email, address: email,
verified: false, verified: false,
}], },
],
}, },
}); });
} }
@ -533,7 +560,7 @@ Meteor.methods({
setPassword(newPassword, userId) { setPassword(newPassword, userId) {
check(userId, String); check(userId, String);
check(newPassword, String); check(newPassword, String);
if(Meteor.user().isAdmin){ if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword); Accounts.setPassword(userId, newPassword);
} }
}, },
@ -548,12 +575,13 @@ if (Meteor.isServer) {
const inviter = Meteor.user(); const inviter = Meteor.user();
const board = Boards.findOne(boardId); const board = Boards.findOne(boardId);
const allowInvite = inviter && const allowInvite =
inviter &&
board && board &&
board.members && board.members &&
_.contains(_.pluck(board.members, 'userId'), inviter._id) && _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
_.where(board.members, {userId: inviter._id})[0].isActive && _.where(board.members, { userId: inviter._id })[0].isActive &&
_.where(board.members, {userId: inviter._id})[0].isAdmin; _.where(board.members, { userId: inviter._id })[0].isAdmin;
if (!allowInvite) throw new Meteor.Error('error-board-notAMember'); if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
this.unblock(); this.unblock();
@ -561,19 +589,21 @@ if (Meteor.isServer) {
const posAt = username.indexOf('@'); const posAt = username.indexOf('@');
let user = null; let user = null;
if (posAt >= 0) { if (posAt >= 0) {
user = Users.findOne({emails: {$elemMatch: {address: username}}}); user = Users.findOne({ emails: { $elemMatch: { address: username } } });
} else { } else {
user = Users.findOne(username) || Users.findOne({username}); user = Users.findOne(username) || Users.findOne({ username });
} }
if (user) { if (user) {
if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf'); if (user._id === inviter._id)
throw new Meteor.Error('error-user-notAllowSelf');
} else { } else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist'); if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
if (Settings.findOne().disableRegistration) throw new Meteor.Error('error-user-notCreated'); if (Settings.findOne().disableRegistration)
throw new Meteor.Error('error-user-notCreated');
// Set in lowercase email before creating account // Set in lowercase email before creating account
const email = username.toLowerCase(); const email = username.toLowerCase();
username = email.substring(0, posAt); username = email.substring(0, posAt);
const newUserId = Accounts.createUser({username, email}); const newUserId = Accounts.createUser({ username, email });
if (!newUserId) throw new Meteor.Error('error-user-notCreated'); if (!newUserId) throw new Meteor.Error('error-user-notCreated');
// assume new user speak same language with inviter // assume new user speak same language with inviter
if (inviter.profile && inviter.profile.language) { if (inviter.profile && inviter.profile.language) {
@ -607,7 +637,7 @@ if (Meteor.isServer) {
} catch (e) { } catch (e) {
throw new Meteor.Error('email-fail', e.message); throw new Meteor.Error('email-fail', e.message);
} }
return {username: user.username, email: user.emails[0].address}; return { username: user.username, email: user.emails[0].address };
}, },
}); });
Accounts.onCreateUser((options, user) => { Accounts.onCreateUser((options, user) => {
@ -621,14 +651,22 @@ if (Meteor.isServer) {
const email = user.services.oidc.email.toLowerCase(); const email = user.services.oidc.email.toLowerCase();
user.username = user.services.oidc.username; user.username = user.services.oidc.username;
user.emails = [{ address: email, verified: true }]; user.emails = [{ address: email, verified: true }];
const initials = user.services.oidc.fullname.match(/\b[a-zA-Z]/g).join('').toUpperCase(); const initials = user.services.oidc.fullname
user.profile = { initials, fullname: user.services.oidc.fullname, boardView: 'board-view-lists' }; .match(/\b[a-zA-Z]/g)
.join('')
.toUpperCase();
user.profile = {
initials,
fullname: user.services.oidc.fullname,
boardView: 'board-view-lists',
};
user.authenticationMethod = 'oauth2'; user.authenticationMethod = 'oauth2';
// see if any existing user has this email address or username, otherwise create new // see if any existing user has this email address or username, otherwise create new
const existingUser = Meteor.users.findOne({$or: [{'emails.address': email}, {'username':user.username}]}); const existingUser = Meteor.users.findOne({
if (!existingUser) $or: [{ 'emails.address': email }, { username: user.username }],
return user; });
if (!existingUser) return user;
// copy across new service info // copy across new service info
const service = _.keys(user.services)[0]; const service = _.keys(user.services)[0];
@ -638,7 +676,7 @@ if (Meteor.isServer) {
existingUser.profile = user.profile; existingUser.profile = user.profile;
existingUser.authenticationMethod = user.authenticationMethod; existingUser.authenticationMethod = user.authenticationMethod;
Meteor.users.remove({_id: existingUser._id}); // remove existing record Meteor.users.remove({ _id: existingUser._id }); // remove existing record
return existingUser; return existingUser;
} }
@ -660,7 +698,10 @@ if (Meteor.isServer) {
} }
if (!options || !options.profile) { if (!options || !options.profile) {
throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required'); throw new Meteor.Error(
'error-invitation-code-blank',
'The invitation code is required'
);
} }
const invitationCode = InvitationCodes.findOne({ const invitationCode = InvitationCodes.findOne({
code: options.profile.invitationcode, code: options.profile.invitationcode,
@ -668,26 +709,41 @@ if (Meteor.isServer) {
valid: true, valid: true,
}); });
if (!invitationCode) { if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist'); throw new Meteor.Error(
'error-invitation-code-not-exist',
'The invitation code doesn\'t exist'
);
} else { } else {
user.profile = {icode: options.profile.invitationcode}; user.profile = { icode: options.profile.invitationcode };
user.profile.boardView = 'board-view-lists'; user.profile.boardView = 'board-view-lists';
// Deletes the invitation code after the user was created successfully. // Deletes the invitation code after the user was created successfully.
setTimeout(Meteor.bindEnvironment(() => { setTimeout(
InvitationCodes.remove({'_id': invitationCode._id}); Meteor.bindEnvironment(() => {
}), 200); InvitationCodes.remove({ _id: invitationCode._id });
}),
200
);
return user; return user;
} }
}); });
} }
Users.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) { if (Meteor.isServer) {
// Let mongoDB ensure username unicity // Let mongoDB ensure username unicity
Meteor.startup(() => { Meteor.startup(() => {
Users._collection._ensureIndex({ Users._collection._ensureIndex({ modifiedAt: -1 });
Users._collection._ensureIndex(
{
username: 1, username: 1,
}, {unique: true}); },
{ unique: true }
);
}); });
// OLD WAY THIS CODE DID WORK: When user is last admin of board, // OLD WAY THIS CODE DID WORK: When user is last admin of board,
@ -712,11 +768,10 @@ if (Meteor.isServer) {
// counter. // counter.
// We need to run this code on the server only, otherwise the incrementation // We need to run this code on the server only, otherwise the incrementation
// will be done twice. // will be done twice.
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
@ -732,7 +787,7 @@ if (Meteor.isServer) {
// direction and then in the other. // direction and then in the other.
function incrementBoards(boardsIds, inc) { function incrementBoards(boardsIds, inc) {
boardsIds.forEach((boardId) => { boardsIds.forEach((boardId) => {
Boards.update(boardId, {$inc: {stars: inc}}); Boards.update(boardId, { $inc: { stars: inc } });
}); });
} }
@ -773,57 +828,76 @@ if (Meteor.isServer) {
}); });
*/ */
Boards.insert({ Boards.insert(
{
title: TAPi18n.__('templates'), title: TAPi18n.__('templates'),
permission: 'private', permission: 'private',
type: 'template-container', type: 'template-container',
}, fakeUser, (err, boardId) => { },
fakeUser,
(err, boardId) => {
// Insert the reference to our templates board // Insert the reference to our templates board
Users.update(fakeUserId.get(), {$set: {'profile.templatesBoardId': boardId}}); Users.update(fakeUserId.get(), {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the card templates swimlane // Insert the card templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('card-templates-swimlane'), title: TAPi18n.__('card-templates-swimlane'),
boardId, boardId,
sort: 1, sort: 1,
type: 'template-container', type: 'template-container',
}, fakeUser, (err, swimlaneId) => { },
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane // Insert the reference to out card templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); Users.update(fakeUserId.get(), {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the list templates swimlane // Insert the list templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'), title: TAPi18n.__('list-templates-swimlane'),
boardId, boardId,
sort: 2, sort: 2,
type: 'template-container', type: 'template-container',
}, fakeUser, (err, swimlaneId) => { },
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane // Insert the reference to out list templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); Users.update(fakeUserId.get(), {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the board templates swimlane // Insert the board templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'), title: TAPi18n.__('board-templates-swimlane'),
boardId, boardId,
sort: 3, sort: 3,
type: 'template-container', type: 'template-container',
}, fakeUser, (err, swimlaneId) => { },
fakeUser,
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane // Insert the reference to out board templates swimlane
Users.update(fakeUserId.get(), {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); Users.update(fakeUserId.get(), {
}); $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
}
);
}); });
}); });
} }
Users.after.insert((userId, doc) => { Users.after.insert((userId, doc) => {
if (doc.createdThroughApi) { if (doc.createdThroughApi) {
// The admin user should be able to create a user despite disabling registration because // The admin user should be able to create a user despite disabling registration because
// it is two different things (registration and creation). // it is two different things (registration and creation).
@ -831,7 +905,7 @@ if (Meteor.isServer) {
// the disableRegistration check. // the disableRegistration check.
// Issue : https://github.com/wekan/wekan/issues/1232 // Issue : https://github.com/wekan/wekan/issues/1232
// PR : https://github.com/wekan/wekan/pull/1251 // PR : https://github.com/wekan/wekan/pull/1251
Users.update(doc._id, {$set: {createdThroughApi: ''}}); Users.update(doc._id, { $set: { createdThroughApi: '' } });
return; return;
} }
@ -840,7 +914,10 @@ if (Meteor.isServer) {
// If ldap, bypass the inviation code if the self registration isn't allowed. // If ldap, bypass the inviation code if the self registration isn't allowed.
// TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type // TODO : pay attention if ldap field in the user model change to another content ex : ldap field to connection_type
if (doc.authenticationMethod !== 'ldap' && disableRegistration) { if (doc.authenticationMethod !== 'ldap' && disableRegistration) {
const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true}); const invitationCode = InvitationCodes.findOne({
code: doc.profile.icode,
valid: true,
});
if (!invitationCode) { if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist'); throw new Meteor.Error('error-invitation-code-not-exist');
} else { } else {
@ -852,8 +929,8 @@ if (Meteor.isServer) {
doc.profile = {}; doc.profile = {};
} }
doc.profile.invitedBoards = invitationCode.boardsToBeInvited; doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
Users.update(doc._id, {$set: {profile: doc.profile}}); Users.update(doc._id, { $set: { profile: doc.profile } });
InvitationCodes.update(invitationCode._id, {$set: {valid: false}}); InvitationCodes.update(invitationCode._id, { $set: { valid: false } });
} }
} }
}); });
@ -862,13 +939,12 @@ if (Meteor.isServer) {
// USERS REST API // USERS REST API
if (Meteor.isServer) { if (Meteor.isServer) {
// Middleware which checks that API is enabled. // Middleware which checks that API is enabled.
JsonRoutes.Middleware.use(function (req, res, next) { JsonRoutes.Middleware.use(function(req, res, next) {
const api = req.url.search('api'); const api = req.url.search('api');
if (api === 1 && process.env.WITH_API === 'true' || api === -1){ if ((api === 1 && process.env.WITH_API === 'true') || api === -1) {
return next(); return next();
} } else {
else { res.writeHead(301, { Location: '/' });
res.writeHead(301, {Location: '/'});
return res.end(); return res.end();
} }
}); });
@ -882,14 +958,13 @@ if (Meteor.isServer) {
JsonRoutes.add('GET', '/api/user', function(req, res) { JsonRoutes.add('GET', '/api/user', function(req, res) {
try { try {
Authentication.checkLoggedIn(req.userId); Authentication.checkLoggedIn(req.userId);
const data = Meteor.users.findOne({ _id: req.userId}); const data = Meteor.users.findOne({ _id: req.userId });
delete data.services; delete data.services;
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data, data,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -906,17 +981,16 @@ if (Meteor.isServer) {
* @return_type [{ _id: string, * @return_type [{ _id: string,
* username: string}] * username: string}]
*/ */
JsonRoutes.add('GET', '/api/users', function (req, res) { JsonRoutes.add('GET', '/api/users', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: Meteor.users.find({}).map(function (doc) { data: Meteor.users.find({}).map(function(doc) {
return { _id: doc._id, username: doc.username }; return { _id: doc._id, username: doc.username };
}), }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -934,7 +1008,7 @@ if (Meteor.isServer) {
* @param {string} userId the user ID * @param {string} userId the user ID
* @return_type Users * @return_type Users
*/ */
JsonRoutes.add('GET', '/api/users/:userId', function (req, res) { JsonRoutes.add('GET', '/api/users/:userId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.userId; const id = req.params.userId;
@ -942,8 +1016,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: Meteor.users.findOne({ _id: id }), data: Meteor.users.findOne({ _id: id }),
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -968,7 +1041,7 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
* title: string} * title: string}
*/ */
JsonRoutes.add('PUT', '/api/users/:userId', function (req, res) { JsonRoutes.add('PUT', '/api/users/:userId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.userId; const id = req.params.userId;
@ -990,8 +1063,16 @@ if (Meteor.isServer) {
}; };
}); });
} else { } else {
if ((action === 'disableLogin') && (id !== req.userId)) { if (action === 'disableLogin' && id !== req.userId) {
Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } }); Users.update(
{ _id: id },
{
$set: {
loginDisabled: true,
'services.resume.loginTokens': '',
},
}
);
} else if (action === 'enableLogin') { } else if (action === 'enableLogin') {
Users.update({ _id: id }, { $set: { loginDisabled: '' } }); Users.update({ _id: id }, { $set: { loginDisabled: '' } });
} }
@ -1002,8 +1083,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data, data,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1030,13 +1110,16 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
* title: string} * title: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function (req, res) { JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/add', function(
req,
res
) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const userId = req.params.userId; const userId = req.params.userId;
const boardId = req.params.boardId; const boardId = req.params.boardId;
const action = req.body.action; const action = req.body.action;
const {isAdmin, isNoComments, isCommentOnly} = req.body; const { isAdmin, isNoComments, isCommentOnly } = req.body;
let data = Meteor.users.findOne({ _id: userId }); let data = Meteor.users.findOne({ _id: userId });
if (data !== undefined) { if (data !== undefined) {
if (action === 'add') { if (action === 'add') {
@ -1045,10 +1128,16 @@ if (Meteor.isServer) {
}).map(function(board) { }).map(function(board) {
if (!board.hasMember(userId)) { if (!board.hasMember(userId)) {
board.addMember(userId); board.addMember(userId);
function isTrue(data){ function isTrue(data) {
return data.toLowerCase() === 'true'; return data.toLowerCase() === 'true';
} }
board.setMemberPermission(userId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), userId); board.setMemberPermission(
userId,
isTrue(isAdmin),
isTrue(isNoComments),
isTrue(isCommentOnly),
userId
);
} }
return { return {
_id: board._id, _id: board._id,
@ -1061,8 +1150,7 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: query, data: query,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1084,7 +1172,10 @@ if (Meteor.isServer) {
* @return_type {_id: string, * @return_type {_id: string,
* title: string} * title: string}
*/ */
JsonRoutes.add('POST', '/api/boards/:boardId/members/:userId/remove', function (req, res) { JsonRoutes.add(
'POST',
'/api/boards/:boardId/members/:userId/remove',
function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const userId = req.params.userId; const userId = req.params.userId;
@ -1110,14 +1201,14 @@ if (Meteor.isServer) {
code: 200, code: 200,
data: query, data: query,
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
}); });
} }
}); }
);
/** /**
* @operation new_user * @operation new_user
@ -1131,7 +1222,7 @@ if (Meteor.isServer) {
* @param {string} password the password of the new user * @param {string} password the password of the new user
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('POST', '/api/users/', function (req, res) { JsonRoutes.add('POST', '/api/users/', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = Accounts.createUser({ const id = Accounts.createUser({
@ -1146,8 +1237,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1165,7 +1255,7 @@ if (Meteor.isServer) {
* @param {string} userId the ID of the user to delete * @param {string} userId the ID of the user to delete
* @return_type {_id: string} * @return_type {_id: string}
*/ */
JsonRoutes.add('DELETE', '/api/users/:userId', function (req, res) { JsonRoutes.add('DELETE', '/api/users/:userId', function(req, res) {
try { try {
Authentication.checkUserId(req.userId); Authentication.checkUserId(req.userId);
const id = req.params.userId; const id = req.params.userId;
@ -1176,8 +1266,7 @@ if (Meteor.isServer) {
_id: id, _id: id,
}, },
}); });
} } catch (error) {
catch (error) {
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: error, data: error,
@ -1185,3 +1274,5 @@ if (Meteor.isServer) {
} }
}); });
} }
export default Users;

View file

@ -4,9 +4,29 @@
"description": "Open-Source kanban", "description": "Open-Source kanban",
"private": true, "private": true,
"scripts": { "scripts": {
"lint": "eslint --ignore-pattern 'packages/*' .", "lint": "eslint --cache --ext .js --ignore-path .eslintignore .",
"lint:eslint:fix": "eslint --ext .js --ignore-path .eslintignore --fix .",
"lint:staged": "lint-staged",
"prettify": "prettier --write '**/*.js' '**/*.jsx'",
"test": "npm run --silent lint" "test": "npm run --silent lint"
}, },
"lint-staged": {
"*.js": [
"meteor npm run prettify",
"meteor npm run lint:eslint:fix",
"git add --force"
],
"*.jsx": [
"meteor npm run prettify",
"meteor npm run lint:eslint:fix",
"git add --force"
],
"*.json": [
"prettier --write",
"git add --force"
]
},
"pre-commit": "lint:staged",
"eslintConfig": { "eslintConfig": {
"extends": "@meteorjs/eslint-config-meteor" "extends": "@meteorjs/eslint-config-meteor"
}, },
@ -20,7 +40,17 @@
}, },
"homepage": "https://wekan.github.io", "homepage": "https://wekan.github.io",
"devDependencies": { "devDependencies": {
"eslint": "^5.16.0" "eslint": "^5.16.0",
"eslint-config-meteor": "0.0.9",
"eslint-config-prettier": "^3.6.0",
"eslint-import-resolver-meteor": "^0.4.0",
"eslint-plugin-import": "^2.18.0",
"eslint-plugin-meteor": "^4.2.2",
"eslint-plugin-prettier": "^3.1.0",
"lint-staged": "^7.3.0",
"pre-commit": "^1.2.2",
"prettier": "^1.18.2",
"prettier-eslint": "^8.8.2"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.4.3", "@babel/runtime": "^7.4.3",

View file

@ -1,3 +1,23 @@
import AccountSettings from '../models/accountSettings';
import Actions from '../models/actions';
import Activities from '../models/activities';
import Announcements from '../models/announcements';
import Boards from '../models/boards';
import CardComments from '../models/cardComments';
import Cards from '../models/cards';
import ChecklistItems from '../models/checklistItems';
import Checklists from '../models/checklists';
import CustomFields from '../models/customFields';
import Integrations from '../models/integrations';
import InvitationCodes from '../models/invitationCodes';
import Lists from '../models/lists';
import Rules from '../models/rules';
import Settings from '../models/settings';
import Swimlanes from '../models/swimlanes';
import Triggers from '../models/triggers';
import UnsavedEdits from '../models/unsavedEdits';
import Users from '../models/users';
// Anytime you change the schema of one of the collection in a non-backward // Anytime you change the schema of one of the collection in a non-backward
// compatible way you have to write a migration in this file using the following // compatible way you have to write a migration in this file using the following
// API: // API:
@ -28,18 +48,22 @@ const noValidateMulti = { ...noValidate, multi: true };
Migrations.add('board-background-color', () => { Migrations.add('board-background-color', () => {
const 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', () => { Migrations.add('lowercase-board-permission', () => {
@ -57,12 +81,16 @@ Migrations.add('change-attachments-type-for-non-images', () => {
const newTypeForNonImage = 'application/octet-stream'; const newTypeForNonImage = 'application/octet-stream';
Attachments.find().forEach((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
);
} }
}); });
}); });
@ -71,10 +99,10 @@ Migrations.add('card-covers', () => {
Cards.find().forEach((card) => { Cards.find().forEach((card) => {
const 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);
} }
}); });
Attachments.update({}, {$unset: {cover: ''}}, noValidateMulti); Attachments.update({}, { $unset: { cover: '' } }, noValidateMulti);
}); });
Migrations.add('use-css-class-for-boards-colors', () => { Migrations.add('use-css-class-for-boards-colors', () => {
@ -89,26 +117,31 @@ Migrations.add('use-css-class-for-boards-colors', () => {
Boards.find().forEach((board) => { Boards.find().forEach((board) => {
const oldBoardColor = board.background.color; const oldBoardColor = board.background.color;
const 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', () => { Migrations.add('denormalize-star-number-per-board', () => {
Boards.find().forEach((board) => { Boards.find().forEach((board) => {
const 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', () => { Migrations.add('add-member-isactive-field', () => {
Boards.find({}, {fields: {members: 1}}).forEach((board) => { Boards.find({}, { fields: { members: 1 } }).forEach((board) => {
const 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();
@ -127,7 +160,7 @@ Migrations.add('add-member-isactive-field', () => {
isActive: false, isActive: false,
}); });
}); });
Boards.update(board._id, {$set: {members: newMemberSet}}, noValidate); Boards.update(board._id, { $set: { members: newMemberSet } }, noValidate);
}); });
}); });
@ -184,7 +217,7 @@ Migrations.add('add-checklist-items', () => {
// Create new items // Create new items
_.sortBy(checklist.items, 'sort').forEach((item, index) => { _.sortBy(checklist.items, 'sort').forEach((item, index) => {
ChecklistItems.direct.insert({ ChecklistItems.direct.insert({
title: (item.title ? item.title : 'Checklist'), title: item.title ? item.title : 'Checklist',
sort: index, sort: index,
isFinished: item.isFinished, isFinished: item.isFinished,
checklistId: checklist._id, checklistId: checklist._id,
@ -193,8 +226,9 @@ Migrations.add('add-checklist-items', () => {
}); });
// Delete old ones // Delete old ones
Checklists.direct.update({ _id: checklist._id }, Checklists.direct.update(
{ $unset: { items : 1 } }, { _id: checklist._id },
{ $unset: { items: 1 } },
noValidate noValidate
); );
}); });
@ -217,251 +251,334 @@ Migrations.add('add-card-types', () => {
Cards.find().forEach((card) => { Cards.find().forEach((card) => {
Cards.direct.update( Cards.direct.update(
{ _id: card._id }, { _id: card._id },
{ $set: { {
$set: {
type: 'cardType-card', type: 'cardType-card',
linkedId: null } }, linkedId: null,
},
},
noValidate noValidate
); );
}); });
}); });
Migrations.add('add-custom-fields-to-cards', () => { Migrations.add('add-custom-fields-to-cards', () => {
Cards.update({ Cards.update(
{
customFields: { customFields: {
$exists: false, $exists: false,
}, },
}, {
$set: {
customFields:[],
}, },
}, noValidateMulti); {
$set: {
customFields: [],
},
},
noValidateMulti
);
}); });
Migrations.add('add-requester-field', () => { Migrations.add('add-requester-field', () => {
Cards.update({ Cards.update(
{
requestedBy: { requestedBy: {
$exists: false, $exists: false,
}, },
}, {
$set: {
requestedBy:'',
}, },
}, noValidateMulti); {
$set: {
requestedBy: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-assigner-field', () => { Migrations.add('add-assigner-field', () => {
Cards.update({ Cards.update(
{
assignedBy: { assignedBy: {
$exists: false, $exists: false,
}, },
}, {
$set: {
assignedBy:'',
}, },
}, noValidateMulti); {
$set: {
assignedBy: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-parent-field-to-cards', () => { Migrations.add('add-parent-field-to-cards', () => {
Cards.update({ Cards.update(
{
parentId: { parentId: {
$exists: false, $exists: false,
}, },
}, {
$set: {
parentId:'',
}, },
}, noValidateMulti); {
$set: {
parentId: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-subtasks-boards', () => { Migrations.add('add-subtasks-boards', () => {
Boards.update({ Boards.update(
{
subtasksDefaultBoardId: { subtasksDefaultBoardId: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
subtasksDefaultBoardId: null, subtasksDefaultBoardId: null,
subtasksDefaultListId: null, subtasksDefaultListId: null,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-subtasks-sort', () => { Migrations.add('add-subtasks-sort', () => {
Boards.update({ Boards.update(
{
subtaskSort: { subtaskSort: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
subtaskSort: -1, subtaskSort: -1,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-subtasks-allowed', () => { Migrations.add('add-subtasks-allowed', () => {
Boards.update({ Boards.update(
{
allowsSubtasks: { allowsSubtasks: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
allowsSubtasks: true, allowsSubtasks: true,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-subtasks-allowed', () => { Migrations.add('add-subtasks-allowed', () => {
Boards.update({ Boards.update(
{
presentParentTask: { presentParentTask: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
presentParentTask: 'no-parent', presentParentTask: 'no-parent',
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-authenticationMethod', () => { Migrations.add('add-authenticationMethod', () => {
Users.update({ Users.update(
'authenticationMethod': { {
authenticationMethod: {
$exists: false, $exists: false,
}, },
}, {
$set: {
'authenticationMethod': 'password',
}, },
}, noValidateMulti); {
$set: {
authenticationMethod: 'password',
},
},
noValidateMulti
);
}); });
Migrations.add('remove-tag', () => { Migrations.add('remove-tag', () => {
Users.update({ Users.update(
}, { {},
{
$unset: { $unset: {
'profile.tags':1, 'profile.tags': 1,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('remove-customFields-references-broken', () => { Migrations.add('remove-customFields-references-broken', () => {
Cards.update({'customFields.$value': null}, Cards.update(
{ $pull: { { 'customFields.$value': null },
customFields: {value: null}, {
$pull: {
customFields: { value: null },
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-product-name', () => { Migrations.add('add-product-name', () => {
Settings.update({ Settings.update(
{
productName: { productName: {
$exists: false, $exists: false,
}, },
}, {
$set: {
productName:'',
}, },
}, noValidateMulti); {
$set: {
productName: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-hide-logo', () => { Migrations.add('add-hide-logo', () => {
Settings.update({ Settings.update(
{
hideLogo: { hideLogo: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
hideLogo: false, hideLogo: false,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-custom-html-after-body-start', () => { Migrations.add('add-custom-html-after-body-start', () => {
Settings.update({ Settings.update(
{
customHTMLafterBodyStart: { customHTMLafterBodyStart: {
$exists: false, $exists: false,
}, },
}, {
$set: {
customHTMLafterBodyStart:'',
}, },
}, noValidateMulti); {
$set: {
customHTMLafterBodyStart: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-custom-html-before-body-end', () => { Migrations.add('add-custom-html-before-body-end', () => {
Settings.update({ Settings.update(
{
customHTMLbeforeBodyEnd: { customHTMLbeforeBodyEnd: {
$exists: false, $exists: false,
}, },
}, {
$set: {
customHTMLbeforeBodyEnd:'',
}, },
}, noValidateMulti); {
$set: {
customHTMLbeforeBodyEnd: '',
},
},
noValidateMulti
);
}); });
Migrations.add('add-displayAuthenticationMethod', () => { Migrations.add('add-displayAuthenticationMethod', () => {
Settings.update({ Settings.update(
{
displayAuthenticationMethod: { displayAuthenticationMethod: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
displayAuthenticationMethod: true, displayAuthenticationMethod: true,
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-defaultAuthenticationMethod', () => { Migrations.add('add-defaultAuthenticationMethod', () => {
Settings.update({ Settings.update(
{
defaultAuthenticationMethod: { defaultAuthenticationMethod: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
defaultAuthenticationMethod: 'password', defaultAuthenticationMethod: 'password',
}, },
}, noValidateMulti); },
noValidateMulti
);
}); });
Migrations.add('add-templates', () => { Migrations.add('add-templates', () => {
Boards.update({ Boards.update(
{
type: { type: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
type: 'board', type: 'board',
}, },
}, noValidateMulti); },
Swimlanes.update({ noValidateMulti
);
Swimlanes.update(
{
type: { type: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
type: 'swimlane', type: 'swimlane',
}, },
}, noValidateMulti); },
Lists.update({ noValidateMulti
);
Lists.update(
{
type: { type: {
$exists: false, $exists: false,
}, },
swimlaneId: { swimlaneId: {
$exists: false, $exists: false,
}, },
}, { },
{
$set: { $set: {
type: 'list', type: 'list',
swimlaneId: '', swimlaneId: '',
}, },
}, noValidateMulti); },
noValidateMulti
);
Users.find({ Users.find({
'profile.templatesBoardId': { 'profile.templatesBoardId': {
$exists: false, $exists: false,
}, },
}).forEach((user) => { }).forEach((user) => {
// Create board and swimlanes // Create board and swimlanes
Boards.insert({ Boards.insert(
{
title: TAPi18n.__('templates'), title: TAPi18n.__('templates'),
permission: 'private', permission: 'private',
type: 'template-container', type: 'template-container',
@ -474,67 +591,172 @@ Migrations.add('add-templates', () => {
isCommentOnly: false, isCommentOnly: false,
}, },
], ],
}, (err, boardId) => { },
(err, boardId) => {
// Insert the reference to our templates board // Insert the reference to our templates board
Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}}); Users.update(user._id, {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the card templates swimlane // Insert the card templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('card-templates-swimlane'), title: TAPi18n.__('card-templates-swimlane'),
boardId, boardId,
sort: 1, sort: 1,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { },
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane // Insert the reference to out card templates swimlane
Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); Users.update(user._id, {
$set: { 'profile.cardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the list templates swimlane // Insert the list templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'), title: TAPi18n.__('list-templates-swimlane'),
boardId, boardId,
sort: 2, sort: 2,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { },
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane // Insert the reference to out list templates swimlane
Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); Users.update(user._id, {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
}); });
}
);
// Insert the board templates swimlane // Insert the board templates swimlane
Swimlanes.insert({ Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'), title: TAPi18n.__('board-templates-swimlane'),
boardId, boardId,
sort: 3, sort: 3,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { },
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane // Insert the reference to out board templates swimlane
Users.update(user._id, {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}}); Users.update(user._id, {
}); $set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
}); });
}
);
}
);
}); });
}); });
Migrations.add('fix-circular-reference_', () => { Migrations.add('fix-circular-reference_', () => {
Cards.find().forEach((card) => { Cards.find().forEach((card) => {
if (card.parentId === card._id) { if (card.parentId === card._id) {
Cards.update(card._id, {$set: {parentId: ''}}, noValidateMulti); Cards.update(card._id, { $set: { parentId: '' } }, noValidateMulti);
} }
}); });
}); });
Migrations.add('mutate-boardIds-in-customfields', () => { Migrations.add('mutate-boardIds-in-customfields', () => {
CustomFields.find().forEach((cf) => { CustomFields.find().forEach((cf) => {
CustomFields.update(cf, { CustomFields.update(
cf,
{
$set: { $set: {
boardIds: [cf.boardId], boardIds: [cf.boardId],
}, },
$unset: { $unset: {
boardId: '', boardId: '',
}, },
}, noValidateMulti); },
noValidateMulti
);
});
});
const firstBatchOfDbsToAddCreatedAndUpdated = [
AccountSettings,
Actions,
Activities,
Announcements,
Boards,
CardComments,
Cards,
ChecklistItems,
Checklists,
CustomFields,
Integrations,
InvitationCodes,
Lists,
Rules,
Settings,
Swimlanes,
Triggers,
UnsavedEdits,
];
firstBatchOfDbsToAddCreatedAndUpdated.forEach((db) => {
db.before.insert((userId, doc) => {
doc.createdAt = Date.now();
doc.updatedAt = doc.createdAt;
});
db.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.updatedAt = new Date();
});
});
const modifiedAtTables = [
AccountSettings,
Actions,
Activities,
Announcements,
Boards,
CardComments,
Cards,
ChecklistItems,
Checklists,
CustomFields,
Integrations,
InvitationCodes,
Lists,
Rules,
Settings,
Swimlanes,
Triggers,
UnsavedEdits,
Users,
];
Migrations.add('add-missing-created-and-modified', () => {
Promise.all(
modifiedAtTables.map((db) =>
db
.rawCollection()
.update(
{ modifiedAt: { $exists: false } },
{ $set: { modifiedAt: new Date() } },
{ multi: true }
)
.then(() =>
db
.rawCollection()
.update(
{ createdAt: { $exists: false } },
{ $set: { createdAt: new Date() } },
{ multi: true }
)
)
)
)
.then(() => {
// eslint-disable-next-line no-console
console.info('Successfully added createdAt and updatedAt to all tables');
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
}); });
}); });