mirror of
https://github.com/wekan/wekan.git
synced 2025-04-23 13:37:09 -04:00
Add createdAt and modifiedAt to all collections
This commit is contained in:
parent
fb728baf0c
commit
c60e80d25b
37 changed files with 3722 additions and 2168 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
953
models/boards.js
953
models/boards.js
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
361
models/lists.js
361
models/lists.js
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
883
models/users.js
883
models/users.js
File diff suppressed because it is too large
Load diff
1
packages/wekan-iframe
Submodule
1
packages/wekan-iframe
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit e105dcc9c3424beee0ff0a9db9ca543a6d4b7f85
|
1
packages/wekan_accounts-oidc/.gitignore
vendored
Normal file
1
packages/wekan_accounts-oidc/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.versions
|
14
packages/wekan_accounts-oidc/LICENSE.txt
Normal file
14
packages/wekan_accounts-oidc/LICENSE.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
Copyright (C) 2016 SWITCH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
75
packages/wekan_accounts-oidc/README.md
Normal file
75
packages/wekan_accounts-oidc/README.md
Normal file
|
@ -0,0 +1,75 @@
|
|||
# salleman:accounts-oidc package
|
||||
|
||||
A Meteor login service for OpenID Connect (OIDC).
|
||||
|
||||
## Installation
|
||||
|
||||
meteor add salleman:accounts-oidc
|
||||
|
||||
## Usage
|
||||
|
||||
`Meteor.loginWithOidc(options, callback)`
|
||||
* `options` - object containing options, see below (optional)
|
||||
* `callback` - callback function (optional)
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
Template.myTemplateName.events({
|
||||
'click #login-button': function() {
|
||||
Meteor.loginWithOidc();
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
These options override service configuration stored in the database.
|
||||
|
||||
* `loginStyle`: `redirect` or `popup`
|
||||
* `redirectUrl`: Where to redirect after successful login. Only used if `loginStyle` is set to `redirect`
|
||||
|
||||
## Manual Configuration Setup
|
||||
|
||||
You can manually configure this package by upserting the service configuration on startup. First, add the `service-configuration` package:
|
||||
|
||||
meteor add service-configuration
|
||||
|
||||
### Service Configuration
|
||||
|
||||
The following service configuration are available:
|
||||
|
||||
* `clientId`: OIDC client identifier
|
||||
* `secret`: OIDC client shared secret
|
||||
* `serverUrl`: URL of the OIDC server. e.g. `https://openid.example.org:8443`
|
||||
* `authorizationEndpoint`: Endpoint of the OIDC authorization service, e.g. `/oidc/authorize`
|
||||
* `tokenEndpoint`: Endpoint of the OIDC token service, e.g. `/oidc/token`
|
||||
* `userinfoEndpoint`: Endpoint of the OIDC userinfo service, e.g. `/oidc/userinfo`
|
||||
* `idTokenWhitelistFields`: A list of fields from IDToken to be added to Meteor.user().services.oidc object
|
||||
|
||||
### Project Configuration
|
||||
|
||||
Then in your project:
|
||||
|
||||
```js
|
||||
if (Meteor.isServer) {
|
||||
Meteor.startup(function () {
|
||||
ServiceConfiguration.configurations.upsert(
|
||||
{ service: 'oidc' },
|
||||
{
|
||||
$set: {
|
||||
loginStyle: 'redirect',
|
||||
clientId: 'my-client-id-registered-with-the-oidc-server',
|
||||
secret: 'my-client-shared-secret',
|
||||
serverUrl: 'https://openid.example.org',
|
||||
authorizationEndpoint: '/oidc/authorize',
|
||||
tokenEndpoint: '/oidc/token',
|
||||
userinfoEndpoint: '/oidc/userinfo',
|
||||
idTokenWhitelistFields: []
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
22
packages/wekan_accounts-oidc/oidc.js
Normal file
22
packages/wekan_accounts-oidc/oidc.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
Accounts.oauth.registerService('oidc');
|
||||
|
||||
if (Meteor.isClient) {
|
||||
Meteor.loginWithOidc = function(options, callback) {
|
||||
// support a callback without options
|
||||
if (! callback && typeof options === "function") {
|
||||
callback = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
|
||||
Oidc.requestCredential(options, credentialRequestCompleteCallback);
|
||||
};
|
||||
} else {
|
||||
Accounts.addAutopublishFields({
|
||||
// not sure whether the OIDC api can be used from the browser,
|
||||
// thus not sure if we should be sending access tokens; but we do it
|
||||
// for all other oauth2 providers, and it may come in handy.
|
||||
forLoggedInUser: ['services.oidc'],
|
||||
forOtherUsers: ['services.oidc.id']
|
||||
});
|
||||
}
|
3
packages/wekan_accounts-oidc/oidc_login_button.css
Normal file
3
packages/wekan_accounts-oidc/oidc_login_button.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
#login-buttons-image-oidc {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACQUlEQVQ4T5WTy2taQRTGv4kSXw0IoYIihCjFmhhfhUqW9a+o0I2LInTRRbtw05V2I9KQuuimi24KXQqChIhQQcQgGGNz0YpvMCG1yL1tGqvBZsKMIIXcjQcOcznnfL+ZOecOEUVx4/Ly8mQ6neqxhKlUKmltbc1Nut2uqJ/bEnJAkiTmEhEEgVqtViiVyjuAP70j/Pj6Htbglzu52WyGdrsNUq1Wqc1mk939+9sHPP7wTVM232g0QMrlMrXb7bIFndgcbAk3ZPP1eh2kVCrRra2tRcFoNEK1WoXf78fg3Rxsfl3H3t4e3G43dnd3wWrMZjNqtRpIsVhcAFKpFPL5PBfF43H8TDj49/2XAvb393F2dgaNRgNKKaLR6ByQz+epw+HAwcEBisUijEYjgsEg1Go1pA9ODtC/+MZFDCKKIo9FIhEIggCSy+Xozs4OYrEY2ChDoRAIIVww/ujhxdrnFTSbTX6Cfr+Pi4sLhMNhnJ6egmSzWepyuZBIJGAwGBAIBLiY2ezTI74qg2UoFIqFr6ys4OrqiveKHB4eckAmk8FgMMD29jZ8Ph8XKj4/5uu/ZyXZKXBAOp2mHo+H/0isD6zDOp0Om5ubsAuvcA+/8ffpkSygUqmApFIp6vV6+b2ZsNfrodVqYTgcwqXtwul04pfhiSzg+PgYJJlMUovFwhvIbHV1lTs70c3NDSaTCa6vr+8A2FvodDr8CmwuepPJtIDIbvdfkInPz89ZRCKFQmFjNBqdjMfjpZ6jVquV1tfX3bcYegI7CyIWlgAAAABJRU5ErkJggg==');
|
||||
}
|
19
packages/wekan_accounts-oidc/package.js
Normal file
19
packages/wekan_accounts-oidc/package.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
Package.describe({
|
||||
summary: "OpenID Connect (OIDC) for Meteor accounts",
|
||||
version: "1.0.10",
|
||||
name: "wekan-accounts-oidc",
|
||||
git: "https://github.com/wekan/meteor-accounts-oidc.git",
|
||||
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('accounts-base@1.2.0', ['client', 'server']);
|
||||
// Export Accounts (etc) to packages using this one.
|
||||
api.imply('accounts-base', ['client', 'server']);
|
||||
api.use('accounts-oauth@1.1.0', ['client', 'server']);
|
||||
api.use('wekan-oidc@1.0.10', ['client', 'server']);
|
||||
|
||||
api.addFiles('oidc_login_button.css', 'client');
|
||||
|
||||
api.addFiles('oidc.js');
|
||||
});
|
1
packages/wekan_oidc/.gitignore
vendored
Normal file
1
packages/wekan_oidc/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.versions
|
14
packages/wekan_oidc/LICENSE.txt
Normal file
14
packages/wekan_oidc/LICENSE.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
Copyright (C) 2016 SWITCH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
7
packages/wekan_oidc/README.md
Normal file
7
packages/wekan_oidc/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# salleman:oidc package
|
||||
|
||||
A Meteor implementation of OpenID Connect Login flow
|
||||
|
||||
## Usage and Documentation
|
||||
|
||||
Look at the `salleman:accounts-oidc` package for the documentation about using OpenID Connect with Meteor.
|
68
packages/wekan_oidc/oidc_client.js
Normal file
68
packages/wekan_oidc/oidc_client.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
Oidc = {};
|
||||
|
||||
// Request OpenID Connect credentials for the user
|
||||
// @param options {optional}
|
||||
// @param credentialRequestCompleteCallback {Function} Callback function to call on
|
||||
// completion. Takes one argument, credentialToken on success, or Error on
|
||||
// error.
|
||||
Oidc.requestCredential = function (options, credentialRequestCompleteCallback) {
|
||||
// support both (options, callback) and (callback).
|
||||
if (!credentialRequestCompleteCallback && typeof options === 'function') {
|
||||
credentialRequestCompleteCallback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
var config = ServiceConfiguration.configurations.findOne({service: 'oidc'});
|
||||
if (!config) {
|
||||
credentialRequestCompleteCallback && credentialRequestCompleteCallback(
|
||||
new ServiceConfiguration.ConfigError('Service oidc not configured.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var credentialToken = Random.secret();
|
||||
var loginStyle = OAuth._loginStyle('oidc', config, options);
|
||||
var scope = config.requestPermissions || ['openid', 'profile', 'email'];
|
||||
|
||||
// options
|
||||
options = options || {};
|
||||
options.client_id = config.clientId;
|
||||
options.response_type = options.response_type || 'code';
|
||||
options.redirect_uri = OAuth._redirectUri('oidc', config);
|
||||
options.state = OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl);
|
||||
options.scope = scope.join(' ');
|
||||
|
||||
if (config.loginStyle && config.loginStyle == 'popup') {
|
||||
options.display = 'popup';
|
||||
}
|
||||
|
||||
var loginUrl = config.serverUrl + config.authorizationEndpoint;
|
||||
// check if the loginUrl already contains a "?"
|
||||
var first = loginUrl.indexOf('?') === -1;
|
||||
for (var k in options) {
|
||||
if (first) {
|
||||
loginUrl += '?';
|
||||
first = false;
|
||||
}
|
||||
else {
|
||||
loginUrl += '&'
|
||||
}
|
||||
loginUrl += encodeURIComponent(k) + '=' + encodeURIComponent(options[k]);
|
||||
}
|
||||
|
||||
//console.log('XXX: loginURL: ' + loginUrl)
|
||||
|
||||
options.popupOptions = options.popupOptions || {};
|
||||
var popupOptions = {
|
||||
width: options.popupOptions.width || 320,
|
||||
height: options.popupOptions.height || 450
|
||||
};
|
||||
|
||||
OAuth.launchLogin({
|
||||
loginService: 'oidc',
|
||||
loginStyle: loginStyle,
|
||||
loginUrl: loginUrl,
|
||||
credentialRequestCompleteCallback: credentialRequestCompleteCallback,
|
||||
credentialToken: credentialToken,
|
||||
popupOptions: popupOptions,
|
||||
});
|
||||
};
|
6
packages/wekan_oidc/oidc_configure.html
Normal file
6
packages/wekan_oidc/oidc_configure.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<template name="configureLoginServiceDialogForOidc">
|
||||
<p>
|
||||
You'll need to create an OpenID Connect client configuration with your provider.
|
||||
Set App Callbacks URLs to: <span class="url">{{siteUrl}}_oauth/oidc</span>
|
||||
</p>
|
||||
</template>
|
17
packages/wekan_oidc/oidc_configure.js
Normal file
17
packages/wekan_oidc/oidc_configure.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
Template.configureLoginServiceDialogForOidc.helpers({
|
||||
siteUrl: function () {
|
||||
return Meteor.absoluteUrl();
|
||||
}
|
||||
});
|
||||
|
||||
Template.configureLoginServiceDialogForOidc.fields = function () {
|
||||
return [
|
||||
{ property: 'clientId', label: 'Client ID'},
|
||||
{ property: 'secret', label: 'Client Secret'},
|
||||
{ property: 'serverUrl', label: 'OIDC Server URL'},
|
||||
{ property: 'authorizationEndpoint', label: 'Authorization Endpoint'},
|
||||
{ property: 'tokenEndpoint', label: 'Token Endpoint'},
|
||||
{ property: 'userinfoEndpoint', label: 'Userinfo Endpoint'},
|
||||
{ property: 'idTokenWhitelistFields', label: 'Id Token Fields'}
|
||||
];
|
||||
};
|
143
packages/wekan_oidc/oidc_server.js
Normal file
143
packages/wekan_oidc/oidc_server.js
Normal file
|
@ -0,0 +1,143 @@
|
|||
Oidc = {};
|
||||
|
||||
OAuth.registerService('oidc', 2, null, function (query) {
|
||||
|
||||
var debug = process.env.DEBUG || false;
|
||||
var token = getToken(query);
|
||||
if (debug) console.log('XXX: register token:', token);
|
||||
|
||||
var accessToken = token.access_token || token.id_token;
|
||||
var expiresAt = (+new Date) + (1000 * parseInt(token.expires_in, 10));
|
||||
|
||||
var userinfo = getUserInfo(accessToken);
|
||||
if (debug) console.log('XXX: userinfo:', userinfo);
|
||||
|
||||
var serviceData = {};
|
||||
serviceData.id = userinfo[process.env.OAUTH2_ID_MAP] || userinfo[id];
|
||||
serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP] || userinfo[uid];
|
||||
serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP] || userinfo[displayName];
|
||||
serviceData.accessToken = accessToken;
|
||||
serviceData.expiresAt = expiresAt;
|
||||
serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP] || userinfo[email];
|
||||
|
||||
if (accessToken) {
|
||||
var tokenContent = getTokenContent(accessToken);
|
||||
var fields = _.pick(tokenContent, getConfiguration().idTokenWhitelistFields);
|
||||
_.extend(serviceData, fields);
|
||||
}
|
||||
|
||||
if (token.refresh_token)
|
||||
serviceData.refreshToken = token.refresh_token;
|
||||
if (debug) console.log('XXX: serviceData:', serviceData);
|
||||
|
||||
var profile = {};
|
||||
profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP] || userinfo[displayName];
|
||||
profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP] || userinfo[email];
|
||||
if (debug) console.log('XXX: profile:', profile);
|
||||
|
||||
return {
|
||||
serviceData: serviceData,
|
||||
options: { profile: profile }
|
||||
};
|
||||
});
|
||||
|
||||
var userAgent = "Meteor";
|
||||
if (Meteor.release) {
|
||||
userAgent += "/" + Meteor.release;
|
||||
}
|
||||
|
||||
var getToken = function (query) {
|
||||
var debug = process.env.DEBUG || false;
|
||||
var config = getConfiguration();
|
||||
var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
|
||||
var response;
|
||||
|
||||
try {
|
||||
response = HTTP.post(
|
||||
serverTokenEndpoint,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
"User-Agent": userAgent
|
||||
},
|
||||
params: {
|
||||
code: query.code,
|
||||
client_id: config.clientId,
|
||||
client_secret: OAuth.openSecret(config.secret),
|
||||
redirect_uri: OAuth._redirectUri('oidc', config),
|
||||
grant_type: 'authorization_code',
|
||||
state: query.state
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
throw _.extend(new Error("Failed to get token from OIDC " + serverTokenEndpoint + ": " + err.message),
|
||||
{ response: err.response });
|
||||
}
|
||||
if (response.data.error) {
|
||||
// if the http response was a json object with an error attribute
|
||||
throw new Error("Failed to complete handshake with OIDC " + serverTokenEndpoint + ": " + response.data.error);
|
||||
} else {
|
||||
if (debug) console.log('XXX: getToken response: ', response.data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
var getUserInfo = function (accessToken) {
|
||||
var debug = process.env.DEBUG || false;
|
||||
var config = getConfiguration();
|
||||
// Some userinfo endpoints use a different base URL than the authorization or token endpoints.
|
||||
// This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
|
||||
if (config.userinfoEndpoint.includes("https://")) {
|
||||
var serverUserinfoEndpoint = config.userinfoEndpoint;
|
||||
} else {
|
||||
var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
|
||||
}
|
||||
var response;
|
||||
try {
|
||||
response = HTTP.get(
|
||||
serverUserinfoEndpoint,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": userAgent,
|
||||
"Authorization": "Bearer " + accessToken
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
throw _.extend(new Error("Failed to fetch userinfo from OIDC " + serverUserinfoEndpoint + ": " + err.message),
|
||||
{response: err.response});
|
||||
}
|
||||
if (debug) console.log('XXX: getUserInfo response: ', response.data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
var getConfiguration = function () {
|
||||
var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
|
||||
if (!config) {
|
||||
throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
var getTokenContent = function (token) {
|
||||
var content = null;
|
||||
if (token) {
|
||||
try {
|
||||
var parts = token.split('.');
|
||||
var header = JSON.parse(new Buffer(parts[0], 'base64').toString());
|
||||
content = JSON.parse(new Buffer(parts[1], 'base64').toString());
|
||||
var signature = new Buffer(parts[2], 'base64');
|
||||
var signed = parts[0] + '.' + parts[1];
|
||||
} catch (err) {
|
||||
this.content = {
|
||||
exp: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
Oidc.retrieveCredential = function (credentialToken, credentialSecret) {
|
||||
return OAuth.retrieveCredential(credentialToken, credentialSecret);
|
||||
};
|
23
packages/wekan_oidc/package.js
Normal file
23
packages/wekan_oidc/package.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
Package.describe({
|
||||
summary: "OpenID Connect (OIDC) flow for Meteor",
|
||||
version: "1.0.12",
|
||||
name: "wekan-oidc",
|
||||
git: "https://github.com/wekan/meteor-accounts-oidc.git",
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('oauth2@1.1.0', ['client', 'server']);
|
||||
api.use('oauth@1.1.0', ['client', 'server']);
|
||||
api.use('http@1.1.0', ['server']);
|
||||
api.use('underscore@1.0.0', 'client');
|
||||
api.use('templating@1.1.0', 'client');
|
||||
api.use('random@1.0.0', 'client');
|
||||
api.use('service-configuration@1.0.0', ['client', 'server']);
|
||||
|
||||
api.export('Oidc');
|
||||
|
||||
api.addFiles(['oidc_configure.html', 'oidc_configure.js'], 'client');
|
||||
|
||||
api.addFiles('oidc_server.js', 'server');
|
||||
api.addFiles('oidc_client.js', 'client');
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue