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,18 +1,44 @@
AccountSettings = new Mongo.Collection('accountSettings'); AccountSettings = new Mongo.Collection('accountSettings');
AccountSettings.attachSchema(new SimpleSchema({ AccountSettings.attachSchema(
_id: { new SimpleSchema({
type: String, _id: {
}, type: String,
booleanValue: { },
type: Boolean, booleanValue: {
optional: true, type: Boolean,
}, optional: true,
sort: { },
type: Number, sort: {
decimal: true, type: Number,
}, 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 });
$setOnInsert: { AccountSettings.upsert(
booleanValue: false, { _id: 'accounts-allowEmailChange' },
sort: 0, {
}, $setOnInsert: {
}); booleanValue: false,
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, { sort: 0,
$setOnInsert: { },
booleanValue: false, }
sort: 1, );
}, AccountSettings.upsert(
}); { _id: 'accounts-allowUserNameChange' },
{
$setOnInsert: {
booleanValue: false,
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,23 +1,49 @@
Announcements = new Mongo.Collection('announcements'); Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({ Announcements.attachSchema(
enabled: { new SimpleSchema({
type: Boolean, enabled: {
defaultValue: false, type: Boolean,
}, defaultValue: false,
title: { },
type: String, title: {
optional: true, type: String,
}, optional: true,
body: { },
type: String, body: {
optional: true, type: String,
}, optional: true,
sort: { },
type: Number, sort: {
decimal: true, type: Number,
}, 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: {
source: '',
}, },
}); {
$unset: {
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;

File diff suppressed because it is too large Load diff

View file

@ -3,55 +3,69 @@ CardComments = new Mongo.Collection('card_comments');
/** /**
* A comment on a card * A comment on a card
*/ */
CardComments.attachSchema(new SimpleSchema({ CardComments.attachSchema(
boardId: { new SimpleSchema({
/** boardId: {
* the board ID /**
*/ * the board ID
type: String, */
}, type: String,
cardId: {
/**
* the card ID
*/
type: String,
},
// XXX Rename in `content`? `text` is a bit vague...
text: {
/**
* the text of the comment
*/
type: String,
},
// XXX We probably don't need this information here, since we already have it
// in the associated comment creation activity
createdAt: {
/**
* when was the comment created
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, cardId: {
// XXX Should probably be called `authorId` /**
userId: { * the card ID
/** */
* the author ID of the comment type: String,
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
}, },
}, // XXX Rename in `content`? `text` is a bit vague...
})); text: {
/**
* the text of the comment
*/
type: String,
},
createdAt: {
/**
* when was the comment created
*/
type: Date,
denyUpdate: false,
// 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();
}
},
},
// XXX Should probably be called `authorId`
userId: {
/**
* the author ID of the comment
*/
type: String,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
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(
try { 'GET',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/cards/:cardId/comments/:commentId',
const paramBoardId = req.params.boardId; function(req, res) {
const paramCommentId = req.params.commentId; try {
const paramCardId = req.params.cardId; Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramBoardId = req.params.boardId;
code: 200, const paramCommentId = req.params.commentId;
data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }), const paramCardId = req.params.cardId;
}); JsonRoutes.sendResult(res, {
code: 200,
data: CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
}),
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
/** /**
* @operation new_comment * @operation new_comment
@ -214,35 +246,42 @@ 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(
try { 'POST',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/cards/:cardId/comments',
const paramBoardId = req.params.boardId; function(req, res) {
const paramCardId = req.params.cardId; try {
const id = CardComments.direct.insert({ Authentication.checkUserId(req.userId);
userId: req.body.authorId, const paramBoardId = req.params.boardId;
text: req.body.comment, const paramCardId = req.params.cardId;
cardId: paramCardId, const id = CardComments.direct.insert({
boardId: paramBoardId, userId: req.body.authorId,
}); text: req.body.comment,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.sendResult(res, { JsonRoutes.sendResult(res, {
code: 200, code: 200,
data: { data: {
_id: id,
},
});
const cardComment = CardComments.findOne({
_id: id, _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) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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(
try { 'DELETE',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/cards/:cardId/comments/:commentId',
const paramBoardId = req.params.boardId; function(req, res) {
const paramCommentId = req.params.commentId; try {
const paramCardId = req.params.cardId; Authentication.checkUserId(req.userId);
CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }); const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, { const paramCommentId = req.params.commentId;
code: 200, const paramCardId = req.params.cardId;
data: { CardComments.remove({
_id: paramCardId, _id: paramCommentId,
}, cardId: paramCardId,
}); boardId: paramBoardId,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
},
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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,40 +3,66 @@ ChecklistItems = new Mongo.Collection('checklistItems');
/** /**
* An item in a checklist * An item in a checklist
*/ */
ChecklistItems.attachSchema(new SimpleSchema({ ChecklistItems.attachSchema(
title: { new SimpleSchema({
/** title: {
* the text of the item /**
*/ * the text of the item
type: String, */
}, type: String,
sort: { },
/** sort: {
* the sorting field of the item /**
*/ * the sorting field of the item
type: Number, */
decimal: true, type: Number,
}, decimal: true,
isFinished: { },
/** isFinished: {
* Is the item checked? /**
*/ * Is the item checked?
type: Boolean, */
defaultValue: false, type: Boolean,
}, defaultValue: false,
checklistId: { },
/** checklistId: {
* the checklist ID the item is attached to /**
*/ * the checklist ID the item is attached to
type: String, */
}, type: String,
cardId: { },
/** cardId: {
* the card ID the item is attached to /**
*/ * the card ID the item is attached to
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,21 +264,25 @@ 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',
const paramItemId = req.params.itemId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
const checklistItem = ChecklistItems.findOne({ _id: paramItemId }); function(req, res) {
if (checklistItem) { Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramItemId = req.params.itemId;
code: 200, const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
data: checklistItem, if (checklistItem) {
}); JsonRoutes.sendResult(res, {
} else { code: 200,
JsonRoutes.sendResult(res, { data: checklistItem,
code: 500, });
}); } else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
} }
}); );
/** /**
* @operation edit_checklist_item * @operation edit_checklist_item
@ -262,25 +297,35 @@ 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')) {
ChecklistItems.direct.update(
{ _id: paramItemId },
{ $set: { title: req.body.title } }
);
}
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
} }
if (req.body.hasOwnProperty('title')) { );
ChecklistItems.direct.update({_id: paramItemId}, {$set: {title: req.body.title}});
}
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
});
/** /**
* @operation delete_checklist_item * @operation delete_checklist_item
@ -295,15 +340,21 @@ 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',
const paramItemId = req.params.itemId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
ChecklistItems.direct.remove({ _id: paramItemId }); function(req, res) {
JsonRoutes.sendResult(res, { Authentication.checkUserId(req.userId);
code: 200, const paramItemId = req.params.itemId;
data: { ChecklistItems.direct.remove({ _id: paramItemId });
_id: paramItemId, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
}); _id: paramItemId,
},
});
}
);
} }
export default ChecklistItems;

View file

@ -3,49 +3,64 @@ Checklists = new Mongo.Collection('checklists');
/** /**
* A Checklist * A Checklist
*/ */
Checklists.attachSchema(new SimpleSchema({ Checklists.attachSchema(
cardId: { new SimpleSchema({
/** cardId: {
* The ID of the card the checklist is in /**
*/ * The ID of the card the checklist is in
type: String, */
}, type: String,
title: {
/**
* the title of the checklist
*/
type: String,
defaultValue: 'Checklist',
},
finishedAt: {
/**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, title: {
sort: { /**
/** * the title of the checklist
* sorting value of the checklist */
*/ type: String,
type: Number, defaultValue: 'Checklist',
decimal: true, },
}, finishedAt: {
})); /**
* When was the checklist finished
*/
type: Date,
optional: true,
},
createdAt: {
/**
* Creation date of the checklist
*/
type: Date,
denyUpdate: false,
// 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();
}
},
},
sort: {
/**
* sorting value of the checklist
*/
type: Number,
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, {
}, { sort: ['sort'] }); checklistId: this._id,
},
{ 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,26 +196,32 @@ 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',
const paramCardId = req.params.cardId; '/api/boards/:boardId/cards/:cardId/checklists',
const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) { function(req, res) {
return { Authentication.checkUserId(req.userId);
_id: doc._id, const paramCardId = req.params.cardId;
title: doc.title, const checklists = Checklists.find({ cardId: paramCardId }).map(function(
}; doc
}); ) {
if (checklists) { return {
JsonRoutes.sendResult(res, { _id: doc._id,
code: 200, title: doc.title,
data: checklists, };
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
}); });
if (checklists) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklists,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
} }
}); );
/** /**
* @operation get_checklist * @operation get_checklist
@ -209,29 +239,38 @@ 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',
const paramChecklistId = req.params.checklistId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
const paramCardId = req.params.cardId; function(req, res) {
const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }); Authentication.checkUserId(req.userId);
if (checklist) { const paramChecklistId = req.params.checklistId;
checklist.items = ChecklistItems.find({checklistId: checklist._id}).map(function (doc) { const paramCardId = req.params.cardId;
return { const checklist = Checklists.findOne({
_id: doc._id, _id: paramChecklistId,
title: doc.title, cardId: paramCardId,
isFinished: doc.isFinished,
};
});
JsonRoutes.sendResult(res, {
code: 200,
data: checklist,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
}); });
if (checklist) {
checklist.items = ChecklistItems.find({
checklistId: checklist._id,
}).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
isFinished: doc.isFinished,
};
});
JsonRoutes.sendResult(res, {
code: 200,
data: checklist,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
} }
}); );
/** /**
* @operation new_checklist * @operation new_checklist
@ -242,36 +281,40 @@ 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({
title: req.body.title, title: req.body.title,
cardId: paramCardId, cardId: paramCardId,
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,
title: item.title, title: item.title,
sort: idx, sort: idx,
});
}); });
}); JsonRoutes.sendResult(res, {
JsonRoutes.sendResult(res, { code: 200,
code: 200, data: {
data: { _id: id,
_id: id, },
}, });
}); } else {
} else { JsonRoutes.sendResult(res, {
JsonRoutes.sendResult(res, { code: 400,
code: 400, });
}); }
} }
}); );
/** /**
* @operation delete_checklist * @operation delete_checklist
@ -284,15 +327,21 @@ 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',
const paramChecklistId = req.params.checklistId; '/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
Checklists.remove({ _id: paramChecklistId }); function(req, res) {
JsonRoutes.sendResult(res, { Authentication.checkUserId(req.userId);
code: 200, const paramChecklistId = req.params.checklistId;
data: { Checklists.remove({ _id: paramChecklistId });
_id: paramChecklistId, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
}); _id: paramChecklistId,
},
});
}
);
} }
export default Checklists;

View file

@ -3,74 +3,100 @@ 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(
boardIds: { new SimpleSchema({
/** boardIds: {
* the ID of the board /**
*/ * the ID of the board
type: [String], */
}, type: [String],
name: { },
/** name: {
* name of the custom field /**
*/ * name of the custom field
type: String, */
}, type: String,
type: { },
/** type: {
* type of the custom field /**
*/ * type of the custom field
type: String, */
allowedValues: ['text', 'number', 'date', 'dropdown'], type: String,
}, allowedValues: ['text', 'number', 'date', 'dropdown'],
settings: { },
/** settings: {
* settings of the custom field /**
*/ * settings of the custom field
type: Object, */
}, type: Object,
'settings.dropdownItems': { },
/** 'settings.dropdownItems': {
* list of drop down items objects /**
*/ * list of drop down items objects
type: [Object], */
optional: true, type: [Object],
}, optional: true,
'settings.dropdownItems.$': { },
type: new SimpleSchema({ 'settings.dropdownItems.$': {
_id: { type: new SimpleSchema({
/** _id: {
* ID of the drop down item /**
*/ * ID of the drop down item
type: String, */
type: String,
},
name: {
/**
* name of the drop down item
*/
type: String,
},
}),
},
showOnCard: {
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
createdAt: {
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
name: { },
/** modifiedAt: {
* name of the drop down item type: Date,
*/ denyUpdate: false,
type: String, // eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
}), },
}, })
showOnCard: { );
/**
* should we show on the cards this custom field
*/
type: Boolean,
},
automaticallyOnCard: {
/**
* should the custom fields automatically be added on cards?
*/
type: Boolean,
},
showLabelOnMiniCard: {
/**
* should the label of the custom field be shown on minicards?
*/
type: Boolean,
},
}));
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(
return { function(cf) {
_id: cf._id, return {
name: cf.name, _id: cf._id,
type: cf.type, name: cf.name,
}; 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',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/custom-fields/:customFieldId',
const paramCustomFieldId = req.params.customFieldId; function(req, res) {
JsonRoutes.sendResult(res, { Authentication.checkUserId(req.userId);
code: 200, const paramBoardId = req.params.boardId;
data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }), const paramCustomFieldId = req.params.customFieldId;
}); JsonRoutes.sendResult(res, {
}); code: 200,
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',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/custom-fields/:customFieldId',
const id = req.params.customFieldId; function(req, res) {
CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} }); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramBoardId = req.params.boardId;
code: 200, const id = req.params.customFieldId;
data: { CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
_id: id, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
}); _id: id,
},
});
}
);
} }
export default CustomFields;

View file

@ -3,75 +3,96 @@ Integrations = new Mongo.Collection('integrations');
/** /**
* Integration with third-party applications * Integration with third-party applications
*/ */
Integrations.attachSchema(new SimpleSchema({ Integrations.attachSchema(
enabled: { new SimpleSchema({
/** enabled: {
* is the integration enabled? /**
*/ * is the integration enabled?
type: Boolean, */
defaultValue: true, type: Boolean,
}, defaultValue: true,
title: {
/**
* name of the integration
*/
type: String,
optional: true,
},
type: {
/**
* type of the integratation (Default to 'outgoing-webhooks')
*/
type: String,
defaultValue: 'outgoing-webhooks',
},
activities: {
/**
* activities the integration gets triggered (list)
*/
type: [String],
defaultValue: ['all'],
},
url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex)
/**
* URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/
type: String,
},
token: {
/**
* token of the integration
*/
type: String,
optional: true,
},
boardId: {
/**
* Board ID of the integration
*/
type: String,
},
createdAt: {
/**
* Creation date of the integration
*/
type: Date,
denyUpdate: false,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, title: {
userId: { /**
/** * name of the integration
* user ID who created the interation */
*/ type: String,
type: String, optional: true,
}, },
})); type: {
/**
* type of the integratation (Default to 'outgoing-webhooks')
*/
type: String,
defaultValue: 'outgoing-webhooks',
},
activities: {
/**
* activities the integration gets triggered (list)
*/
type: [String],
defaultValue: ['all'],
},
url: {
// URL validation regex (https://mathiasbynens.be/demo/url-regex)
/**
* URL validation regex (https://mathiasbynens.be/demo/url-regex)
*/
type: String,
},
token: {
/**
* token of the integration
*/
type: String,
optional: true,
},
boardId: {
/**
* Board ID of the integration
*/
type: String,
},
createdAt: {
/**
* Creation date of the integration
*/
type: Date,
denyUpdate: false,
// 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();
}
},
},
userId: {
/**
* user ID who created the interation
*/
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(
try { 'DELETE',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/integrations/:intId/activities',
const paramIntId = req.params.intId; function(req, res) {
const newActivities = req.body.activities; try {
Authentication.checkBoardAccess(req.userId, paramBoardId); const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
const newActivities = req.body.activities;
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) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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(
try { 'POST',
const paramBoardId = req.params.boardId; '/api/boards/:boardId/integrations/:intId/activities',
const paramIntId = req.params.intId; function(req, res) {
const newActivities = req.body.activities; try {
Authentication.checkBoardAccess(req.userId, paramBoardId); const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
const newActivities = req.body.activities;
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) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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,45 +1,78 @@
InvitationCodes = new Mongo.Collection('invitation_codes'); InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({ InvitationCodes.attachSchema(
code: { new SimpleSchema({
type: String, code: {
}, type: String,
email: { },
type: String, email: {
unique: true, type: String,
regEx: SimpleSchema.RegEx.Email, unique: true,
}, regEx: SimpleSchema.RegEx.Email,
createdAt: { },
type: Date, createdAt: {
denyUpdate: false, type: Date,
}, denyUpdate: false,
// always be the admin if only one admin optional: true,
authorId: { // eslint-disable-next-line consistent-return
type: String, autoValue() {
}, if (this.isInsert) {
boardsToBeInvited: { return new Date();
type: [String], } else {
optional: true, this.unset();
}, }
valid: { },
type: Boolean, },
defaultValue: true, 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
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
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,125 +3,161 @@ 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(
title: { new SimpleSchema({
/** title: {
* the title of the list /**
*/ * the title of the list
type: String, */
}, type: String,
archived: {
/**
* is the list archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
}, },
}, archived: {
boardId: { /**
/** * is the list archived
* the board associated to this list */
*/ type: Boolean,
type: String, // eslint-disable-next-line consistent-return
}, autoValue() {
swimlaneId: { if (this.isInsert && !this.isSet) {
/** return false;
* the swimlane associated to this list. Used for templates }
*/ },
type: String,
defaultValue: '',
},
createdAt: {
/**
* creation date
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, boardId: {
sort: { /**
/** * the board associated to this list
* is the list sorted */
*/ type: String,
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* last update of the list
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
}, swimlaneId: {
wipLimit: { /**
/** * the swimlane associated to this list. Used for templates
* WIP object, see below */
*/ type: String,
type: Object, defaultValue: '',
optional: true, },
}, createdAt: {
'wipLimit.value': { /**
/** * creation date
* value of the WIP */
*/ type: Date,
type: Number, // eslint-disable-next-line consistent-return
decimal: false, autoValue() {
defaultValue: 1, if (this.isInsert) {
}, return new Date();
'wipLimit.enabled': { } else {
/** this.unset();
* is the WIP enabled }
*/ },
type: Boolean, },
defaultValue: false, sort: {
}, /**
'wipLimit.soft': { * is the list sorted
/** */
* is the WIP a soft or hard requirement type: Number,
*/ decimal: true,
type: Boolean, // XXX We should probably provide a default
defaultValue: false, optional: true,
}, },
color: { updatedAt: {
/** /**
* the color of the list * last update of the list
*/ */
type: String, type: Date,
optional: true, optional: true,
// silver is the default, so it is left out // eslint-disable-next-line consistent-return
allowedValues: [ autoValue() {
'white', 'green', 'yellow', 'orange', 'red', 'purple', if (this.isUpdate || this.isUpsert || this.isInsert) {
'blue', 'sky', 'lime', 'pink', 'black', return new Date();
'peachpuff', 'crimson', 'plum', 'darkgreen', } else {
'slateblue', 'magenta', 'gold', 'navy', 'gray', this.unset();
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', }
], },
}, },
type: { modifiedAt: {
/** type: Date,
* The type of list denyUpdate: false,
*/ // eslint-disable-next-line consistent-return
type: String, autoValue() {
defaultValue: 'list', if (this.isInsert || this.isUpsert || this.isUpdate) {
}, return new Date();
})); } else {
this.unset();
}
},
},
wipLimit: {
/**
* WIP object, see below
*/
type: Object,
optional: true,
},
'wipLimit.value': {
/**
* value of the WIP
*/
type: Number,
decimal: false,
defaultValue: 1,
},
'wipLimit.enabled': {
/**
* is the WIP enabled
*/
type: Boolean,
defaultValue: false,
},
'wipLimit.soft': {
/**
* is the WIP a soft or hard requirement
*/
type: Boolean,
defaultValue: false,
},
color: {
/**
* the color of the list
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
},
type: {
/**
* The type of list
*/
type: String,
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(
return { function(doc) {
_id: doc._id, return {
title: doc.title, _id: doc._id,
}; 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,23 +1,51 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules'); Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({ Rules.attachSchema(
title: { new SimpleSchema({
type: String, title: {
optional: false, type: String,
}, optional: false,
triggerId: { },
type: String, triggerId: {
optional: false, type: String,
}, optional: false,
actionId: { },
type: String, actionId: {
optional: false, type: String,
}, optional: false,
boardId: { },
type: String, boardId: {
optional: false, type: String,
}, 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,67 +1,85 @@
Settings = new Mongo.Collection('settings'); Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({ Settings.attachSchema(
disableRegistration: { new SimpleSchema({
type: Boolean, disableRegistration: {
}, type: Boolean,
'mailServer.username': { },
type: String, 'mailServer.username': {
optional: true, type: String,
}, optional: true,
'mailServer.password': { },
type: String, 'mailServer.password': {
optional: true, type: String,
}, optional: true,
'mailServer.host': { },
type: String, 'mailServer.host': {
optional: true, type: String,
}, optional: true,
'mailServer.port': { },
type: String, 'mailServer.port': {
optional: true, type: String,
}, optional: true,
'mailServer.enableTLS': { },
type: Boolean, 'mailServer.enableTLS': {
optional: true, type: Boolean,
}, optional: true,
'mailServer.from': { },
type: String, 'mailServer.from': {
optional: true, type: String,
}, optional: true,
productName: { },
type: String, productName: {
optional: true, type: String,
}, optional: true,
customHTMLafterBodyStart: { },
type: String, customHTMLafterBodyStart: {
optional: true, type: String,
}, optional: true,
customHTMLbeforeBodyEnd: { },
type: String, customHTMLbeforeBodyEnd: {
optional: true, type: String,
}, optional: true,
displayAuthenticationMethod: { },
type: Boolean, displayAuthenticationMethod: {
optional: true, type: Boolean,
}, optional: true,
defaultAuthenticationMethod: { },
type: String, defaultAuthenticationMethod: {
optional: false, type: String,
}, optional: false,
hideLogo: { },
type: Boolean, hideLogo: {
optional: true, type: Boolean,
}, optional: true,
createdAt: { },
type: Date, createdAt: {
denyUpdate: true, type: Date,
}, denyUpdate: true,
modifiedAt: { // eslint-disable-next-line consistent-return
type: Date, autoValue() {
}, if (this.isInsert) {
})); return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
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, {
sendInvitationEmail(invitation._id); $set: { boardsToBeInvited: boards },
}else {
const code = getRandomNum(100000, 999999);
InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){
if (!err && _id) {
sendInvitationEmail(_id);
} else {
throw new Meteor.Error('invitation-generated-fail', err.message);
}
}); });
sendInvitationEmail(invitation._id);
} else {
const code = getRandomNum(100000, 999999);
InvitationCodes.insert(
{
code,
email,
boardsToBeInvited: boards,
createdAt: new Date(),
authorId: Meteor.userId(),
},
function(err, _id) {
if (!err && _id) {
sendInvitationEmail(_id);
} else {
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,89 +3,125 @@ 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(
title: { new SimpleSchema({
/** title: {
* the title of the swimlane /**
*/ * the title of the swimlane
type: String, */
}, type: String,
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
}, },
}, archived: {
boardId: { /**
/** * is the swimlane archived?
* the ID of the board the swimlane is attached to */
*/ type: Boolean,
type: String, // eslint-disable-next-line consistent-return
}, autoValue() {
createdAt: { if (this.isInsert && !this.isSet) {
/** return false;
* creation date of the swimlane }
*/ },
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
}, },
}, boardId: {
sort: { /**
/** * the ID of the board the swimlane is attached to
* the sort value of the swimlane */
*/ type: String,
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
color: {
/**
* the color of the swimlane
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white', 'green', 'yellow', 'orange', 'red', 'purple',
'blue', 'sky', 'lime', 'pink', 'black',
'peachpuff', 'crimson', 'plum', 'darkgreen',
'slateblue', 'magenta', 'gold', 'navy', 'gray',
'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo',
],
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
}, },
}, createdAt: {
type: { /**
/** * creation date of the swimlane
* The type of swimlane */
*/ type: Date,
type: String, // eslint-disable-next-line consistent-return
defaultValue: 'swimlane', autoValue() {
}, if (this.isInsert) {
})); return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* the sort value of the swimlane
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
color: {
/**
* the color of the swimlane
*/
type: String,
optional: true,
// silver is the default, so it is left out
allowedValues: [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
},
updatedAt: {
/**
* when was the swimlane last edited
*/
type: Date,
optional: true,
// eslint-disable-next-line consistent-return
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();
} else {
this.unset();
}
},
},
type: {
/**
* The type of swimlane
*/
type: String,
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(
swimlaneId: this._id, Filter.mongoSelector({
archived: false, swimlaneId: this._id,
}), { sort: ['sort'] }); archived: false,
}),
{ sort: ['sort'] }
);
}, },
lists() { lists() {
return Lists.find({ return Lists.find(
boardId: this.boardId, {
swimlaneId: {$in: [this._id, '']}, boardId: this.boardId,
archived: false, swimlaneId: { $in: [this._id, ''] },
}, { sort: ['sort'] }); archived: false,
},
{ 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, {
swimlaneId: {$in: [doc._id, '']}, boardId: doc.boardId,
archived: false, swimlaneId: { $in: [doc._id, ''] },
}, { sort: ['sort'] }); archived: false,
},
{ 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(
return { function(doc) {
_id: doc._id, return {
title: doc.title, _id: doc._id,
}; 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,25 +438,29 @@ 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(
try { 'DELETE',
Authentication.checkUserId( req.userId); '/api/boards/:boardId/swimlanes/:swimlaneId',
const paramBoardId = req.params.boardId; function(req, res) {
const paramSwimlaneId = req.params.swimlaneId; try {
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId }); Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, { const paramBoardId = req.params.boardId;
code: 200, const paramSwimlaneId = req.params.swimlaneId;
data: { Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
_id: paramSwimlaneId, JsonRoutes.sendResult(res, {
}, code: 200,
}); data: {
_id: paramSwimlaneId,
},
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
} }
catch (error) { );
JsonRoutes.sendResult(res, {
code: 200,
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,31 +2,66 @@
// `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(
fieldName: { new SimpleSchema({
type: String, fieldName: {
}, type: String,
docId: {
type: String,
},
value: {
type: String,
},
userId: {
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
}, },
}, docId: {
})); type: String,
},
value: {
type: String,
},
userId: {
type: String,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
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;

File diff suppressed because it is too large Load diff

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: { {
$exists: false,
},
}, {
$set: {
background: { background: {
type: 'color', $exists: false,
color: defaultColor,
}, },
}, },
}, noValidateMulti); {
$set: {
background: {
type: 'color',
color: defaultColor,
},
},
},
noValidateMulti
);
}); });
Migrations.add('lowercase-board-permission', () => { Migrations.add('lowercase-board-permission', () => {
@ -57,24 +81,28 @@ 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(
$set: { file._id,
'original.type': newTypeForNonImage, {
'copies.attachments.type': newTypeForNonImage, $set: {
'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage,
},
}, },
}, noValidate); noValidate
);
} }
}); });
}); });
Migrations.add('card-covers', () => { 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(
$set: { color: newBoardColor }, board._id,
$unset: { background: '' }, {
}, noValidate); $set: { color: newBoardColor },
$unset: { background: '' },
},
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,324 +251,512 @@ 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: { {
type: 'cardType-card', $set: {
linkedId: null } }, type: 'cardType-card',
linkedId: null,
},
},
noValidate noValidate
); );
}); });
}); });
Migrations.add('add-custom-fields-to-cards', () => { Migrations.add('add-custom-fields-to-cards', () => {
Cards.update({ Cards.update(
customFields: { {
$exists: false, customFields: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
customFields:[], customFields: [],
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-requester-field', () => { Migrations.add('add-requester-field', () => {
Cards.update({ Cards.update(
requestedBy: { {
$exists: false, requestedBy: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
requestedBy:'', requestedBy: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-assigner-field', () => { Migrations.add('add-assigner-field', () => {
Cards.update({ Cards.update(
assignedBy: { {
$exists: false, assignedBy: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
assignedBy:'', assignedBy: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-parent-field-to-cards', () => { Migrations.add('add-parent-field-to-cards', () => {
Cards.update({ Cards.update(
parentId: { {
$exists: false, parentId: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
parentId:'', parentId: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-subtasks-boards', () => { Migrations.add('add-subtasks-boards', () => {
Boards.update({ Boards.update(
subtasksDefaultBoardId: { {
$exists: false, subtasksDefaultBoardId: {
$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: { {
$exists: false, subtaskSort: {
$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: { {
$exists: false, allowsSubtasks: {
$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: { {
$exists: false, presentParentTask: {
$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': { {
$exists: false, authenticationMethod: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
'authenticationMethod': 'password', authenticationMethod: 'password',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('remove-tag', () => { Migrations.add('remove-tag', () => {
Users.update({ Users.update(
}, { {},
$unset: { {
'profile.tags':1, $unset: {
'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: { {
$exists: false, productName: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
productName:'', productName: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-hide-logo', () => { Migrations.add('add-hide-logo', () => {
Settings.update({ Settings.update(
hideLogo: { {
$exists: false, hideLogo: {
$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: { {
$exists: false, customHTMLafterBodyStart: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
customHTMLafterBodyStart:'', customHTMLafterBodyStart: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-custom-html-before-body-end', () => { Migrations.add('add-custom-html-before-body-end', () => {
Settings.update({ Settings.update(
customHTMLbeforeBodyEnd: { {
$exists: false, customHTMLbeforeBodyEnd: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
customHTMLbeforeBodyEnd:'', customHTMLbeforeBodyEnd: '',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-displayAuthenticationMethod', () => { Migrations.add('add-displayAuthenticationMethod', () => {
Settings.update({ Settings.update(
displayAuthenticationMethod: { {
$exists: false, displayAuthenticationMethod: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
displayAuthenticationMethod: true, displayAuthenticationMethod: true,
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-defaultAuthenticationMethod', () => { Migrations.add('add-defaultAuthenticationMethod', () => {
Settings.update({ Settings.update(
defaultAuthenticationMethod: { {
$exists: false, defaultAuthenticationMethod: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
defaultAuthenticationMethod: 'password', defaultAuthenticationMethod: 'password',
},
}, },
}, noValidateMulti); noValidateMulti
);
}); });
Migrations.add('add-templates', () => { Migrations.add('add-templates', () => {
Boards.update({ Boards.update(
type: { {
$exists: false, type: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
type: 'board', type: 'board',
},
}, },
}, noValidateMulti); noValidateMulti
Swimlanes.update({ );
type: { Swimlanes.update(
$exists: false, {
type: {
$exists: false,
},
}, },
}, { {
$set: { $set: {
type: 'swimlane', type: 'swimlane',
},
}, },
}, noValidateMulti); noValidateMulti
Lists.update({ );
type: { Lists.update(
$exists: false, {
type: {
$exists: false,
},
swimlaneId: {
$exists: false,
},
}, },
swimlaneId: { {
$exists: false, $set: {
type: 'list',
swimlaneId: '',
},
}, },
}, { noValidateMulti
$set: { );
type: 'list',
swimlaneId: '',
},
}, 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'), {
permission: 'private', title: TAPi18n.__('templates'),
type: 'template-container', permission: 'private',
members: [
{
userId: user._id,
isAdmin: true,
isActive: true,
isNoComments: false,
isCommentOnly: false,
},
],
}, (err, boardId) => {
// Insert the reference to our templates board
Users.update(user._id, {$set: {'profile.templatesBoardId': boardId}});
// Insert the card templates swimlane
Swimlanes.insert({
title: TAPi18n.__('card-templates-swimlane'),
boardId,
sort: 1,
type: 'template-container', type: 'template-container',
}, (err, swimlaneId) => { members: [
{
userId: user._id,
isAdmin: true,
isActive: true,
isNoComments: false,
isCommentOnly: false,
},
],
},
(err, boardId) => {
// Insert the reference to our templates board
Users.update(user._id, {
$set: { 'profile.templatesBoardId': boardId },
});
// Insert the reference to out card templates swimlane // Insert the card templates swimlane
Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}}); Swimlanes.insert(
}); {
title: TAPi18n.__('card-templates-swimlane'),
boardId,
sort: 1,
type: 'template-container',
},
(err, swimlaneId) => {
// Insert the reference to out card templates swimlane
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'), {
boardId, title: TAPi18n.__('list-templates-swimlane'),
sort: 2, boardId,
type: 'template-container', sort: 2,
}, (err, swimlaneId) => { type: 'template-container',
},
(err, swimlaneId) => {
// Insert the reference to out list templates swimlane
Users.update(user._id, {
$set: { 'profile.listTemplatesSwimlaneId': swimlaneId },
});
}
);
// Insert the reference to out list templates swimlane // Insert the board templates swimlane
Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}}); Swimlanes.insert(
}); {
title: TAPi18n.__('board-templates-swimlane'),
// Insert the board templates swimlane boardId,
Swimlanes.insert({ sort: 3,
title: TAPi18n.__('board-templates-swimlane'), type: 'template-container',
boardId, },
sort: 3, (err, swimlaneId) => {
type: 'template-container', // Insert the reference to out board templates swimlane
}, (err, swimlaneId) => { Users.update(user._id, {
$set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
// Insert the reference to out board templates swimlane });
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(
$set: { cf,
boardIds: [cf.boardId], {
$set: {
boardIds: [cf.boardId],
},
$unset: {
boardId: '',
},
}, },
$unset: { noValidateMulti
boardId: '', );
},
}, 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);
});
});