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
indent_style = space
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": {
"es6": true,
"node": true,
"browser": true
"browser": true,
"meteor": true
},
"parserOptions": {
"ecmaVersion": 2017,
@ -28,11 +34,12 @@
"no-unneeded-ternary": 2,
"radix": 2,
"semi": [2, "always"],
"camelcase": [2, {"properties": "never"}],
"camelcase": [2, { "properties": "never" }],
"comma-spacing": 2,
"comma-style": 2,
"eol-last": 2,
"linebreak-style": [2, "unix"],
"meteor/audit-argument-checks": 0,
"new-parens": 2,
"no-lonely-if": 2,
"no-multiple-empty-lines": 2,
@ -52,8 +59,26 @@
"prefer-const": 2,
"prefer-spread": 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": {
"Meteor": false,
"Session": false,
@ -100,7 +125,7 @@
"Attachments": true,
"Boards": true,
"CardComments": true,
"DatePicker" : true,
"DatePicker": true,
"Cards": true,
"CustomFields": 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.attachSchema(new SimpleSchema({
_id: {
type: String,
},
booleanValue: {
type: Boolean,
optional: true,
},
sort: {
type: Number,
decimal: true,
},
}));
AccountSettings.attachSchema(
new SimpleSchema({
_id: {
type: String,
},
booleanValue: {
type: Boolean,
optional: true,
},
sort: {
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({
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) {
Meteor.startup(() => {
AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, {
$setOnInsert: {
booleanValue: false,
sort: 0,
},
});
AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, {
$setOnInsert: {
booleanValue: false,
sort: 1,
},
});
AccountSettings._collection._ensureIndex({ modifiedAt: -1 });
AccountSettings.upsert(
{ _id: 'accounts-allowEmailChange' },
{
$setOnInsert: {
booleanValue: false,
sort: 0,
},
}
);
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.allow({
@ -17,3 +19,16 @@ Actions.helpers({
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) => {
const activity = Activities._transform(doc);
RulesHelper.executeRules(activity);
});
Activities.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = Date.now();
});
if (Meteor.isServer) {
@ -78,11 +82,21 @@ if (Meteor.isServer) {
// are largely used in the App. See #524.
Meteor.startup(() => {
Activities._collection._ensureIndex({ createdAt: -1 });
Activities._collection._ensureIndex({ modifiedAt: -1 });
Activities._collection._ensureIndex({ cardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ boardId: 1, createdAt: -1 });
Activities._collection._ensureIndex({ commentId: 1 }, { partialFilterExpression: { commentId: { $exists: true } } });
Activities._collection._ensureIndex({ attachmentId: 1 }, { partialFilterExpression: { attachmentId: { $exists: true } } });
Activities._collection._ensureIndex({ customFieldId: 1 }, { partialFilterExpression: { customFieldId: { $exists: true } } });
Activities._collection._ensureIndex(
{ commentId: 1 },
{ 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.
//Activities._collection._dropIndex({ labelId: 1 }, { "indexKey": -1 });
//Activities._collection._dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
@ -189,18 +203,35 @@ if (Meteor.isServer) {
// params.labelId = activity.labelId;
//}
if (board) {
const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId');
const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId');
watchers = _.union(watchers, watchingUsers, _.intersection(participants, trackingUsers));
const watchingUsers = _.pluck(
_.where(board.watchers, { level: 'watching' }),
'userId'
);
const trackingUsers = _.pluck(
_.where(board.watchers, { level: 'tracking' }),
'userId'
);
watchers = _.union(
watchers,
watchingUsers,
_.intersection(participants, trackingUsers)
);
}
Notifications.getUsers(watchers).forEach((user) => {
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) {
Meteor.call('outgoingWebhooks', integrations, description, params);
}
});
}
export default Activities;

View file

@ -1,23 +1,49 @@
Announcements = new Mongo.Collection('announcements');
Announcements.attachSchema(new SimpleSchema({
enabled: {
type: Boolean,
defaultValue: false,
},
title: {
type: String,
optional: true,
},
body: {
type: String,
optional: true,
},
sort: {
type: Number,
decimal: true,
},
}));
Announcements.attachSchema(
new SimpleSchema({
enabled: {
type: Boolean,
defaultValue: false,
},
title: {
type: String,
optional: true,
},
body: {
type: String,
optional: true,
},
sort: {
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({
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) {
Meteor.startup(() => {
Announcements._collection._ensureIndex({ modifiedAt: -1 });
const announcements = Announcements.findOne({});
if(!announcements){
Announcements.insert({enabled: false, sort: 0});
if (!announcements) {
Announcements.insert({ enabled: false, sort: 0 });
}
});
}
export default Announcements;

View file

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

View file

@ -1,7 +1,5 @@
Avatars = new FS.Collection('avatars', {
stores: [
new FS.Store.GridFS('avatars'),
],
stores: [new FS.Store.GridFS('avatars')],
filter: {
maxSize: 72000,
allow: {
@ -18,10 +16,14 @@ Avatars.allow({
insert: isOwner,
update: isOwner,
remove: isOwner,
download() { return true; },
download() {
return true;
},
fetch: ['userId'],
});
Avatars.files.before.insert((userId, doc) => {
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
*/
CardComments.attachSchema(new SimpleSchema({
boardId: {
/**
* the board ID
*/
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();
}
CardComments.attachSchema(
new SimpleSchema({
boardId: {
/**
* the board ID
*/
type: String,
},
},
// XXX Should probably be called `authorId`
userId: {
/**
* the author ID of the comment
*/
type: String,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
cardId: {
/**
* the card ID
*/
type: String,
},
},
}));
// 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({
insert(userId, doc) {
@ -80,7 +94,7 @@ CardComments.helpers({
CardComments.hookOptions.after.update = { fetchPrevious: false };
function commentCreation(userId, doc){
function commentCreation(userId, doc) {
const card = Cards.findOne(doc.cardId);
Activities.insert({
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) {
// Comments are often fetched within a card, so we create an index to make these
// queries more efficient.
Meteor.startup(() => {
CardComments._collection._ensureIndex({ modifiedAt: -1 });
CardComments._collection._ensureIndex({ cardId: 1, createdAt: -1 });
});
@ -152,14 +172,20 @@ if (Meteor.isServer) {
* comment: 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 {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, {
code: 200,
data: CardComments.find({ boardId: paramBoardId, cardId: paramCardId}).map(function (doc) {
data: CardComments.find({
boardId: paramBoardId,
cardId: paramCardId,
}).map(function(doc) {
return {
_id: doc._id,
comment: doc.text,
@ -167,8 +193,7 @@ if (Meteor.isServer) {
};
}),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -185,24 +210,31 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to retrieve
* @return_type CardComments
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) {
try {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId;
JsonRoutes.sendResult(res, {
code: 200,
data: CardComments.findOne({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId }),
});
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId;
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
@ -214,35 +246,42 @@ if (Meteor.isServer) {
* @param {string} text the content of the comment
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) {
try {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
const id = CardComments.direct.insert({
userId: req.body.authorId,
text: req.body.comment,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/comments',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
const id = CardComments.direct.insert({
userId: req.body.authorId,
text: req.body.comment,
cardId: paramCardId,
boardId: paramBoardId,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
const cardComment = CardComments.findOne({
_id: id,
},
});
const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId });
commentCreation(req.body.authorId, cardComment);
cardId: paramCardId,
boardId: paramBoardId,
});
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
@ -253,25 +292,34 @@ if (Meteor.isServer) {
* @param {string} commentId the ID of the comment to delete
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) {
try {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId;
CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
},
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCommentId = req.params.commentId;
const paramCardId = req.params.cardId;
CardComments.remove({
_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
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} 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: {
/**
* 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
// queries more efficient.
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
// 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).
@ -1581,6 +1595,11 @@ if (Meteor.isServer) {
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 also card_comments / checklists / attachments
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
*/
ChecklistItems.attachSchema(new SimpleSchema({
title: {
/**
* the text of the item
*/
type: String,
},
sort: {
/**
* the sorting field of the item
*/
type: Number,
decimal: true,
},
isFinished: {
/**
* Is the item checked?
*/
type: Boolean,
defaultValue: false,
},
checklistId: {
/**
* the checklist ID the item is attached to
*/
type: String,
},
cardId: {
/**
* the card ID the item is attached to
*/
type: String,
},
}));
ChecklistItems.attachSchema(
new SimpleSchema({
title: {
/**
* the text of the item
*/
type: String,
},
sort: {
/**
* the sorting field of the item
*/
type: Number,
decimal: true,
},
isFinished: {
/**
* Is the item checked?
*/
type: Boolean,
defaultValue: false,
},
checklistId: {
/**
* the checklist ID the item is attached to
*/
type: String,
},
cardId: {
/**
* the card ID the item is attached to
*/
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({
insert(userId, doc) {
@ -62,10 +88,10 @@ ChecklistItems.mutations({
setTitle(title) {
return { $set: { title } };
},
check(){
check() {
return { $set: { isFinished: true } };
},
uncheck(){
uncheck() {
return { $set: { isFinished: false } };
},
toggleItem() {
@ -79,7 +105,7 @@ ChecklistItems.mutations({
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 boardId = card.boardId;
let activityType;
if(doc.isFinished){
if (doc.isFinished) {
activityType = 'checkedItem';
}else{
} else {
activityType = 'uncheckedItem';
}
const act = {
@ -122,19 +148,19 @@ function publishCheckActivity(userId, doc){
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
checklistItemName:doc.title,
checklistItemName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
};
Activities.insert(act);
}
function publishChekListCompleted(userId, doc){
function publishChekListCompleted(userId, doc) {
const card = Cards.findOne(doc.cardId);
const boardId = card.boardId;
const checklistId = doc.checklistId;
const checkList = Checklists.findOne({_id:checklistId});
if(checkList.isFinished()){
const checkList = Checklists.findOne({ _id: checklistId });
if (checkList.isFinished()) {
const act = {
userId,
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 boardId = card.boardId;
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
// Currently in checklist all are set as uncompleted/not checked,
// 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
// Maybe something related here?
// wekan/client/components/rules/triggers/checklistTriggers.js
if(checkList.isFinished()){
if (checkList.isFinished()) {
const act = {
userId,
activityType: 'uncompleteChecklist',
@ -185,6 +211,7 @@ function publishChekListUncompleted(userId, doc){
// Activities
if (Meteor.isServer) {
Meteor.startup(() => {
ChecklistItems._collection._ensureIndex({ modifiedAt: -1 });
ChecklistItems._collection._ensureIndex({ checklistId: 1 });
ChecklistItems._collection._ensureIndex({ cardId: 1 });
});
@ -198,6 +225,10 @@ if (Meteor.isServer) {
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) => {
itemCreation(userId, doc);
@ -214,7 +245,7 @@ if (Meteor.isServer) {
boardId,
checklistId: doc.checklistId,
checklistItemId: doc._id,
checklistItemName:doc.title,
checklistItemName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
@ -233,21 +264,25 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item
* @return_type ChecklistItems
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramItemId = req.params.itemId;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklistItem,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId;
const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
if (checklistItem) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklistItem,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
}
});
);
/**
* @operation edit_checklist_item
@ -262,25 +297,35 @@ if (Meteor.isServer) {
* @param {string} [title] the new text of the item
* @return_type {_id: string}
*/
JsonRoutes.add('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add(
'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')) {
ChecklistItems.direct.update({_id: paramItemId}, {$set: {isFinished: req.body.isFinished}});
if (req.body.hasOwnProperty('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
@ -295,15 +340,21 @@ if (Meteor.isServer) {
* @param {string} itemId the ID of the item to be removed
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramItemId = req.params.itemId;
ChecklistItems.direct.remove({ _id: paramItemId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramItemId,
},
});
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramItemId = req.params.itemId;
ChecklistItems.direct.remove({ _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
*/
Checklists.attachSchema(new SimpleSchema({
cardId: {
/**
* The ID of the card the checklist is in
*/
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();
}
Checklists.attachSchema(
new SimpleSchema({
cardId: {
/**
* The ID of the card the checklist is in
*/
type: String,
},
},
sort: {
/**
* sorting value of the checklist
*/
type: Number,
decimal: true,
},
}));
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,
// 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({
copy(newCardId) {
@ -53,7 +68,7 @@ Checklists.helpers({
this._id = null;
this.cardId = newCardId;
const newChecklistId = Checklists.insert(this);
ChecklistItems.find({checklistId: oldChecklistId}).forEach((item) => {
ChecklistItems.find({ checklistId: oldChecklistId }).forEach((item) => {
item._id = null;
item.checklistId = newChecklistId;
item.cardId = newCardId;
@ -65,9 +80,12 @@ Checklists.helpers({
return ChecklistItems.find({ checklistId: this._id }).count();
},
items() {
return ChecklistItems.find({
checklistId: this._id,
}, { sort: ['sort'] });
return ChecklistItems.find(
{
checklistId: this._id,
},
{ sort: ['sort'] }
);
},
finishedCount() {
return ChecklistItems.find({
@ -78,20 +96,20 @@ Checklists.helpers({
isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
},
checkAllItems(){
const checkItems = ChecklistItems.find({checklistId: this._id});
checkItems.forEach(function(item){
checkAllItems() {
const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item) {
item.check();
});
},
uncheckAllItems(){
const checkItems = ChecklistItems.find({checklistId: this._id});
checkItems.forEach(function(item){
uncheckAllItems() {
const checkItems = ChecklistItems.find({ checklistId: this._id });
checkItems.forEach(function(item) {
item.uncheck();
});
},
itemIndex(itemId) {
const items = self.findOne({_id : this._id}).items;
const items = self.findOne({ _id: this._id }).items;
return _.pluck(items, '_id').indexOf(itemId);
},
});
@ -124,6 +142,7 @@ Checklists.mutations({
if (Meteor.isServer) {
Meteor.startup(() => {
Checklists._collection._ensureIndex({ modifiedAt: -1 });
Checklists._collection._ensureIndex({ cardId: 1, createdAt: 1 });
});
@ -135,12 +154,17 @@ if (Meteor.isServer) {
cardId: doc.cardId,
boardId: card.boardId,
checklistId: doc._id,
checklistName:doc.title,
checklistName: doc.title,
listId: card.listId,
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) => {
const activities = Activities.find({ checklistId: doc._id });
const card = Cards.findOne(doc.cardId);
@ -155,7 +179,7 @@ if (Meteor.isServer) {
cardId: doc.cardId,
boardId: Cards.findOne(doc.cardId).boardId,
checklistId: doc._id,
checklistName:doc.title,
checklistName: doc.title,
listId: card.listId,
swimlaneId: card.swimlaneId,
});
@ -172,26 +196,32 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
};
});
if (checklists) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklists,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId;
const checklists = Checklists.find({ cardId: paramCardId }).map(function(
doc
) {
return {
_id: doc._id,
title: doc.title,
};
});
if (checklists) {
JsonRoutes.sendResult(res, {
code: 200,
data: checklists,
});
} else {
JsonRoutes.sendResult(res, {
code: 500,
});
}
}
});
);
/**
* @operation get_checklist
@ -209,29 +239,38 @@ if (Meteor.isServer) {
* title: string,
* isFinished: boolean}]}
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId });
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,
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId;
const paramCardId = req.params.cardId;
const checklist = Checklists.findOne({
_id: paramChecklistId,
cardId: paramCardId,
});
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
@ -242,36 +281,40 @@ if (Meteor.isServer) {
* @param {string} title the title of the new checklist
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add(
'POST',
'/api/boards/:boardId/cards/:cardId/checklists',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramCardId = req.params.cardId;
const id = Checklists.insert({
title: req.body.title,
cardId: paramCardId,
sort: 0,
});
if (id) {
req.body.items.forEach(function (item, idx) {
ChecklistItems.insert({
cardId: paramCardId,
checklistId: id,
title: item.title,
sort: idx,
const paramCardId = req.params.cardId;
const id = Checklists.insert({
title: req.body.title,
cardId: paramCardId,
sort: 0,
});
if (id) {
req.body.items.forEach(function(item, idx) {
ChecklistItems.insert({
cardId: paramCardId,
checklistId: id,
title: item.title,
sort: idx,
});
});
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
} else {
JsonRoutes.sendResult(res, {
code: 400,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
} else {
JsonRoutes.sendResult(res, {
code: 400,
});
}
}
});
);
/**
* @operation delete_checklist
@ -284,15 +327,21 @@ if (Meteor.isServer) {
* @param {string} checklistId the ID of the checklist to remove
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramChecklistId = req.params.checklistId;
Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramChecklistId,
},
});
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramChecklistId = req.params.checklistId;
Checklists.remove({ _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
*/
CustomFields.attachSchema(new SimpleSchema({
boardIds: {
/**
* the ID of the board
*/
type: [String],
},
name: {
/**
* name of the custom field
*/
type: String,
},
type: {
/**
* type of the custom field
*/
type: String,
allowedValues: ['text', 'number', 'date', 'dropdown'],
},
settings: {
/**
* settings of the custom field
*/
type: Object,
},
'settings.dropdownItems': {
/**
* list of drop down items objects
*/
type: [Object],
optional: true,
},
'settings.dropdownItems.$': {
type: new SimpleSchema({
_id: {
/**
* ID of the drop down item
*/
type: String,
CustomFields.attachSchema(
new SimpleSchema({
boardIds: {
/**
* the ID of the board
*/
type: [String],
},
name: {
/**
* name of the custom field
*/
type: String,
},
type: {
/**
* type of the custom field
*/
type: String,
allowedValues: ['text', 'number', 'date', 'dropdown'],
},
settings: {
/**
* settings of the custom field
*/
type: Object,
},
'settings.dropdownItems': {
/**
* list of drop down items objects
*/
type: [Object],
optional: true,
},
'settings.dropdownItems.$': {
type: new SimpleSchema({
_id: {
/**
* ID of the drop down item
*/
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: {
/**
* name of the drop down item
*/
type: String,
},
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();
}
},
}),
},
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({
addBoard(boardId) {
@ -88,19 +114,28 @@ CustomFields.mutations({
CustomFields.allow({
insert(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
_id: {$in: doc.boardIds},
}).fetch());
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
},
update(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
_id: {$in: doc.boardIds},
}).fetch());
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
},
remove(userId, doc) {
return allowIsAnyBoardMember(userId, Boards.find({
_id: {$in: doc.boardIds},
}).fetch());
return allowIsAnyBoardMember(
userId,
Boards.find({
_id: { $in: doc.boardIds },
}).fetch()
);
},
fetch: ['userId', 'boardIds'],
});
@ -108,7 +143,7 @@ CustomFields.allow({
// not sure if we need this?
//CustomFields.hookOptions.after.update = { fetchPrevious: false };
function customFieldCreation(userId, doc){
function customFieldCreation(userId, doc) {
Activities.insert({
userId,
activityType: 'createCustomField',
@ -142,6 +177,7 @@ function customFieldEdit(userId, doc){
if (Meteor.isServer) {
Meteor.startup(() => {
CustomFields._collection._ensureIndex({ modifiedAt: -1 });
CustomFields._collection._ensureIndex({ boardIds: 1 });
});
@ -149,12 +185,17 @@ if (Meteor.isServer) {
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) => {
if (_.contains(fieldNames, 'boardIds') && modifier.$pull) {
Cards.update(
{boardId: modifier.$pull.boardIds, 'customFields._id': doc._id},
{$pull: {'customFields': {'_id': doc._id}}},
{multi: true}
{ boardId: modifier.$pull.boardIds, 'customFields._id': doc._id },
{ $pull: { customFields: { _id: doc._id } } },
{ multi: true }
);
customFieldEdit(userId, doc);
Activities.remove({
@ -180,9 +221,9 @@ if (Meteor.isServer) {
});
Cards.update(
{boardId: {$in: doc.boardIds}, 'customFields._id': doc._id},
{$pull: {'customFields': {'_id': doc._id}}},
{multi: true}
{ boardId: { $in: doc.boardIds }, 'customFields._id': doc._id },
{ $pull: { customFields: { _id: doc._id } } },
{ multi: true }
);
});
}
@ -198,18 +239,23 @@ if (Meteor.isServer) {
* name: string,
* type: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields', function(
req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.find({ boardIds: {$in: [paramBoardId]} }).map(function (cf) {
return {
_id: cf._id,
name: cf.name,
type: cf.type,
};
}),
data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
function(cf) {
return {
_id: cf._id,
name: cf.name,
type: cf.type,
};
}
),
});
});
@ -221,15 +267,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field
* @return_type CustomFields
*/
JsonRoutes.add('GET', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.findOne({ _id: paramCustomFieldId, boardIds: {$in: [paramBoardId]} }),
});
});
JsonRoutes.add(
'GET',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId;
JsonRoutes.sendResult(res, {
code: 200,
data: CustomFields.findOne({
_id: paramCustomFieldId,
boardIds: { $in: [paramBoardId] },
}),
});
}
);
/**
* @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?
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function (req, res) {
Authentication.checkUserId( req.userId);
JsonRoutes.add('POST', '/api/boards/:boardId/custom-fields', function(
req,
res
) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const id = CustomFields.direct.insert({
name: req.body.name,
@ -254,10 +310,13 @@ if (Meteor.isServer) {
showOnCard: req.body.showOnCard,
automaticallyOnCard: req.body.automaticallyOnCard,
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);
JsonRoutes.sendResult(res, {
@ -278,16 +337,22 @@ if (Meteor.isServer) {
* @param {string} customFieldId the ID of the custom field
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/custom-fields/:customFieldId', function (req, res) {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: {$in: [paramBoardId]} });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/custom-fields/:customFieldId',
function(req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const id = req.params.customFieldId;
CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
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
*/
Integrations.attachSchema(new SimpleSchema({
enabled: {
/**
* is the integration enabled?
*/
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();
}
Integrations.attachSchema(
new SimpleSchema({
enabled: {
/**
* is the integration enabled?
*/
type: Boolean,
defaultValue: true,
},
},
userId: {
/**
* user ID who created the interation
*/
type: String,
},
}));
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,
// 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({
insert(userId, doc) {
@ -89,6 +110,7 @@ Integrations.allow({
//INTEGRATIONS REST API
if (Meteor.isServer) {
Meteor.startup(() => {
Integrations._collection._ensureIndex({ modifiedAt: -1 });
Integrations._collection._ensureIndex({ boardId: 1 });
});
@ -99,18 +121,23 @@ if (Meteor.isServer) {
* @param {string} boardId the board ID
* @return_type [Integrations]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(
req,
res
) {
try {
const paramBoardId = req.params.boardId;
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;
});
JsonRoutes.sendResult(res, {code: 200, data});
}
catch (error) {
JsonRoutes.sendResult(res, { code: 200, data });
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -126,7 +153,10 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID
* @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 {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
@ -134,10 +164,12 @@ if (Meteor.isServer) {
JsonRoutes.sendResult(res, {
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, {
code: 200,
data: error,
@ -153,7 +185,10 @@ if (Meteor.isServer) {
* @param {string} url the URL of the integration
* @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 {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
@ -170,8 +205,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -192,7 +226,10 @@ if (Meteor.isServer) {
* @param {string} [activities] new list of activities of the integration
* @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 {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
@ -200,28 +237,38 @@ if (Meteor.isServer) {
if (req.body.hasOwnProperty('enabled')) {
const newEnabled = req.body.enabled;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
{$set: {enabled: newEnabled}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $set: { enabled: newEnabled } }
);
}
if (req.body.hasOwnProperty('title')) {
const newTitle = req.body.title;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
{$set: {title: newTitle}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $set: { title: newTitle } }
);
}
if (req.body.hasOwnProperty('url')) {
const newUrl = req.body.url;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
{$set: {url: newUrl}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $set: { url: newUrl } }
);
}
if (req.body.hasOwnProperty('token')) {
const newToken = req.body.token;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
{$set: {token: newToken}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $set: { token: newToken } }
);
}
if (req.body.hasOwnProperty('activities')) {
const newActivities = req.body.activities;
Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
{$set: {activities: newActivities}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $set: { activities: newActivities } }
);
}
JsonRoutes.sendResult(res, {
@ -230,8 +277,7 @@ if (Meteor.isServer) {
_id: paramIntId,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -248,28 +294,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to remove from the integration
* @return_type Integrations
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) {
try {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/integrations/:intId/activities',
function(req, res) {
try {
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},
{$pullAll: {activities: newActivities}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $pullAll: { activities: newActivities } }
);
JsonRoutes.sendResult(res, {
code: 200,
data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}),
});
JsonRoutes.sendResult(res, {
code: 200,
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
@ -280,28 +334,36 @@ if (Meteor.isServer) {
* @param {string} newActivities the activities to add to the integration
* @return_type Integrations
*/
JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res) {
try {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
const newActivities = req.body.activities;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.add(
'POST',
'/api/boards/:boardId/integrations/:intId/activities',
function(req, res) {
try {
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},
{$addToSet: {activities: { $each: newActivities}}});
Integrations.direct.update(
{ _id: paramIntId, boardId: paramBoardId },
{ $addToSet: { activities: { $each: newActivities } } }
);
JsonRoutes.sendResult(res, {
code: 200,
data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}),
});
JsonRoutes.sendResult(res, {
code: 200,
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
@ -311,21 +373,23 @@ if (Meteor.isServer) {
* @param {string} intId the integration ID
* @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 {
const paramBoardId = req.params.boardId;
const paramIntId = req.params.intId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId});
Integrations.direct.remove({ _id: paramIntId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramIntId,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
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.attachSchema(new SimpleSchema({
code: {
type: String,
},
email: {
type: String,
unique: true,
regEx: SimpleSchema.RegEx.Email,
},
createdAt: {
type: Date,
denyUpdate: false,
},
// always be the admin if only one admin
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
defaultValue: true,
},
}));
InvitationCodes.attachSchema(
new SimpleSchema({
code: {
type: String,
},
email: {
type: String,
unique: true,
regEx: SimpleSchema.RegEx.Email,
},
createdAt: {
type: Date,
denyUpdate: false,
optional: true,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
// always be the admin if only one admin
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
defaultValue: true,
},
})
);
InvitationCodes.helpers({
author(){
author() {
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) => {
// doc.createdAt = new Date();
// doc.authorId = userId;
// });
if (Meteor.isServer) {
Meteor.startup(() => {
InvitationCodes._collection._ensureIndex({ modifiedAt: -1 });
});
Boards.deny({
fetch: ['members'],
});
}
export default InvitationCodes;

View file

@ -3,125 +3,161 @@ Lists = new Mongo.Collection('lists');
/**
* A list (column) in the Wekan board.
*/
Lists.attachSchema(new SimpleSchema({
title: {
/**
* the title of the list
*/
type: String,
},
archived: {
/**
* is the list archived
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
Lists.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the list
*/
type: String,
},
},
boardId: {
/**
* the board associated to this list
*/
type: String,
},
swimlaneId: {
/**
* 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();
}
archived: {
/**
* is the list archived
*/
type: Boolean,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return false;
}
},
},
},
sort: {
/**
* is the list sorted
*/
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();
}
boardId: {
/**
* the board associated to this list
*/
type: String,
},
},
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',
},
}));
swimlaneId: {
/**
* the swimlane associated to this list. Used for templates
*/
type: String,
defaultValue: '',
},
createdAt: {
/**
* creation date
*/
type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
sort: {
/**
* is the list sorted
*/
type: Number,
decimal: true,
// XXX We should probably provide a default
optional: true,
},
updatedAt: {
/**
* last update of the list
*/
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();
}
},
},
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({
insert(userId, doc) {
@ -172,10 +208,8 @@ Lists.helpers({
listId: this._id,
archived: false,
};
if (swimlaneId)
selector.swimlaneId = swimlaneId;
return Cards.find(Filter.mongoSelector(selector),
{ sort: ['sort'] });
if (swimlaneId) selector.swimlaneId = swimlaneId;
return Cards.find(Filter.mongoSelector(selector), { sort: ['sort'] });
},
cardsUnfiltered(swimlaneId) {
@ -183,10 +217,8 @@ Lists.helpers({
listId: this._id,
archived: false,
};
if (swimlaneId)
selector.swimlaneId = swimlaneId;
return Cards.find(selector,
{ sort: ['sort'] });
if (swimlaneId) selector.swimlaneId = swimlaneId;
return Cards.find(selector, { sort: ['sort'] });
},
allCards() {
@ -197,11 +229,12 @@ Lists.helpers({
return Boards.findOne(this.boardId);
},
getWipLimit(option){
getWipLimit(option) {
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;
} else if(!option) {
} else if (!option) {
return list.wipLimit;
} 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
@ -209,8 +242,7 @@ Lists.helpers({
},
colorClass() {
if (this.color)
return this.color;
if (this.color) return this.color;
return '';
},
@ -219,7 +251,7 @@ Lists.helpers({
},
remove() {
Lists.remove({ _id: this._id});
Lists.remove({ _id: this._id });
},
});
@ -271,10 +303,10 @@ Lists.mutations({
});
Meteor.methods({
applyWipLimit(listId, limit){
applyWipLimit(listId, limit) {
check(listId, String);
check(limit, Number);
if(limit === 0){
if (limit === 0) {
limit = 1;
}
Lists.findOne({ _id: listId }).setWipLimit(limit);
@ -283,7 +315,7 @@ Meteor.methods({
enableWipLimit(listId) {
check(listId, String);
const list = Lists.findOne({ _id: listId });
if(list.getWipLimit('value') === 0){
if (list.getWipLimit('value') === 0) {
list.setWipLimit(1);
}
list.toggleWipLimit(!list.getWipLimit('enabled'));
@ -300,6 +332,7 @@ Lists.hookOptions.after.update = { fetchPrevious: false };
if (Meteor.isServer) {
Meteor.startup(() => {
Lists._collection._ensureIndex({ modifiedAt: -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) => {
const cards = Cards.find({ listId: doc._id });
if (cards) {
@ -353,22 +391,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/lists', function(req, res) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Lists.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
};
}),
data: Lists.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return {
_id: doc._id,
title: doc.title,
};
}
),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -384,17 +423,23 @@ if (Meteor.isServer) {
* @param {string} listId the List ID
* @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 {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
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, {
code: 200,
data: error,
@ -410,9 +455,9 @@ if (Meteor.isServer) {
* @param {string} title the title of the List
* @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 {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId);
const id = Lists.insert({
@ -426,8 +471,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -446,9 +490,12 @@ if (Meteor.isServer) {
* @param {string} listId the ID of the list to remove
* @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 {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Lists.remove({ _id: paramListId, boardId: paramBoardId });
@ -458,13 +505,13 @@ if (Meteor.isServer) {
_id: paramListId,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
}
export default Lists;

View file

@ -1,23 +1,51 @@
import { Meteor } from 'meteor/meteor';
Rules = new Mongo.Collection('rules');
Rules.attachSchema(new SimpleSchema({
title: {
type: String,
optional: false,
},
triggerId: {
type: String,
optional: false,
},
actionId: {
type: String,
optional: false,
},
boardId: {
type: String,
optional: false,
},
}));
Rules.attachSchema(
new SimpleSchema({
title: {
type: String,
optional: false,
},
triggerId: {
type: String,
optional: false,
},
actionId: {
type: String,
optional: false,
},
boardId: {
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({
rename(description) {
@ -26,15 +54,14 @@ Rules.mutations({
});
Rules.helpers({
getAction(){
return Actions.findOne({_id:this.actionId});
getAction() {
return Actions.findOne({ _id: this.actionId });
},
getTrigger(){
return Triggers.findOne({_id:this.triggerId});
getTrigger() {
return Triggers.findOne({ _id: this.triggerId });
},
});
Rules.allow({
insert(userId, doc) {
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
@ -46,3 +73,16 @@ Rules.allow({
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.attachSchema(new SimpleSchema({
disableRegistration: {
type: Boolean,
},
'mailServer.username': {
type: String,
optional: true,
},
'mailServer.password': {
type: String,
optional: true,
},
'mailServer.host': {
type: String,
optional: true,
},
'mailServer.port': {
type: String,
optional: true,
},
'mailServer.enableTLS': {
type: Boolean,
optional: true,
},
'mailServer.from': {
type: String,
optional: true,
},
productName: {
type: String,
optional: true,
},
customHTMLafterBodyStart: {
type: String,
optional: true,
},
customHTMLbeforeBodyEnd: {
type: String,
optional: true,
},
displayAuthenticationMethod: {
type: Boolean,
optional: true,
},
defaultAuthenticationMethod: {
type: String,
optional: false,
},
hideLogo: {
type: Boolean,
optional: true,
},
createdAt: {
type: Date,
denyUpdate: true,
},
modifiedAt: {
type: Date,
},
}));
Settings.attachSchema(
new SimpleSchema({
disableRegistration: {
type: Boolean,
},
'mailServer.username': {
type: String,
optional: true,
},
'mailServer.password': {
type: String,
optional: true,
},
'mailServer.host': {
type: String,
optional: true,
},
'mailServer.port': {
type: String,
optional: true,
},
'mailServer.enableTLS': {
type: Boolean,
optional: true,
},
'mailServer.from': {
type: String,
optional: true,
},
productName: {
type: String,
optional: true,
},
customHTMLafterBodyStart: {
type: String,
optional: true,
},
customHTMLbeforeBodyEnd: {
type: String,
optional: true,
},
displayAuthenticationMethod: {
type: Boolean,
optional: true,
},
defaultAuthenticationMethod: {
type: String,
optional: false,
},
hideLogo: {
type: Boolean,
optional: true,
},
createdAt: {
type: Date,
denyUpdate: true,
// eslint-disable-next-line consistent-return
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({
mailUrl () {
mailUrl() {
if (!this.mailServer.host) {
return null;
}
@ -69,7 +87,9 @@ Settings.helpers({
if (!this.mailServer.username && !this.mailServer.password) {
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({
@ -86,50 +106,75 @@ Settings.before.update((userId, doc, fieldNames, modifier) => {
if (Meteor.isServer) {
Meteor.startup(() => {
Settings._collection._ensureIndex({ modifiedAt: -1 });
const setting = Settings.findOne({});
if(!setting){
if (!setting) {
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 defaultSetting = {disableRegistration: false, mailServer: {
username: '', password: '', host: '', port: '', enableTLS: false, from,
}, createdAt: now, modifiedAt: now, displayAuthenticationMethod: true,
defaultAuthenticationMethod: 'password'};
const defaultSetting = {
disableRegistration: false,
mailServer: {
username: '',
password: '',
host: '',
port: '',
enableTLS: false,
from,
},
createdAt: now,
modifiedAt: now,
displayAuthenticationMethod: true,
defaultAuthenticationMethod: 'password',
};
Settings.insert(defaultSetting);
}
const newSetting = Settings.findOne();
if (!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) => {
// assign new values to mail-from & MAIL_URL in environment
if (_.contains(fieldNames, 'mailServer') && doc.mailServer.host) {
const protocol = doc.mailServer.enableTLS ? 'smtps://' : 'smtp://';
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 {
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;
}
});
function getRandomNum (min, max) {
function getRandomNum(min, max) {
const range = max - min;
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];
if (value){
if (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 author = Users.findOne(Meteor.userId());
try {
@ -172,30 +217,47 @@ if (Meteor.isServer) {
check(boards, [String]);
const user = Users.findOne(Meteor.userId());
if(!user.isAdmin){
if (!user.isAdmin) {
throw new Meteor.Error('not-allowed');
}
emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email)) {
// Checks if the email is already link to an account.
const userExist = Users.findOne({email});
if (userExist){
throw new Meteor.Error('user-exist', `The user with the email ${email} has already an account.`);
const userExist = Users.findOne({ email });
if (userExist) {
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.
const invitation = InvitationCodes.findOne({email});
if (invitation){
InvitationCodes.update(invitation, {$set : {boardsToBeInvited: boards}});
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);
}
const invitation = InvitationCodes.findOne({ email });
if (invitation) {
InvitationCodes.update(invitation, {
$set: { boardsToBeInvited: boards },
});
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({
to: user.emails[0].address,
from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-smtp-test-subject', {lng: lang}),
text: TAPi18n.__('email-smtp-test-text', {lng: lang}),
subject: TAPi18n.__('email-smtp-test-subject', { lng: lang }),
text: TAPi18n.__('email-smtp-test-text', { lng: lang }),
});
} catch ({message}) {
throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message);
} catch ({ message }) {
throw new Meteor.Error(
'email-fail',
`${TAPi18n.__('email-fail-text', { lng: lang })}: ${message}`,
message
);
}
return {
message: 'email-sent',
@ -227,7 +293,7 @@ if (Meteor.isServer) {
};
},
getCustomUI(){
getCustomUI() {
const setting = Settings.findOne({});
if (!setting.productName) {
return {
@ -240,7 +306,7 @@ if (Meteor.isServer) {
}
},
getMatomoConf(){
getMatomoConf() {
return {
address: getEnvVar('MATOMO_ADDRESS'),
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.
*/
Swimlanes.attachSchema(new SimpleSchema({
title: {
/**
* the title of the swimlane
*/
type: String,
},
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
Swimlanes.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the swimlane
*/
type: String,
},
},
boardId: {
/**
* the ID of the board the swimlane is attached to
*/
type: String,
},
createdAt: {
/**
* creation date of the swimlane
*/
type: Date,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert) {
return new Date();
} else {
this.unset();
}
archived: {
/**
* is the swimlane archived?
*/
type: Boolean,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert && !this.isSet) {
return false;
}
},
},
},
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,
autoValue() { // eslint-disable-line consistent-return
if (this.isUpdate) {
return new Date();
} else {
this.unset();
}
boardId: {
/**
* the ID of the board the swimlane is attached to
*/
type: String,
},
},
type: {
/**
* The type of swimlane
*/
type: String,
defaultValue: 'swimlane',
},
}));
createdAt: {
/**
* creation date of the swimlane
*/
type: Date,
// eslint-disable-next-line consistent-return
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({
insert(userId, doc) {
@ -109,7 +145,7 @@ Swimlanes.helpers({
const _id = Swimlanes.insert(this);
const query = {
swimlaneId: {$in: [oldId, '']},
swimlaneId: { $in: [oldId, ''] },
archived: false,
};
if (oldBoardId) {
@ -126,18 +162,24 @@ Swimlanes.helpers({
},
cards() {
return Cards.find(Filter.mongoSelector({
swimlaneId: this._id,
archived: false,
}), { sort: ['sort'] });
return Cards.find(
Filter.mongoSelector({
swimlaneId: this._id,
archived: false,
}),
{ sort: ['sort'] }
);
},
lists() {
return Lists.find({
boardId: this.boardId,
swimlaneId: {$in: [this._id, '']},
archived: false,
}, { sort: ['sort'] });
return Lists.find(
{
boardId: this.boardId,
swimlaneId: { $in: [this._id, ''] },
archived: false,
},
{ sort: ['sort'] }
);
},
myLists() {
@ -153,8 +195,7 @@ Swimlanes.helpers({
},
colorClass() {
if (this.color)
return this.color;
if (this.color) return this.color;
return '';
},
@ -182,7 +223,7 @@ Swimlanes.helpers({
},
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 };
if (Meteor.isServer) {
Meteor.startup(() => {
Swimlanes._collection._ensureIndex({ modifiedAt: -1 });
Swimlanes._collection._ensureIndex({ boardId: 1 });
});
@ -239,18 +286,21 @@ if (Meteor.isServer) {
});
Swimlanes.before.remove(function(userId, doc) {
const lists = Lists.find({
boardId: doc.boardId,
swimlaneId: {$in: [doc._id, '']},
archived: false,
}, { sort: ['sort'] });
const lists = Lists.find(
{
boardId: doc.boardId,
swimlaneId: { $in: [doc._id, ''] },
archived: false,
},
{ sort: ['sort'] }
);
if (lists.count() < 2) {
lists.forEach((list) => {
list.remove();
});
} else {
Cards.remove({swimlaneId: doc._id});
Cards.remove({ swimlaneId: doc._id });
}
Activities.insert({
@ -287,22 +337,23 @@ if (Meteor.isServer) {
* @return_type [{_id: string,
* title: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) {
JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function(req, res) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(function (doc) {
return {
_id: doc._id,
title: doc.title,
};
}),
data: Swimlanes.find({ boardId: paramBoardId, archived: false }).map(
function(doc) {
return {
_id: doc._id,
title: doc.title,
};
}
),
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -319,17 +370,23 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane
* @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 {
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Authentication.checkBoardAccess( req.userId, paramBoardId);
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
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, {
code: 200,
data: error,
@ -346,9 +403,9 @@ if (Meteor.isServer) {
* @param {string} title the new title of the swimlane
* @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 {
Authentication.checkUserId( req.userId);
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const board = Boards.findOne(paramBoardId);
const id = Swimlanes.insert({
@ -362,8 +419,7 @@ if (Meteor.isServer) {
_id: id,
},
});
}
catch (error) {
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
@ -382,25 +438,29 @@ if (Meteor.isServer) {
* @param {string} swimlaneId the ID of the swimlane
* @return_type {_id: string}
*/
JsonRoutes.add('DELETE', '/api/boards/:boardId/swimlanes/:swimlaneId', function (req, res) {
try {
Authentication.checkUserId( req.userId);
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramSwimlaneId,
},
});
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/swimlanes/:swimlaneId',
function(req, res) {
try {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Swimlanes.remove({ _id: paramSwimlaneId, boardId: paramBoardId });
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.mutations({
@ -23,7 +25,6 @@ Triggers.allow({
});
Triggers.helpers({
description() {
return this.desc;
},
@ -56,3 +57,16 @@ Triggers.helpers({
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.
UnsavedEditCollection = new Mongo.Collection('unsaved-edits');
UnsavedEditCollection.attachSchema(new SimpleSchema({
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;
}
UnsavedEditCollection.attachSchema(
new SimpleSchema({
fieldName: {
type: String,
},
},
}));
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) {
function isAuthor(userId, doc, fieldNames = []) {
return userId === doc.userId && fieldNames.indexOf('userId') === -1;
}
Meteor.startup(() => {
UnsavedEditCollection._collection._ensureIndex({ modifiedAt: -1 });
UnsavedEditCollection._collection._ensureIndex({ userId: 1 });
});
UnsavedEditCollection.allow({
@ -36,3 +71,5 @@ if (Meteor.isServer) {
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",
"private": true,
"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"
},
"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": {
"extends": "@meteorjs/eslint-config-meteor"
},
@ -20,7 +40,17 @@
},
"homepage": "https://wekan.github.io",
"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": {
"@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
// compatible way you have to write a migration in this file using the following
// API:
@ -28,18 +48,22 @@ const noValidateMulti = { ...noValidate, multi: true };
Migrations.add('board-background-color', () => {
const defaultColor = '#16A085';
Boards.update({
background: {
$exists: false,
},
}, {
$set: {
Boards.update(
{
background: {
type: 'color',
color: defaultColor,
$exists: false,
},
},
}, noValidateMulti);
{
$set: {
background: {
type: 'color',
color: defaultColor,
},
},
},
noValidateMulti
);
});
Migrations.add('lowercase-board-permission', () => {
@ -57,24 +81,28 @@ Migrations.add('change-attachments-type-for-non-images', () => {
const newTypeForNonImage = 'application/octet-stream';
Attachments.find().forEach((file) => {
if (!file.isImage()) {
Attachments.update(file._id, {
$set: {
'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage,
Attachments.update(
file._id,
{
$set: {
'original.type': newTypeForNonImage,
'copies.attachments.type': newTypeForNonImage,
},
},
}, noValidate);
noValidate
);
}
});
});
Migrations.add('card-covers', () => {
Cards.find().forEach((card) => {
const cover = Attachments.findOne({ cardId: card._id, cover: true });
const cover = Attachments.findOne({ cardId: card._id, cover: true });
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', () => {
@ -89,26 +117,31 @@ Migrations.add('use-css-class-for-boards-colors', () => {
Boards.find().forEach((board) => {
const oldBoardColor = board.background.color;
const newBoardColor = associationTable[oldBoardColor];
Boards.update(board._id, {
$set: { color: newBoardColor },
$unset: { background: '' },
}, noValidate);
Boards.update(
board._id,
{
$set: { color: newBoardColor },
$unset: { background: '' },
},
noValidate
);
});
});
Migrations.add('denormalize-star-number-per-board', () => {
Boards.find().forEach((board) => {
const nStars = Users.find({'profile.starredBoards': board._id}).count();
Boards.update(board._id, {$set: {stars: nStars}}, noValidate);
const nStars = Users.find({ 'profile.starredBoards': board._id }).count();
Boards.update(board._id, { $set: { stars: nStars } }, noValidate);
});
});
// We want to keep a trace of former members so we can efficiently publish their
// infos in the general board publication.
Migrations.add('add-member-isactive-field', () => {
Boards.find({}, {fields: {members: 1}}).forEach((board) => {
Boards.find({}, { fields: { members: 1 } }).forEach((board) => {
const allUsersWithSomeActivity = _.chain(
Activities.find({ boardId: board._id }, { fields:{ userId:1 }}).fetch())
Activities.find({ boardId: board._id }, { fields: { userId: 1 } }).fetch()
)
.pluck('userId')
.uniq()
.value();
@ -127,7 +160,7 @@ Migrations.add('add-member-isactive-field', () => {
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
_.sortBy(checklist.items, 'sort').forEach((item, index) => {
ChecklistItems.direct.insert({
title: (item.title ? item.title : 'Checklist'),
title: item.title ? item.title : 'Checklist',
sort: index,
isFinished: item.isFinished,
checklistId: checklist._id,
@ -193,8 +226,9 @@ Migrations.add('add-checklist-items', () => {
});
// Delete old ones
Checklists.direct.update({ _id: checklist._id },
{ $unset: { items : 1 } },
Checklists.direct.update(
{ _id: checklist._id },
{ $unset: { items: 1 } },
noValidate
);
});
@ -217,324 +251,512 @@ Migrations.add('add-card-types', () => {
Cards.find().forEach((card) => {
Cards.direct.update(
{ _id: card._id },
{ $set: {
type: 'cardType-card',
linkedId: null } },
{
$set: {
type: 'cardType-card',
linkedId: null,
},
},
noValidate
);
});
});
Migrations.add('add-custom-fields-to-cards', () => {
Cards.update({
customFields: {
$exists: false,
Cards.update(
{
customFields: {
$exists: false,
},
},
}, {
$set: {
customFields:[],
{
$set: {
customFields: [],
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-requester-field', () => {
Cards.update({
requestedBy: {
$exists: false,
Cards.update(
{
requestedBy: {
$exists: false,
},
},
}, {
$set: {
requestedBy:'',
{
$set: {
requestedBy: '',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-assigner-field', () => {
Cards.update({
assignedBy: {
$exists: false,
Cards.update(
{
assignedBy: {
$exists: false,
},
},
}, {
$set: {
assignedBy:'',
{
$set: {
assignedBy: '',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-parent-field-to-cards', () => {
Cards.update({
parentId: {
$exists: false,
Cards.update(
{
parentId: {
$exists: false,
},
},
}, {
$set: {
parentId:'',
{
$set: {
parentId: '',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-subtasks-boards', () => {
Boards.update({
subtasksDefaultBoardId: {
$exists: false,
Boards.update(
{
subtasksDefaultBoardId: {
$exists: false,
},
},
}, {
$set: {
subtasksDefaultBoardId: null,
subtasksDefaultListId: null,
{
$set: {
subtasksDefaultBoardId: null,
subtasksDefaultListId: null,
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-subtasks-sort', () => {
Boards.update({
subtaskSort: {
$exists: false,
Boards.update(
{
subtaskSort: {
$exists: false,
},
},
}, {
$set: {
subtaskSort: -1,
{
$set: {
subtaskSort: -1,
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-subtasks-allowed', () => {
Boards.update({
allowsSubtasks: {
$exists: false,
Boards.update(
{
allowsSubtasks: {
$exists: false,
},
},
}, {
$set: {
allowsSubtasks: true,
{
$set: {
allowsSubtasks: true,
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-subtasks-allowed', () => {
Boards.update({
presentParentTask: {
$exists: false,
Boards.update(
{
presentParentTask: {
$exists: false,
},
},
}, {
$set: {
presentParentTask: 'no-parent',
{
$set: {
presentParentTask: 'no-parent',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-authenticationMethod', () => {
Users.update({
'authenticationMethod': {
$exists: false,
Users.update(
{
authenticationMethod: {
$exists: false,
},
},
}, {
$set: {
'authenticationMethod': 'password',
{
$set: {
authenticationMethod: 'password',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('remove-tag', () => {
Users.update({
}, {
$unset: {
'profile.tags':1,
Users.update(
{},
{
$unset: {
'profile.tags': 1,
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('remove-customFields-references-broken', () => {
Cards.update({'customFields.$value': null},
{ $pull: {
customFields: {value: null},
Cards.update(
{ 'customFields.$value': null },
{
$pull: {
customFields: { value: null },
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-product-name', () => {
Settings.update({
productName: {
$exists: false,
Settings.update(
{
productName: {
$exists: false,
},
},
}, {
$set: {
productName:'',
{
$set: {
productName: '',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-hide-logo', () => {
Settings.update({
hideLogo: {
$exists: false,
Settings.update(
{
hideLogo: {
$exists: false,
},
},
}, {
$set: {
hideLogo: false,
{
$set: {
hideLogo: false,
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-custom-html-after-body-start', () => {
Settings.update({
customHTMLafterBodyStart: {
$exists: false,
Settings.update(
{
customHTMLafterBodyStart: {
$exists: false,
},
},
}, {
$set: {
customHTMLafterBodyStart:'',
{
$set: {
customHTMLafterBodyStart: '',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-custom-html-before-body-end', () => {
Settings.update({
customHTMLbeforeBodyEnd: {
$exists: false,
Settings.update(
{
customHTMLbeforeBodyEnd: {
$exists: false,
},
},
}, {
$set: {
customHTMLbeforeBodyEnd:'',
{
$set: {
customHTMLbeforeBodyEnd: '',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-displayAuthenticationMethod', () => {
Settings.update({
displayAuthenticationMethod: {
$exists: false,
Settings.update(
{
displayAuthenticationMethod: {
$exists: false,
},
},
}, {
$set: {
displayAuthenticationMethod: true,
{
$set: {
displayAuthenticationMethod: true,
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-defaultAuthenticationMethod', () => {
Settings.update({
defaultAuthenticationMethod: {
$exists: false,
Settings.update(
{
defaultAuthenticationMethod: {
$exists: false,
},
},
}, {
$set: {
defaultAuthenticationMethod: 'password',
{
$set: {
defaultAuthenticationMethod: 'password',
},
},
}, noValidateMulti);
noValidateMulti
);
});
Migrations.add('add-templates', () => {
Boards.update({
type: {
$exists: false,
Boards.update(
{
type: {
$exists: false,
},
},
}, {
$set: {
type: 'board',
{
$set: {
type: 'board',
},
},
}, noValidateMulti);
Swimlanes.update({
type: {
$exists: false,
noValidateMulti
);
Swimlanes.update(
{
type: {
$exists: false,
},
},
}, {
$set: {
type: 'swimlane',
{
$set: {
type: 'swimlane',
},
},
}, noValidateMulti);
Lists.update({
type: {
$exists: false,
noValidateMulti
);
Lists.update(
{
type: {
$exists: false,
},
swimlaneId: {
$exists: false,
},
},
swimlaneId: {
$exists: false,
{
$set: {
type: 'list',
swimlaneId: '',
},
},
}, {
$set: {
type: 'list',
swimlaneId: '',
},
}, noValidateMulti);
noValidateMulti
);
Users.find({
'profile.templatesBoardId': {
$exists: false,
},
}).forEach((user) => {
// Create board and swimlanes
Boards.insert({
title: TAPi18n.__('templates'),
permission: 'private',
type: 'template-container',
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,
Boards.insert(
{
title: TAPi18n.__('templates'),
permission: 'private',
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
Users.update(user._id, {$set: {'profile.cardTemplatesSwimlaneId': swimlaneId}});
});
// Insert the card templates swimlane
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
Swimlanes.insert({
title: TAPi18n.__('list-templates-swimlane'),
boardId,
sort: 2,
type: 'template-container',
}, (err, swimlaneId) => {
// Insert the list templates swimlane
Swimlanes.insert(
{
title: TAPi18n.__('list-templates-swimlane'),
boardId,
sort: 2,
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
Users.update(user._id, {$set: {'profile.listTemplatesSwimlaneId': swimlaneId}});
});
// Insert the board templates swimlane
Swimlanes.insert({
title: TAPi18n.__('board-templates-swimlane'),
boardId,
sort: 3,
type: 'template-container',
}, (err, swimlaneId) => {
// Insert the reference to out board templates swimlane
Users.update(user._id, {$set: {'profile.boardTemplatesSwimlaneId': swimlaneId}});
});
});
// Insert the board templates swimlane
Swimlanes.insert(
{
title: TAPi18n.__('board-templates-swimlane'),
boardId,
sort: 3,
type: 'template-container',
},
(err, swimlaneId) => {
// Insert the reference to out board templates swimlane
Users.update(user._id, {
$set: { 'profile.boardTemplatesSwimlaneId': swimlaneId },
});
}
);
}
);
});
});
Migrations.add('fix-circular-reference_', () => {
Cards.find().forEach((card) => {
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', () => {
CustomFields.find().forEach((cf) => {
CustomFields.update(cf, {
$set: {
boardIds: [cf.boardId],
CustomFields.update(
cf,
{
$set: {
boardIds: [cf.boardId],
},
$unset: {
boardId: '',
},
},
$unset: {
boardId: '',
},
}, noValidateMulti);
noValidateMulti
);
});
});
const firstBatchOfDbsToAddCreatedAndUpdated = [
AccountSettings,
Actions,
Activities,
Announcements,
Boards,
CardComments,
Cards,
ChecklistItems,
Checklists,
CustomFields,
Integrations,
InvitationCodes,
Lists,
Rules,
Settings,
Swimlanes,
Triggers,
UnsavedEdits,
];
firstBatchOfDbsToAddCreatedAndUpdated.forEach((db) => {
db.before.insert((userId, doc) => {
doc.createdAt = Date.now();
doc.updatedAt = doc.createdAt;
});
db.before.update((userId, doc, fieldNames, modifier, options) => {
modifier.$set = modifier.$set || {};
modifier.$set.updatedAt = new Date();
});
});
const modifiedAtTables = [
AccountSettings,
Actions,
Activities,
Announcements,
Boards,
CardComments,
Cards,
ChecklistItems,
Checklists,
CustomFields,
Integrations,
InvitationCodes,
Lists,
Rules,
Settings,
Swimlanes,
Triggers,
UnsavedEdits,
Users,
];
Migrations.add('add-missing-created-and-modified', () => {
Promise.all(
modifiedAtTables.map((db) =>
db
.rawCollection()
.update(
{ modifiedAt: { $exists: false } },
{ $set: { modifiedAt: new Date() } },
{ multi: true }
)
.then(() =>
db
.rawCollection()
.update(
{ createdAt: { $exists: false } },
{ $set: { createdAt: new Date() } },
{ multi: true }
)
)
)
)
.then(() => {
// eslint-disable-next-line no-console
console.info('Successfully added createdAt and updatedAt to all tables');
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
});
});