summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
Diffstat (limited to 'models')
-rw-r--r--models/accountSettings.js8
-rw-r--r--models/activities.js6
-rw-r--r--models/announcements.js36
-rw-r--r--models/attachments.js153
-rw-r--r--models/boards.js245
-rw-r--r--models/cardComments.js138
-rw-r--r--models/cards.js132
-rw-r--r--models/checklistItems.js145
-rw-r--r--models/checklists.js252
-rw-r--r--models/export.js4
-rw-r--r--models/integrations.js264
-rw-r--r--models/invitationCodes.js4
-rw-r--r--models/lists.js198
-rw-r--r--models/settings.js28
-rw-r--r--models/swimlanes.js219
-rw-r--r--models/trelloCreator.js72
-rw-r--r--models/users.js319
-rw-r--r--models/wekanCreator.js150
18 files changed, 1659 insertions, 714 deletions
diff --git a/models/accountSettings.js b/models/accountSettings.js
index db4087c0..6dfbac5d 100644
--- a/models/accountSettings.js
+++ b/models/accountSettings.js
@@ -23,11 +23,17 @@ AccountSettings.allow({
if (Meteor.isServer) {
Meteor.startup(() => {
- AccountSettings.upsert({ _id: 'accounts-allowEmailChange' }, {
+ AccountSettings.upsert({_id: 'accounts-allowEmailChange'}, {
$setOnInsert: {
booleanValue: false,
sort: 0,
},
});
+ AccountSettings.upsert({_id: 'accounts-allowUserNameChange'}, {
+ $setOnInsert: {
+ booleanValue: false,
+ sort: 1,
+ },
+ });
});
}
diff --git a/models/activities.js b/models/activities.js
index 237283f8..f64b53f8 100644
--- a/models/activities.js
+++ b/models/activities.js
@@ -23,6 +23,9 @@ Activities.helpers({
list() {
return Lists.findOne(this.listId);
},
+ swimlane() {
+ return Swimlanes.findOne(this.swimlaneId);
+ },
oldList() {
return Lists.findOne(this.oldListId);
},
@@ -39,7 +42,7 @@ Activities.helpers({
return Checklists.findOne(this.checklistId);
},
checklistItem() {
- return Checklists.findOne(this.checklistId).getItem(this.checklistItemId);
+ return ChecklistItems.findOne(this.checklistItemId);
},
customField() {
return CustomFields.findOne(this.customFieldId);
@@ -114,6 +117,7 @@ if (Meteor.isServer) {
if (activity.commentId) {
const comment = activity.comment();
params.comment = comment.text;
+ params.commentId = comment._id;
}
if (activity.attachmentId) {
const attachment = activity.attachment();
diff --git a/models/announcements.js b/models/announcements.js
new file mode 100644
index 00000000..2cb1e1b7
--- /dev/null
+++ b/models/announcements.js
@@ -0,0 +1,36 @@
+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.allow({
+ update(userId) {
+ const user = Users.findOne(userId);
+ return user && user.isAdmin;
+ },
+});
+
+if (Meteor.isServer) {
+ Meteor.startup(() => {
+ const announcements = Announcements.findOne({});
+ if(!announcements){
+ Announcements.insert({enabled: false, sort: 0});
+ }
+ });
+}
diff --git a/models/attachments.js b/models/attachments.js
index 560bec99..5e5c4926 100644
--- a/models/attachments.js
+++ b/models/attachments.js
@@ -1,83 +1,90 @@
-Attachments = new FS.Collection('attachments', {
- stores: [
+ 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', {
- // If the uploaded document is not an image we need to enforce browser
- // download instead of execution. This is particularly important for HTML
- // files that the browser will just execute if we don't serve them with the
- // appropriate `application/octet-stream` MIME header which can lead to user
- // data leaks. I imagine other formats (like PDF) can also be attack vectors.
- // See https://github.com/wekan/wekan/issues/99
- // XXX Should we use `beforeWrite` option of CollectionFS instead of
- // collection-hooks?
- // We should use `beforeWrite`.
- beforeWrite: (fileObj) => {
- if (!fileObj.isImage()) {
- return {
- type: 'application/octet-stream',
- };
+ // 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', {
+ // If the uploaded document is not an image we need to enforce browser
+ // download instead of execution. This is particularly important for HTML
+ // files that the browser will just execute if we don't serve them with the
+ // appropriate `application/octet-stream` MIME header which can lead to user
+ // data leaks. I imagine other formats (like PDF) can also be attack vectors.
+ // See https://github.com/wekan/wekan/issues/99
+ // XXX Should we use `beforeWrite` option of CollectionFS instead of
+ // collection-hooks?
+ // We should use `beforeWrite`.
+ beforeWrite: (fileObj) => {
+ if (!fileObj.isImage()) {
+ return {
+ type: 'application/octet-stream',
+ };
+ }
+ return {};
+ },
+ }),
+ ],
+ });
+
+
+ if (Meteor.isServer) {
+ Attachments.allow({
+ insert(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ update(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ remove(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ // We authorize the attachment download either:
+ // - if the board is public, everyone (even unconnected) can download it
+ // - if the board is private, only board members can download it
+ download(userId, doc) {
+ const board = Boards.findOne(doc.boardId);
+ if (board.isPublic()) {
+ return true;
+ } else {
+ return board.hasMember(userId);
}
- return {};
},
- }),
- ],
-});
-if (Meteor.isServer) {
- Attachments.allow({
- insert(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- update(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- remove(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- // We authorize the attachment download either:
- // - if the board is public, everyone (even unconnected) can download it
- // - if the board is private, only board members can download it
- download(userId, doc) {
- const board = Boards.findOne(doc.boardId);
- if (board.isPublic()) {
- return true;
- } else {
- return board.hasMember(userId);
- }
- },
+ fetch: ['boardId'],
+ });
+ }
- fetch: ['boardId'],
- });
-}
+ // XXX Enforce a schema for the Attachments CollectionFS
-// XXX Enforce a schema for the Attachments CollectionFS
+ if (Meteor.isServer) {
+ Attachments.files.after.insert((userId, doc) => {
+ // If the attachment doesn't have a source field
+ // or its source is different than import
+ if (!doc.source || doc.source !== 'import') {
+ // Add activity about adding the attachment
+ Activities.insert({
+ userId,
+ type: 'card',
+ activityType: 'addAttachment',
+ attachmentId: doc._id,
+ boardId: doc.boardId,
+ cardId: doc.cardId,
+ });
+ } 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: '',
+ },
+ });
+ }
+ });
-if (Meteor.isServer) {
- Attachments.files.after.insert((userId, doc) => {
- // If the attachment doesn't have a source field
- // or its source is different than import
- if (!doc.source || doc.source !== 'import') {
- // Add activity about adding the attachment
- Activities.insert({
- userId,
- type: 'card',
- activityType: 'addAttachment',
+ Attachments.files.after.remove((userId, doc) => {
+ Activities.remove({
attachmentId: doc._id,
- boardId: doc.boardId,
- cardId: doc.cardId,
});
- } 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.files.after.remove((userId, doc) => {
- Activities.remove({
- attachmentId: doc._id,
});
- });
-}
+ }
diff --git a/models/boards.js b/models/boards.js
index 98c0e46d..44ce0b62 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -187,6 +187,20 @@ Boards.helpers({
return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
},
+ swimlanes() {
+ return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } });
+ },
+
+ hasOvertimeCards(){
+ const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} );
+ return card !== undefined;
+ },
+
+ hasSpentTimeCards(){
+ const card = Cards.findOne({spentTime: { $gt: 0 }, boardId: this._id, archived: false} );
+ return card !== undefined;
+ },
+
activities() {
return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } });
},
@@ -246,6 +260,27 @@ Boards.helpers({
Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } });
return _id;
},
+
+ searchCards(term) {
+ check(term, Match.OneOf(String, null, undefined));
+
+ let query = { boardId: this._id };
+ const projection = { limit: 10, sort: { createdAt: -1 } };
+
+ if (term) {
+ const regex = new RegExp(term, 'i');
+
+ query = {
+ boardId: this._id,
+ $or: [
+ { title: regex },
+ { description: regex },
+ ],
+ };
+ }
+
+ return Cards.find(query, projection);
+ },
});
Boards.mutations({
@@ -302,6 +337,15 @@ Boards.mutations({
return { $pull: { labels: { _id: labelId } } };
},
+ changeOwnership(fromId, toId) {
+ const memberIndex = this.memberIndex(fromId);
+ return {
+ $set: {
+ [`members.${memberIndex}.userId`]: toId,
+ },
+ };
+ },
+
addMember(memberId) {
const memberIndex = this.memberIndex(memberId);
if (memberIndex >= 0) {
@@ -560,83 +604,152 @@ if (Meteor.isServer) {
//BOARDS REST API
if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res, next) {
- Authentication.checkLoggedIn(req.userId);
- const paramUserId = req.params.userId;
- // A normal user should be able to see their own boards,
- // admins can access boards of any user
- Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId);
-
- const data = Boards.find({
- archived: false,
- 'members.userId': req.userId,
- }, {
- sort: ['title'],
- }).map(function(board) {
- return {
- _id: board._id,
- title: board.title,
- };
- });
+ JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) {
+ try {
+ Authentication.checkLoggedIn(req.userId);
+ const paramUserId = req.params.userId;
+ // A normal user should be able to see their own boards,
+ // admins can access boards of any user
+ Authentication.checkAdminOrCondition(req.userId, req.userId === paramUserId);
+
+ const data = Boards.find({
+ archived: false,
+ 'members.userId': paramUserId,
+ }, {
+ sort: ['title'],
+ }).map(function(board) {
+ return {
+ _id: board._id,
+ title: board.title,
+ };
+ });
- JsonRoutes.sendResult(res, {code: 200, data});
+ JsonRoutes.sendResult(res, {code: 200, data});
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('GET', '/api/boards', function (req, res, next) {
- Authentication.checkUserId(req.userId);
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Boards.find({ permission: 'public' }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- }),
- });
+ JsonRoutes.add('GET', '/api/boards', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Boards.find({ permission: 'public' }).map(function (doc) {
+ return {
+ _id: doc._id,
+ title: doc.title,
+ };
+ }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('GET', '/api/boards/:id', function (req, res, next) {
- const id = req.params.id;
- Authentication.checkBoardAccess( req.userId, id);
+ JsonRoutes.add('GET', '/api/boards/:id', function (req, res) {
+ try {
+ const id = req.params.id;
+ Authentication.checkBoardAccess(req.userId, id);
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Boards.findOne({ _id: id }),
- });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Boards.findOne({ _id: id }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('POST', '/api/boards', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- const id = Boards.insert({
- title: req.body.title,
- members: [
- {
- userId: req.body.owner,
- isAdmin: true,
- isActive: true,
- isCommentOnly: false,
+ JsonRoutes.add('POST', '/api/boards', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const id = Boards.insert({
+ title: req.body.title,
+ members: [
+ {
+ userId: req.body.owner,
+ isAdmin: true,
+ isActive: true,
+ isCommentOnly: false,
+ },
+ ],
+ permission: 'public',
+ color: 'belize',
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
},
- ],
- permission: 'public',
- color: 'belize',
- });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
+ });
+
+ JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const id = req.params.id;
+ Boards.remove({ _id: id });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data:{
+ _id: id,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res, next) {
- Authentication.checkUserId( req.userId);
+ JsonRoutes.add('PUT', '/api/boards/:id/labels', function (req, res) {
+ Authentication.checkUserId(req.userId);
const id = req.params.id;
- Boards.remove({ _id: id });
- JsonRoutes.sendResult(res, {
- code: 200,
- data:{
- _id: id,
- },
- });
+ try {
+ if (req.body.hasOwnProperty('label')) {
+ const board = Boards.findOne({ _id: id });
+ const color = req.body.label.color;
+ const name = req.body.label.name;
+ const labelId = Random.id(6);
+ if (!board.getLabel(name, color)) {
+ Boards.direct.update({ _id: id }, { $push: { labels: { _id: labelId, name, color } } });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: labelId,
+ });
+ } else {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ });
+ }
+ }
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ data: error,
+ });
+ }
});
}
diff --git a/models/cardComments.js b/models/cardComments.js
index 352030f1..b6cb10fa 100644
--- a/models/cardComments.js
+++ b/models/cardComments.js
@@ -87,66 +87,98 @@ if (Meteor.isServer) {
//CARD COMMENT REST API
if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res, next) {
- 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) {
- return {
- _id: doc._id,
- comment: doc.text,
- authorId: doc.userId,
- };
- }),
- });
+ JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) {
+ try {
+ 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) {
+ return {
+ _id: doc._id,
+ comment: doc.text,
+ authorId: doc.userId,
+ };
+ }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res, next) {
- 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,
+ });
+ }
});
- JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res, next) {
- 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,
+ });
- const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId });
- commentCreation(req.body.authorId, cardComment);
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ const cardComment = CardComments.findOne({_id: id, cardId:paramCardId, boardId: paramBoardId });
+ commentCreation(req.body.authorId, cardComment);
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res, next) {
- 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,
+ });
+ }
});
}
diff --git a/models/cards.js b/models/cards.js
index 17abf430..8b917ee3 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -18,9 +18,12 @@ Cards.attachSchema(new SimpleSchema({
listId: {
type: String,
},
- // The system could work without this `boardId` information (we could deduce
- // the board identifier from the card), but it would make the system more
- // difficult to manage and less efficient.
+ swimlaneId: {
+ type: String,
+ },
+ // The system could work without this `boardId` information (we could deduce
+ // the board identifier from the card), but it would make the system more
+ // difficult to manage and less efficient.
boardId: {
type: String,
},
@@ -71,6 +74,10 @@ Cards.attachSchema(new SimpleSchema({
type: [String],
optional: true,
},
+ receivedAt: {
+ type: Date,
+ optional: true,
+ },
startAt: {
type: Date,
optional: true,
@@ -79,8 +86,22 @@ Cards.attachSchema(new SimpleSchema({
type: Date,
optional: true,
},
- // XXX Should probably be called `authorId`. Is it even needed since we have
- // the `members` field?
+ endAt: {
+ type: Date,
+ optional: true,
+ },
+ spentTime: {
+ type: Number,
+ decimal: true,
+ optional: true,
+ },
+ isOvertime: {
+ type: Boolean,
+ defaultValue: false,
+ optional: true,
+ },
+ // XXX Should probably be called `authorId`. Is it even needed since we have
+ // the `members` field?
userId: {
type: String,
autoValue() { // eslint-disable-line consistent-return
@@ -151,13 +172,13 @@ Cards.helpers({
cover() {
const cover = Attachments.findOne(this.coverId);
- // if we return a cover before it is fully stored, we will get errors when we try to display it
- // todo XXX we could return a default "upload pending" image in the meantime?
+ // if we return a cover before it is fully stored, we will get errors when we try to display it
+ // todo XXX we could return a default "upload pending" image in the meantime?
return cover && cover.url() && cover;
},
checklists() {
- return Checklists.find({cardId: this._id}, {sort: {createdAt: 1}});
+ return Checklists.find({cardId: this._id}, {sort: { sort: 1 } });
},
checklistItemCount() {
@@ -219,6 +240,14 @@ Cards.helpers({
cardId: this._id,
});
},
+
+ canBeRestored() {
+ const list = Lists.findOne({_id: this.listId});
+ if(!list.getWipLimit('soft') && list.getWipLimit('enabled') && list.getWipLimit('value') === list.cards().count()){
+ return false;
+ }
+ return true;
+ },
});
Cards.mutations({
@@ -238,11 +267,15 @@ Cards.mutations({
return {$set: {description}};
},
- move(listId, sortIndex) {
- const mutatedFields = {listId};
- if (sortIndex) {
- mutatedFields.sort = sortIndex;
- }
+ move(swimlaneId, listId, sortIndex) {
+ const list = Lists.findOne(listId);
+ const mutatedFields = {
+ swimlaneId,
+ listId,
+ boardId: list.boardId,
+ sort: sortIndex,
+ };
+
return {$set: mutatedFields};
},
@@ -312,6 +345,14 @@ Cards.mutations({
return {$unset: {coverId: ''}};
},
+ setReceived(receivedAt) {
+ return {$set: {receivedAt}};
+ },
+
+ unsetReceived() {
+ return {$unset: {receivedAt: ''}};
+ },
+
setStart(startAt) {
return {$set: {startAt}};
},
@@ -327,6 +368,26 @@ Cards.mutations({
unsetDue() {
return {$unset: {dueAt: ''}};
},
+
+ setEnd(endAt) {
+ return {$set: {endAt}};
+ },
+
+ unsetEnd() {
+ return {$unset: {endAt: ''}};
+ },
+
+ setOvertime(isOvertime) {
+ return {$set: {isOvertime}};
+ },
+
+ setSpentTime(spentTime) {
+ return {$set: {spentTime}};
+ },
+
+ unsetSpentTime() {
+ return {$unset: {spentTime: '', isOvertime: false}};
+ },
});
@@ -371,7 +432,7 @@ function cardMembers(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'members'))
return;
let memberId;
- // Say hello to the new member
+ // Say hello to the new member
if (modifier.$addToSet && modifier.$addToSet.members) {
memberId = modifier.$addToSet.members;
if (!_.contains(doc.members, memberId)) {
@@ -385,10 +446,10 @@ function cardMembers(userId, doc, fieldNames, modifier) {
}
}
- // Say goodbye to the former member
+ // Say goodbye to the former member
if (modifier.$pull && modifier.$pull.members) {
memberId = modifier.$pull.members;
- // Check that the former member is member of the card
+ // Check that the former member is member of the card
if (_.contains(doc.members, memberId)) {
Activities.insert({
userId,
@@ -428,8 +489,8 @@ function cardRemover(userId, doc) {
if (Meteor.isServer) {
- // Cards are often fetched within a board, so we create an index to make these
- // queries more efficient.
+ // 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});
});
@@ -438,31 +499,31 @@ if (Meteor.isServer) {
cardCreation(userId, doc);
});
- // New activity for card (un)archivage
+ // New activity for card (un)archivage
Cards.after.update((userId, doc, fieldNames) => {
cardState(userId, doc, fieldNames);
});
- //New activity for card moves
+ //New activity for card moves
Cards.after.update(function (userId, doc, fieldNames) {
const oldListId = this.previous.listId;
cardMove(userId, doc, fieldNames, oldListId);
});
- // Add a new activity if we add or remove a member to the card
+ // Add a new activity if we add or remove a member to the card
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardMembers(userId, doc, fieldNames, modifier);
});
- // Remove all activities associated with a card if we remove the card
- // Remove also card_comments / checklists / attachments
+ // Remove all activities associated with a card if we remove the card
+ // Remove also card_comments / checklists / attachments
Cards.after.remove((userId, doc) => {
cardRemover(userId, doc);
});
}
//LISTS REST API
if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function (req, res, next) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function (req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
@@ -478,7 +539,7 @@ if (Meteor.isServer) {
});
});
- JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res, next) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;
@@ -489,11 +550,12 @@ if (Meteor.isServer) {
});
});
- JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function (req, res, next) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function (req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
const check = Users.findOne({_id: req.body.authorId});
+ const members = req.body.members || [req.body.authorId];
if (typeof check !== 'undefined') {
const id = Cards.direct.insert({
title: req.body.title,
@@ -501,8 +563,9 @@ if (Meteor.isServer) {
listId: paramListId,
description: req.body.description,
userId: req.body.authorId,
+ swimlaneId: req.body.swimlaneId,
sort: 0,
- members: [req.body.authorId],
+ members,
});
JsonRoutes.sendResult(res, {
code: 200,
@@ -521,7 +584,7 @@ if (Meteor.isServer) {
}
});
- JsonRoutes.add('PUT', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res, next) {
+ JsonRoutes.add('PUT', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
@@ -530,12 +593,12 @@ if (Meteor.isServer) {
if (req.body.hasOwnProperty('title')) {
const newTitle = req.body.title;
Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
- {$set: {title: newTitle}});
+ {$set: {title: newTitle}});
}
if (req.body.hasOwnProperty('listId')) {
const newParamListId = req.body.listId;
Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
- {$set: {listId: newParamListId}});
+ {$set: {listId: newParamListId}});
const card = Cards.findOne({_id: paramCardId} );
cardMove(req.body.authorId, card, {fieldName: 'listId'}, paramListId);
@@ -544,7 +607,12 @@ if (Meteor.isServer) {
if (req.body.hasOwnProperty('description')) {
const newDescription = req.body.description;
Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
- {$set: {description: newDescription}});
+ {$set: {description: newDescription}});
+ }
+ if (req.body.hasOwnProperty('labelIds')) {
+ const newlabelIds = req.body.labelIds;
+ Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false},
+ {$set: {labelIds: newlabelIds}});
}
JsonRoutes.sendResult(res, {
code: 200,
@@ -555,7 +623,7 @@ if (Meteor.isServer) {
});
- JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res, next) {
+ JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId/cards/:cardId', function (req, res) {
Authentication.checkUserId(req.userId);
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
diff --git a/models/checklistItems.js b/models/checklistItems.js
new file mode 100644
index 00000000..e075eda2
--- /dev/null
+++ b/models/checklistItems.js
@@ -0,0 +1,145 @@
+ChecklistItems = new Mongo.Collection('checklistItems');
+
+ChecklistItems.attachSchema(new SimpleSchema({
+ title: {
+ type: String,
+ },
+ sort: {
+ type: Number,
+ decimal: true,
+ },
+ isFinished: {
+ type: Boolean,
+ defaultValue: false,
+ },
+ checklistId: {
+ type: String,
+ },
+ cardId: {
+ type: String,
+ },
+}));
+
+ChecklistItems.allow({
+ insert(userId, doc) {
+ return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
+ },
+ update(userId, doc) {
+ return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
+ },
+ remove(userId, doc) {
+ return allowIsBoardMemberByCard(userId, Cards.findOne(doc.cardId));
+ },
+ fetch: ['userId', 'cardId'],
+});
+
+ChecklistItems.before.insert((userId, doc) => {
+ if (!doc.userId) {
+ doc.userId = userId;
+ }
+});
+
+// Mutations
+ChecklistItems.mutations({
+ setTitle(title) {
+ return { $set: { title } };
+ },
+ toggleItem() {
+ return { $set: { isFinished: !this.isFinished } };
+ },
+ move(checklistId, sortIndex) {
+ const cardId = Checklists.findOne(checklistId).cardId;
+ const mutatedFields = {
+ cardId,
+ checklistId,
+ sort: sortIndex,
+ };
+
+ return {$set: mutatedFields};
+ },
+});
+
+// Activities helper
+function itemCreation(userId, doc) {
+ const card = Cards.findOne(doc.cardId);
+ const boardId = card.boardId;
+ Activities.insert({
+ userId,
+ activityType: 'addChecklistItem',
+ cardId: doc.cardId,
+ boardId,
+ checklistId: doc.checklistId,
+ checklistItemId: doc._id,
+ });
+}
+
+function itemRemover(userId, doc) {
+ Activities.remove({
+ checklistItemId: doc._id,
+ });
+}
+
+// Activities
+if (Meteor.isServer) {
+ Meteor.startup(() => {
+ ChecklistItems._collection._ensureIndex({ checklistId: 1 });
+ });
+
+ ChecklistItems.after.insert((userId, doc) => {
+ itemCreation(userId, doc);
+ });
+
+ ChecklistItems.after.remove((userId, doc) => {
+ itemRemover(userId, doc);
+ });
+}
+
+if (Meteor.isServer) {
+ 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('PUT', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId', function (req, res) {
+ Authentication.checkUserId( req.userId);
+
+ const paramItemId = req.params.itemId;
+
+ 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,
+ },
+ });
+ });
+
+ 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,
+ },
+ });
+ });
+}
diff --git a/models/checklists.js b/models/checklists.js
index 35ef8ae1..c58453ef 100644
--- a/models/checklists.js
+++ b/models/checklists.js
@@ -6,24 +6,7 @@ Checklists.attachSchema(new SimpleSchema({
},
title: {
type: String,
- },
- items: {
- type: [Object],
- defaultValue: [],
- },
- 'items.$._id': {
- type: String,
- },
- 'items.$.title': {
- type: String,
- },
- 'items.$.sort': {
- type: Number,
- decimal: true,
- },
- 'items.$.isFinished': {
- type: Boolean,
- defaultValue: false,
+ defaultValue: 'Checklist',
},
finishedAt: {
type: Date,
@@ -44,41 +27,26 @@ Checklists.attachSchema(new SimpleSchema({
type: Number,
decimal: true,
},
- newItemIndex: {
- type: Number,
- decimal: true,
- defaultValue: 0,
- },
}));
-const self = Checklists;
-
Checklists.helpers({
itemCount() {
- return this.items.length;
+ return ChecklistItems.find({ checklistId: this._id }).count();
},
- getItems() {
- return this.items.sort(function (itemA, itemB) {
- if (itemA.sort < itemB.sort) {
- return -1;
- }
- if (itemA.sort > itemB.sort) {
- return 1;
- }
- return 0;
- });
+ items() {
+ return ChecklistItems.find({
+ checklistId: this._id,
+ }, { sort: ['sort'] });
},
finishedCount() {
- return this.items.filter((item) => {
- return item.isFinished;
- }).length;
+ return ChecklistItems.find({
+ checklistId: this._id,
+ isFinished: true,
+ }).count();
},
isFinished() {
return 0 !== this.itemCount() && this.itemCount() === this.finishedCount();
},
- getItem(_id) {
- return _.findWhere(this.items, { _id });
- },
itemIndex(itemId) {
const items = self.findOne({_id : this._id}).items;
return _.pluck(items, '_id').indexOf(itemId);
@@ -106,82 +74,9 @@ Checklists.before.insert((userId, doc) => {
});
Checklists.mutations({
- //for checklist itself
setTitle(title) {
return { $set: { title } };
},
- //for items in checklist
- addItem(title) {
- const itemCount = this.itemCount();
- const _id = `${this._id}${this.newItemIndex}`;
- return {
- $addToSet: { items: { _id, title, isFinished: false, sort: itemCount } },
- $set: { newItemIndex: this.newItemIndex + 1},
- };
- },
- removeItem(itemId) {
- return { $pull: { items: { _id: itemId } } };
- },
- editItem(itemId, title) {
- if (this.getItem(itemId)) {
- const itemIndex = this.itemIndex(itemId);
- return {
- $set: {
- [`items.${itemIndex}.title`]: title,
- },
- };
- }
- return {};
- },
- finishItem(itemId) {
- if (this.getItem(itemId)) {
- const itemIndex = this.itemIndex(itemId);
- return {
- $set: {
- [`items.${itemIndex}.isFinished`]: true,
- },
- };
- }
- return {};
- },
- resumeItem(itemId) {
- if (this.getItem(itemId)) {
- const itemIndex = this.itemIndex(itemId);
- return {
- $set: {
- [`items.${itemIndex}.isFinished`]: false,
- },
- };
- }
- return {};
- },
- toggleItem(itemId) {
- const item = this.getItem(itemId);
- if (item) {
- const itemIndex = this.itemIndex(itemId);
- return {
- $set: {
- [`items.${itemIndex}.isFinished`]: !item.isFinished,
- },
- };
- }
- return {};
- },
- sortItems(itemIDs) {
- const validItems = [];
- for (const itemID of itemIDs) {
- if (this.getItem(itemID)) {
- validItems.push(this.itemIndex(itemID));
- }
- }
- const modifiedValues = {};
- for (let i = 0; i < validItems.length; i++) {
- modifiedValues[`items.${validItems[i]}.sort`] = i;
- }
- return {
- $set: modifiedValues,
- };
- },
});
if (Meteor.isServer) {
@@ -199,30 +94,6 @@ if (Meteor.isServer) {
});
});
- //TODO: so there will be no activity for adding item into checklist, maybe will be implemented in the future.
- // The future is now
- Checklists.after.update((userId, doc, fieldNames, modifier) => {
- if (fieldNames.includes('items')) {
- if (modifier.$addToSet) {
- Activities.insert({
- userId,
- activityType: 'addChecklistItem',
- cardId: doc.cardId,
- boardId: Cards.findOne(doc.cardId).boardId,
- checklistId: doc._id,
- checklistItemId: modifier.$addToSet.items._id,
- });
- } else if (modifier.$pull) {
- const activity = Activities.findOne({
- checklistItemId: modifier.$pull.items._id,
- });
- if (activity) {
- Activities.remove(activity._id);
- }
- }
- }
- });
-
Checklists.before.remove((userId, doc) => {
const activities = Activities.find({ checklistId: doc._id });
if (activities) {
@@ -233,64 +104,91 @@ if (Meteor.isServer) {
});
}
-//CARD COMMENT REST API
if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res, next) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
const paramCardId = req.params.cardId;
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Checklists.find({ cardId: paramCardId }).map(function (doc) {
- return {
- _id: doc._id,
- title: doc.title,
- };
- }),
+ 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/:checklistId', function (req, res, next) {
+ 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;
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }),
- });
+ 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('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res, next) {
+ JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) {
Authentication.checkUserId( req.userId);
- const paramCardId = req.params.cardId;
- const checklistToSend = {};
- checklistToSend.cardId = paramCardId;
- checklistToSend.title = req.body.title;
- checklistToSend.items = [];
- const id = Checklists.insert(checklistToSend);
- const checklist = Checklists.findOne({_id: id});
- req.body.items.forEach(function (item) {
- checklist.addItem(item);
- }, this);
-
-
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
+ 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.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res, next) {
+ JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) {
Authentication.checkUserId( req.userId);
- const paramCommentId = req.params.commentId;
- const paramCardId = req.params.cardId;
- Checklists.remove({ _id: paramCommentId, cardId: paramCardId });
+ const paramChecklistId = req.params.checklistId;
+ Checklists.remove({ _id: paramChecklistId });
JsonRoutes.sendResult(res, {
code: 200,
data: {
- _id: paramCardId,
+ _id: paramChecklistId,
},
});
});
diff --git a/models/export.js b/models/export.js
index 49656134..aff66801 100644
--- a/models/export.js
+++ b/models/export.js
@@ -53,12 +53,16 @@ class Exporter {
_.extend(result, Boards.findOne(this._boardId, { fields: { stars: 0 } }));
result.lists = Lists.find(byBoard, noBoardId).fetch();
result.cards = Cards.find(byBoard, noBoardId).fetch();
+ result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
result.comments = CardComments.find(byBoard, noBoardId).fetch();
result.activities = Activities.find(byBoard, noBoardId).fetch();
result.checklists = [];
+ result.checklistItems = [];
result.cards.forEach((card) => {
result.checklists.push(...Checklists.find({ cardId: card._id }).fetch());
+ result.checklistItems.push(...ChecklistItems.find({ cardId: card._id }).fetch());
});
+
// [Old] for attachments we only export IDs and absolute url to original doc
// [New] Encode attachment to base64
const getBase64Data = function(doc, callback) {
diff --git a/models/integrations.js b/models/integrations.js
index 826873ce..1062b93b 100644
--- a/models/integrations.js
+++ b/models/integrations.js
@@ -59,132 +59,188 @@ Integrations.allow({
//INTEGRATIONS REST API
if (Meteor.isServer) {
// Get all integrations in board
- JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res, next) {
- const paramBoardId = req.params.boardId;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
+ 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) {
- return doc;
- });
+ const data = Integrations.find({ boardId: paramBoardId }, { fields: { token: 0 } }).map(function(doc) {
+ return doc;
+ });
- JsonRoutes.sendResult(res, {code: 200, data});
+ JsonRoutes.sendResult(res, {code: 200, data});
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
// Get a single integration in board
- JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res, next) {
- const paramBoardId = req.params.boardId;
- const paramIntId = req.params.intId;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
-
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }),
- });
+ JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ const paramIntId = req.params.intId;
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
+
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
// Create a new integration
- JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res, next) {
- const paramBoardId = req.params.boardId;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
-
- const id = Integrations.insert({
- userId: req.userId,
- boardId: paramBoardId,
- url: req.body.url,
- });
-
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
+
+ const id = Integrations.insert({
+ userId: req.userId,
+ boardId: paramBoardId,
+ url: req.body.url,
+ });
+
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
// Edit integration data
- JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res, next) {
- const paramBoardId = req.params.boardId;
- const paramIntId = req.params.intId;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
+ JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ const paramIntId = req.params.intId;
+ Authentication.checkBoardAccess(req.userId, paramBoardId);
+
+ if (req.body.hasOwnProperty('enabled')) {
+ const newEnabled = req.body.enabled;
+ 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}});
+ }
+ if (req.body.hasOwnProperty('url')) {
+ const newUrl = req.body.url;
+ 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}});
+ }
+ if (req.body.hasOwnProperty('activities')) {
+ const newActivities = req.body.activities;
+ Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
+ {$set: {activities: newActivities}});
+ }
- if (req.body.hasOwnProperty('enabled')) {
- const newEnabled = req.body.enabled;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {enabled: newEnabled}});
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramIntId,
+ },
+ });
}
- if (req.body.hasOwnProperty('title')) {
- const newTitle = req.body.title;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {title: newTitle}});
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
}
- if (req.body.hasOwnProperty('url')) {
- const newUrl = req.body.url;
- 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}});
- }
- if (req.body.hasOwnProperty('activities')) {
- const newActivities = req.body.activities;
- Integrations.direct.update({_id: paramIntId, boardId: paramBoardId},
- {$set: {activities: newActivities}});
- }
-
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramIntId,
- },
- });
});
// Delete subscribed activities
- JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res, next) {
- 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}});
-
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}),
- });
+ 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}});
+
+ 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,
+ });
+ }
});
// Add subscribed activities
- JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res, next) {
- 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}}});
-
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}),
- });
+ 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}}});
+
+ 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,
+ });
+ }
});
// Delete integration
- JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res, next) {
- const paramBoardId = req.params.boardId;
- const paramIntId = req.params.intId;
- Authentication.checkBoardAccess(req.userId, paramBoardId);
-
- Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId});
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramIntId,
- },
- });
+ 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});
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramIntId,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
}
diff --git a/models/invitationCodes.js b/models/invitationCodes.js
index 5761977a..53163f06 100644
--- a/models/invitationCodes.js
+++ b/models/invitationCodes.js
@@ -34,8 +34,8 @@ InvitationCodes.helpers({
});
// InvitationCodes.before.insert((userId, doc) => {
- // doc.createdAt = new Date();
- // doc.authorId = userId;
+// doc.createdAt = new Date();
+// doc.authorId = userId;
// });
if (Meteor.isServer) {
diff --git a/models/lists.js b/models/lists.js
index d9a5b8e2..6f6996cb 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -42,6 +42,23 @@ Lists.attachSchema(new SimpleSchema({
}
},
},
+ wipLimit: {
+ type: Object,
+ optional: true,
+ },
+ 'wipLimit.value': {
+ type: Number,
+ decimal: false,
+ defaultValue: 1,
+ },
+ 'wipLimit.enabled': {
+ type: Boolean,
+ defaultValue: false,
+ },
+ 'wipLimit.soft': {
+ type: Boolean,
+ defaultValue: false,
+ },
}));
Lists.allow({
@@ -58,11 +75,15 @@ Lists.allow({
});
Lists.helpers({
- cards() {
- return Cards.find(Filter.mongoSelector({
+ cards(swimlaneId) {
+ const selector = {
listId: this._id,
archived: false,
- }), { sort: ['sort'] });
+ };
+ if (swimlaneId)
+ selector.swimlaneId = swimlaneId;
+ return Cards.find(Filter.mongoSelector(selector),
+ { sort: ['sort'] });
},
allCards() {
@@ -72,6 +93,17 @@ Lists.helpers({
board() {
return Boards.findOne(this.boardId);
},
+
+ 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
+ return 0;
+ } 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
+ }
+ },
});
Lists.mutations({
@@ -86,6 +118,44 @@ Lists.mutations({
restore() {
return { $set: { archived: false } };
},
+
+ toggleSoftLimit(toggle) {
+ return { $set: { 'wipLimit.soft': toggle } };
+ },
+
+ toggleWipLimit(toggle) {
+ return { $set: { 'wipLimit.enabled': toggle } };
+ },
+
+ setWipLimit(limit) {
+ return { $set: { 'wipLimit.value': limit } };
+ },
+});
+
+Meteor.methods({
+ applyWipLimit(listId, limit){
+ check(listId, String);
+ check(limit, Number);
+ if(limit === 0){
+ limit = 1;
+ }
+ Lists.findOne({ _id: listId }).setWipLimit(limit);
+ },
+
+ enableWipLimit(listId) {
+ check(listId, String);
+ const list = Lists.findOne({ _id: listId });
+ if(list.getWipLimit('value') === 0){
+ list.setWipLimit(1);
+ }
+ list.toggleWipLimit(!list.getWipLimit('enabled'));
+ },
+
+ enableSoftLimit(listId) {
+ check(listId, String);
+ const list = Lists.findOne({ _id: listId });
+ list.toggleSoftLimit(!list.getWipLimit('soft'));
+ },
});
Lists.hookOptions.after.update = { fetchPrevious: false };
@@ -131,57 +201,89 @@ if (Meteor.isServer) {
//LISTS REST API
if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res, next) {
- const paramBoardId = req.params.boardId;
- 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,
- };
- }),
- });
+ JsonRoutes.add('GET', '/api/boards/:boardId/lists', function (req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ 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,
+ };
+ }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId', function (req, res, next) {
- const paramBoardId = req.params.boardId;
- const paramListId = req.params.listId;
- Authentication.checkBoardAccess( req.userId, paramBoardId);
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }),
- });
+ 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);
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Lists.findOne({ _id: paramListId, boardId: paramBoardId, archived: false }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const id = Lists.insert({
- title: req.body.title,
- boardId: paramBoardId,
- });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ JsonRoutes.add('POST', '/api/boards/:boardId/lists', function (req, res) {
+ try {
+ Authentication.checkUserId( req.userId);
+ const paramBoardId = req.params.boardId;
+ const id = Lists.insert({
+ title: req.body.title,
+ boardId: paramBoardId,
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- const paramBoardId = req.params.boardId;
- const paramListId = req.params.listId;
- Lists.remove({ _id: paramListId, boardId: paramBoardId });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: paramListId,
- },
- });
+ JsonRoutes.add('DELETE', '/api/boards/:boardId/lists/:listId', function (req, res) {
+ try {
+ Authentication.checkUserId( req.userId);
+ const paramBoardId = req.params.boardId;
+ const paramListId = req.params.listId;
+ Lists.remove({ _id: paramListId, boardId: paramBoardId });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: paramListId,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
}
diff --git a/models/settings.js b/models/settings.js
index a490d9c5..34f693d9 100644
--- a/models/settings.js
+++ b/models/settings.js
@@ -105,7 +105,7 @@ if (Meteor.isServer) {
inviter: Users.findOne(icode.authorId).username,
user: icode.email.split('@')[0],
icode: icode.code,
- url: FlowRouter.url('sign-in'),
+ url: FlowRouter.url('sign-up'),
};
const lang = author.getLanguage();
Email.send({
@@ -141,5 +141,31 @@ if (Meteor.isServer) {
}
});
},
+
+ sendSMTPTestEmail() {
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('invalid-user');
+ }
+ const user = Meteor.user();
+ if (!user.emails && !user.emails[0] && user.emails[0].address) {
+ throw new Meteor.Error('email-invalid');
+ }
+ this.unblock();
+ const lang = user.getLanguage();
+ try {
+ 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}),
+ });
+ } catch ({message}) {
+ throw new Meteor.Error('email-fail', `${TAPi18n.__('email-fail-text', {lng: lang})}: ${ message }`, message);
+ }
+ return {
+ message: 'email-sent',
+ email: user.emails[0].address,
+ };
+ },
});
}
diff --git a/models/swimlanes.js b/models/swimlanes.js
new file mode 100644
index 00000000..72ef3f36
--- /dev/null
+++ b/models/swimlanes.js
@@ -0,0 +1,219 @@
+Swimlanes = new Mongo.Collection('swimlanes');
+
+Swimlanes.attachSchema(new SimpleSchema({
+ title: {
+ type: String,
+ },
+ archived: {
+ type: Boolean,
+ autoValue() { // eslint-disable-line consistent-return
+ if (this.isInsert && !this.isSet) {
+ return false;
+ }
+ },
+ },
+ boardId: {
+ type: String,
+ },
+ createdAt: {
+ type: Date,
+ autoValue() { // eslint-disable-line consistent-return
+ if (this.isInsert) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+ sort: {
+ type: Number,
+ decimal: true,
+ // XXX We should probably provide a default
+ optional: true,
+ },
+ updatedAt: {
+ type: Date,
+ optional: true,
+ autoValue() { // eslint-disable-line consistent-return
+ if (this.isUpdate) {
+ return new Date();
+ } else {
+ this.unset();
+ }
+ },
+ },
+}));
+
+Swimlanes.allow({
+ insert(userId, doc) {
+ return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
+ },
+ update(userId, doc) {
+ return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
+ },
+ remove(userId, doc) {
+ return allowIsBoardMemberNonComment(userId, Boards.findOne(doc.boardId));
+ },
+ fetch: ['boardId'],
+});
+
+Swimlanes.helpers({
+ cards() {
+ return Cards.find(Filter.mongoSelector({
+ swimlaneId: this._id,
+ archived: false,
+ }), { sort: ['sort'] });
+ },
+
+ allCards() {
+ return Cards.find({ swimlaneId: this._id });
+ },
+
+ board() {
+ return Boards.findOne(this.boardId);
+ },
+});
+
+Swimlanes.mutations({
+ rename(title) {
+ return { $set: { title } };
+ },
+
+ archive() {
+ return { $set: { archived: true } };
+ },
+
+ restore() {
+ return { $set: { archived: false } };
+ },
+});
+
+Swimlanes.hookOptions.after.update = { fetchPrevious: false };
+
+if (Meteor.isServer) {
+ Meteor.startup(() => {
+ Swimlanes._collection._ensureIndex({ boardId: 1 });
+ });
+
+ Swimlanes.after.insert((userId, doc) => {
+ Activities.insert({
+ userId,
+ type: 'swimlane',
+ activityType: 'createSwimlane',
+ boardId: doc.boardId,
+ swimlaneId: doc._id,
+ });
+ });
+
+ Swimlanes.before.remove((userId, doc) => {
+ Activities.insert({
+ userId,
+ type: 'swimlane',
+ activityType: 'removeSwimlane',
+ boardId: doc.boardId,
+ swimlaneId: doc._id,
+ title: doc.title,
+ });
+ });
+
+ Swimlanes.after.update((userId, doc) => {
+ if (doc.archived) {
+ Activities.insert({
+ userId,
+ type: 'swimlane',
+ activityType: 'archivedSwimlane',
+ swimlaneId: doc._id,
+ boardId: doc.boardId,
+ });
+ }
+ });
+}
+
+//SWIMLANE REST API
+if (Meteor.isServer) {
+ JsonRoutes.add('GET', '/api/boards/:boardId/swimlanes', function (req, res) {
+ try {
+ const paramBoardId = req.params.boardId;
+ 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,
+ };
+ }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
+ });
+
+ 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);
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Swimlanes.findOne({ _id: paramSwimlaneId, boardId: paramBoardId, archived: false }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
+ });
+
+ JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function (req, res) {
+ try {
+ Authentication.checkUserId( req.userId);
+ const paramBoardId = req.params.boardId;
+ const id = Swimlanes.insert({
+ title: req.body.title,
+ boardId: paramBoardId,
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
+ });
+
+ 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,
+ });
+ }
+ });
+
+}
diff --git a/models/trelloCreator.js b/models/trelloCreator.js
index e7f98e85..30f0bc2b 100644
--- a/models/trelloCreator.js
+++ b/models/trelloCreator.js
@@ -23,6 +23,8 @@ export class TrelloCreator {
// Map of labels Trello ID => Wekan ID
this.labels = {};
+ // Default swimlane
+ this.swimlane = null;
// Map of lists Trello ID => Wekan ID
this.lists = {};
// Map of cards Trello ID => Wekan ID
@@ -113,7 +115,6 @@ export class TrelloCreator {
check(trelloLabels, [Match.ObjectIncluding({
// XXX refine control by validating 'color' against a list of allowed
// values (is it worth the maintenance?)
- color: String,
name: String,
})]);
}
@@ -150,6 +151,7 @@ export class TrelloCreator {
isAdmin: true,
isActive: true,
isCommentOnly: false,
+ swimlaneId: false,
}],
permission: this.getPermission(trelloBoard.prefs.permissionLevel),
slug: getSlug(trelloBoard.name) || 'board',
@@ -176,6 +178,7 @@ export class TrelloCreator {
isAdmin: this.getAdmin(trelloMembership.memberType),
isActive: true,
isCommentOnly: false,
+ swimlaneId: false,
});
}
}
@@ -184,7 +187,7 @@ export class TrelloCreator {
trelloBoard.labels.forEach((label) => {
const labelToCreate = {
_id: Random.id(6),
- color: label.color,
+ color: label.color ? label.color : 'black',
name: label.name,
};
// We need to remember them by Trello ID, as this is the only ref we have
@@ -229,6 +232,7 @@ export class TrelloCreator {
dateLastActivity: this._now(),
description: card.desc,
listId: this.lists[card.idList],
+ swimlaneId: this.swimlane,
sort: card.pos,
title: card.name,
// we attribute the card to its creator if available
@@ -375,6 +379,7 @@ export class TrelloCreator {
// we require.
createdAt: this._now(this.createdAt.lists[list.id]),
title: list.name,
+ sort: list.pos,
};
const listId = Lists.direct.insert(listToCreate);
Lists.direct.update(listId, {$set: {'updatedAt': this._now()}});
@@ -396,29 +401,51 @@ export class TrelloCreator {
});
}
+ createSwimlanes(boardId) {
+ const swimlaneToCreate = {
+ archived: false,
+ boardId,
+ // We are being defensing here by providing a default date (now) if the
+ // creation date wasn't found on the action log. This happen on old
+ // Wekan boards (eg from 2013) that didn't log the 'createList' action
+ // we require.
+ createdAt: this._now(),
+ title: 'Default',
+ sort: 1,
+ };
+ const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
+ Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}});
+ this.swimlane = swimlaneId;
+ }
+
createChecklists(trelloChecklists) {
trelloChecklists.forEach((checklist) => {
- // Create the checklist
- const checklistToCreate = {
- cardId: this.cards[checklist.idCard],
- title: checklist.name,
- createdAt: this._now(),
- sort: checklist.pos,
- };
- const checklistId = Checklists.direct.insert(checklistToCreate);
- // keep track of Trello id => WeKan id
- this.checklists[checklist.id] = checklistId;
- // Now add the items to the checklist
- const itemsToCreate = [];
- checklist.checkItems.forEach((item) => {
- itemsToCreate.push({
- _id: checklistId + itemsToCreate.length,
- title: item.name,
- isFinished: item.state === 'complete',
- sort: item.pos,
+ if (this.cards[checklist.idCard]) {
+ // Create the checklist
+ const checklistToCreate = {
+ cardId: this.cards[checklist.idCard],
+ title: checklist.name,
+ createdAt: this._now(),
+ sort: checklist.pos,
+ };
+ const checklistId = Checklists.direct.insert(checklistToCreate);
+ // keep track of Trello id => WeKan id
+ this.checklists[checklist.id] = checklistId;
+ // Now add the items to the checklistItems
+ let counter = 0;
+ checklist.checkItems.forEach((item) => {
+ counter++;
+ const checklistItemTocreate = {
+ _id: checklistId + counter,
+ title: item.name,
+ checklistId: this.checklists[checklist.id],
+ cardId: this.cards[checklist.idCard],
+ sort: item.pos,
+ isFinished: item.state === 'complete',
+ };
+ ChecklistItems.direct.insert(checklistItemTocreate);
});
- });
- Checklists.direct.update(checklistId, {$set: {items: itemsToCreate}});
+ }
});
}
@@ -604,6 +631,7 @@ export class TrelloCreator {
this.parseActions(board.actions);
const boardId = this.createBoardAndLabels(board);
this.createLists(board.lists, boardId);
+ this.createSwimlanes(boardId);
this.createCards(board.cards, boardId);
this.createChecklists(board.checklists);
this.importActions(board.actions, boardId);
diff --git a/models/users.js b/models/users.js
index c2238cde..0093f7cb 100644
--- a/models/users.js
+++ b/models/users.js
@@ -43,7 +43,9 @@ Users.attachSchema(new SimpleSchema({
optional: true,
autoValue() { // eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
- return {};
+ return {
+ boardView: 'board-view-lists',
+ };
}
},
},
@@ -95,6 +97,10 @@ Users.attachSchema(new SimpleSchema({
type: String,
optional: true,
},
+ 'profile.boardView': {
+ type: String,
+ optional: true,
+ },
services: {
type: Object,
optional: true,
@@ -108,8 +114,23 @@ Users.attachSchema(new SimpleSchema({
type: Boolean,
optional: true,
},
+ createdThroughApi: {
+ type: Boolean,
+ optional: true,
+ },
+ loginDisabled: {
+ type: Boolean,
+ optional: true,
+ },
}));
+Users.allow({
+ update(userId) {
+ const user = Users.findOne(userId);
+ return user && Meteor.user().isAdmin;
+ },
+});
+
// Search a user in the complete server database by its name or username. This
// is used for instance to add a new user to a board.
const searchInFields = ['username', 'profile.fullname'];
@@ -144,36 +165,36 @@ if (Meteor.isClient) {
Users.helpers({
boards() {
- return Boards.find({ userId: this._id });
+ return Boards.find({ 'members.userId': this._id });
},
starredBoards() {
- const { starredBoards = [] } = this.profile;
- return Boards.find({ archived: false, _id: { $in: starredBoards } });
+ const {starredBoards = []} = this.profile;
+ return Boards.find({archived: false, _id: {$in: starredBoards}});
},
hasStarred(boardId) {
- const { starredBoards = [] } = this.profile;
+ const {starredBoards = []} = this.profile;
return _.contains(starredBoards, boardId);
},
invitedBoards() {
- const { invitedBoards = [] } = this.profile;
- return Boards.find({ archived: false, _id: { $in: invitedBoards } });
+ const {invitedBoards = []} = this.profile;
+ return Boards.find({archived: false, _id: {$in: invitedBoards}});
},
isInvitedTo(boardId) {
- const { invitedBoards = [] } = this.profile;
+ const {invitedBoards = []} = this.profile;
return _.contains(invitedBoards, boardId);
},
hasTag(tag) {
- const { tags = [] } = this.profile;
+ const {tags = []} = this.profile;
return _.contains(tags, tag);
},
hasNotification(activityId) {
- const { notifications = [] } = this.profile;
+ const {notifications = []} = this.profile;
return _.contains(notifications, activityId);
},
@@ -183,7 +204,7 @@ Users.helpers({
},
getEmailBuffer() {
- const { emailBuffer = [] } = this.profile;
+ const {emailBuffer = []} = this.profile;
return emailBuffer;
},
@@ -308,22 +329,30 @@ Users.mutations({
},
setAvatarUrl(avatarUrl) {
- return { $set: { 'profile.avatarUrl': avatarUrl } };
+ return {$set: {'profile.avatarUrl': avatarUrl}};
},
setShowCardsCountAt(limit) {
- return { $set: { 'profile.showCardsCountAt': limit } };
+ return {$set: {'profile.showCardsCountAt': limit}};
+ },
+
+ setBoardView(view) {
+ return {
+ $set : {
+ 'profile.boardView': view,
+ },
+ };
},
});
Meteor.methods({
- setUsername(username) {
+ setUsername(username, userId) {
check(username, String);
- const nUsersWithUsername = Users.find({ username }).count();
+ const nUsersWithUsername = Users.find({username}).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else {
- Users.update(this.userId, { $set: { username } });
+ Users.update(userId, {$set: {username}});
}
},
toggleSystemMessages() {
@@ -334,13 +363,13 @@ Meteor.methods({
check(limit, Number);
Meteor.user().setShowCardsCountAt(limit);
},
- setEmail(email) {
+ setEmail(email, userId) {
check(email, String);
- const existingUser = Users.findOne({ 'emails.address': email }, { fields: { _id: 1 } });
+ const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}});
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
- Users.update(this.userId, {
+ Users.update(userId, {
$set: {
emails: [{
address: email,
@@ -350,11 +379,19 @@ Meteor.methods({
});
}
},
- setUsernameAndEmail(username, email) {
+ setUsernameAndEmail(username, email, userId) {
check(username, String);
check(email, String);
- Meteor.call('setUsername', username);
- Meteor.call('setEmail', email);
+ check(userId, String);
+ Meteor.call('setUsername', username, userId);
+ Meteor.call('setEmail', email, userId);
+ },
+ setPassword(newPassword, userId) {
+ check(userId, String);
+ check(newPassword, String);
+ if(Meteor.user().isAdmin){
+ Accounts.setPassword(userId, newPassword);
+ }
},
});
@@ -371,8 +408,8 @@ if (Meteor.isServer) {
board &&
board.members &&
_.contains(_.pluck(board.members, 'userId'), inviter._id) &&
- _.where(board.members, { userId: inviter._id })[0].isActive &&
- _.where(board.members, { userId: inviter._id })[0].isAdmin;
+ _.where(board.members, {userId: inviter._id})[0].isActive &&
+ _.where(board.members, {userId: inviter._id})[0].isAdmin;
if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
this.unblock();
@@ -380,9 +417,9 @@ if (Meteor.isServer) {
const posAt = username.indexOf('@');
let user = null;
if (posAt >= 0) {
- user = Users.findOne({ emails: { $elemMatch: { address: username } } });
+ user = Users.findOne({emails: {$elemMatch: {address: username}}});
} else {
- user = Users.findOne(username) || Users.findOne({ username });
+ user = Users.findOne(username) || Users.findOne({username});
}
if (user) {
if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
@@ -392,7 +429,7 @@ if (Meteor.isServer) {
// Set in lowercase email before creating account
const email = username.toLowerCase();
username = email.substring(0, posAt);
- const newUserId = Accounts.createUser({ username, email });
+ const newUserId = Accounts.createUser({username, email});
if (!newUserId) throw new Meteor.Error('error-user-notCreated');
// assume new user speak same language with inviter
if (inviter.profile && inviter.profile.language) {
@@ -426,7 +463,7 @@ if (Meteor.isServer) {
} catch (e) {
throw new Meteor.Error('email-fail', e.message);
}
- return { username: user.username, email: user.emails[0].address };
+ return {username: user.username, email: user.emails[0].address};
},
});
Accounts.onCreateUser((options, user) => {
@@ -435,6 +472,12 @@ if (Meteor.isServer) {
user.isAdmin = true;
return user;
}
+
+ if (options.from === 'admin') {
+ user.createdThroughApi = true;
+ return user;
+ }
+
const disableRegistration = Settings.findOne().disableRegistration;
if (!disableRegistration) {
return user;
@@ -443,11 +486,16 @@ if (Meteor.isServer) {
if (!options || !options.profile) {
throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required');
}
- const invitationCode = InvitationCodes.findOne({ code: options.profile.invitationcode, email: options.email, valid: true });
+ const invitationCode = InvitationCodes.findOne({
+ code: options.profile.invitationcode,
+ email: options.email,
+ valid: true,
+ });
if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist');
} else {
- user.profile = { icode: options.profile.invitationcode };
+ user.profile = {icode: options.profile.invitationcode};
+ user.profile.boardView = 'board-view-lists';
}
return user;
@@ -459,7 +507,7 @@ if (Meteor.isServer) {
Meteor.startup(() => {
Users._collection._ensureIndex({
username: 1,
- }, { unique: true });
+ }, {unique: true});
});
// Each board document contains the de-normalized number of users that have
@@ -478,6 +526,7 @@ if (Meteor.isServer) {
function getStarredBoardsIds(doc) {
return doc.profile && doc.profile.starredBoards;
}
+
const oldIds = getStarredBoardsIds(this.previous);
const newIds = getStarredBoardsIds(user);
@@ -486,9 +535,10 @@ if (Meteor.isServer) {
// direction and then in the other.
function incrementBoards(boardsIds, inc) {
boardsIds.forEach((boardId) => {
- Boards.update(boardId, { $inc: { stars: inc } });
+ Boards.update(boardId, {$inc: {stars: inc}});
});
}
+
incrementBoards(_.difference(oldIds, newIds), -1);
incrementBoards(_.difference(newIds, oldIds), +1);
});
@@ -514,8 +564,14 @@ if (Meteor.isServer) {
permission: 'private',
}, fakeUser, (err, boardId) => {
- ['welcome-list1', 'welcome-list2'].forEach((title) => {
- Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser);
+ Swimlanes.insert({
+ title: TAPi18n.__('welcome-swimlane'),
+ boardId,
+ sort: 1,
+ }, fakeUser);
+
+ ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
+ Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
});
});
});
@@ -524,10 +580,21 @@ if (Meteor.isServer) {
Users.after.insert((userId, doc) => {
+ if (doc.createdThroughApi) {
+ // The admin user should be able to create a user despite disabling registration because
+ // it is two different things (registration and creation).
+ // So, when a new user is created via the api (only admin user can do that) one must avoid
+ // the disableRegistration check.
+ // Issue : https://github.com/wekan/wekan/issues/1232
+ // PR : https://github.com/wekan/wekan/pull/1251
+ Users.update(doc._id, {$set: {createdThroughApi: ''}});
+ return;
+ }
+
//invite user to corresponding boards
const disableRegistration = Settings.findOne().disableRegistration;
if (disableRegistration) {
- const invitationCode = InvitationCodes.findOne({ code: doc.profile.icode, valid: true });
+ const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true});
if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist');
} else {
@@ -539,8 +606,8 @@ if (Meteor.isServer) {
doc.profile = {};
}
doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
- Users.update(doc._id, { $set: { profile: doc.profile } });
- InvitationCodes.update(invitationCode._id, { $set: { valid: false } });
+ Users.update(doc._id, {$set: {profile: doc.profile}});
+ InvitationCodes.update(invitationCode._id, {$set: {valid: false}});
}
}
});
@@ -549,59 +616,143 @@ if (Meteor.isServer) {
// USERS REST API
if (Meteor.isServer) {
- JsonRoutes.add('GET', '/api/user', function(req, res, next) {
- Authentication.checkLoggedIn(req.userId);
- const data = Meteor.users.findOne({ _id: req.userId});
- delete data.services;
- JsonRoutes.sendResult(res, {
- code: 200,
- data,
- });
+ JsonRoutes.add('GET', '/api/user', function(req, res) {
+ try {
+ Authentication.checkLoggedIn(req.userId);
+ const data = Meteor.users.findOne({ _id: req.userId});
+ delete data.services;
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data,
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('GET', '/api/users', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Meteor.users.find({}).map(function (doc) {
- return { _id: doc._id, username: doc.username };
- }),
- });
+ JsonRoutes.add('GET', '/api/users', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Meteor.users.find({}).map(function (doc) {
+ return { _id: doc._id, username: doc.username };
+ }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('GET', '/api/users/:id', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- const id = req.params.id;
- JsonRoutes.sendResult(res, {
- code: 200,
- data: Meteor.users.findOne({ _id: id }),
- });
+
+ JsonRoutes.add('GET', '/api/users/:id', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const id = req.params.id;
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: Meteor.users.findOne({ _id: id }),
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('POST', '/api/users/', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- const id = Accounts.createUser({
- username: req.body.username,
- email: req.body.email,
- password: 'default',
- });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ JsonRoutes.add('PUT', '/api/users/:id', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const id = req.params.id;
+ const action = req.body.action;
+ let data = Meteor.users.findOne({ _id: id });
+ if (data !== undefined) {
+ if (action === 'takeOwnership') {
+ data = Boards.find({
+ 'members.userId': id,
+ 'members.isAdmin': true,
+ }).map(function(board) {
+ if (board.hasMember(req.userId)) {
+ board.removeMember(req.userId);
+ }
+ board.changeOwnership(id, req.userId);
+ return {
+ _id: board._id,
+ title: board.title,
+ };
+ });
+ } else {
+ if ((action === 'disableLogin') && (id !== req.userId)) {
+ Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } });
+ } else if (action === 'enableLogin') {
+ Users.update({ _id: id }, { $set: { loginDisabled: '' } });
+ }
+ data = Meteor.users.findOne({ _id: id });
+ }
+ }
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data,
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
- JsonRoutes.add('DELETE', '/api/users/:id', function (req, res, next) {
- Authentication.checkUserId( req.userId);
- const id = req.params.id;
- Meteor.users.remove({ _id: id });
- JsonRoutes.sendResult(res, {
- code: 200,
- data: {
- _id: id,
- },
- });
+ JsonRoutes.add('POST', '/api/users/', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const id = Accounts.createUser({
+ username: req.body.username,
+ email: req.body.email,
+ password: req.body.password,
+ from: 'admin',
+ });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
});
-}
+ JsonRoutes.add('DELETE', '/api/users/:id', function (req, res) {
+ try {
+ Authentication.checkUserId(req.userId);
+ const id = req.params.id;
+ Meteor.users.remove({ _id: id });
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: {
+ _id: id,
+ },
+ });
+ }
+ catch (error) {
+ JsonRoutes.sendResult(res, {
+ code: 200,
+ data: error,
+ });
+ }
+ });
+}
diff --git a/models/wekanCreator.js b/models/wekanCreator.js
index 3cd65fd7..4551979b 100644
--- a/models/wekanCreator.js
+++ b/models/wekanCreator.js
@@ -14,6 +14,7 @@ export class WekanCreator {
board: null,
cards: {},
lists: {},
+ swimlanes: {},
};
// The object creator Wekan Id, indexed by the object Wekan id
// (so we only parse actions once!)
@@ -23,6 +24,8 @@ export class WekanCreator {
// Map of labels Wekan ID => Wekan ID
this.labels = {};
+ // Map of swimlanes Wekan ID => Wekan ID
+ this.swimlanes = {};
// Map of lists Wekan ID => Wekan ID
this.lists = {};
// Map of cards Wekan ID => Wekan ID
@@ -33,6 +36,8 @@ export class WekanCreator {
this.attachmentIds = {};
// Map of checklists Wekan ID => Wekan ID
this.checklists = {};
+ // Map of checklistItems Wekan ID => Wekan ID
+ this.checklistItems = {};
// The comments, indexed by Wekan card id (to map when importing cards)
this.comments = {};
// the members, indexed by Wekan member id => Wekan user ID
@@ -121,59 +126,62 @@ export class WekanCreator {
})]);
}
+ checkSwimlanes(wekanSwimlanes) {
+ check(wekanSwimlanes, [Match.ObjectIncluding({
+ archived: Boolean,
+ title: String,
+ })]);
+ }
+
checkChecklists(wekanChecklists) {
check(wekanChecklists, [Match.ObjectIncluding({
cardId: String,
title: String,
- items: [Match.ObjectIncluding({
- isFinished: Boolean,
- title: String,
- })],
+ })]);
+ }
+
+ checkChecklistItems(wekanChecklistItems) {
+ check(wekanChecklistItems, [Match.ObjectIncluding({
+ cardId: String,
+ title: String,
})]);
}
// You must call parseActions before calling this one.
- createBoardAndLabels(wekanBoard) {
+ createBoardAndLabels(boardToImport) {
const boardToCreate = {
- archived: wekanBoard.archived,
- color: wekanBoard.color,
+ archived: boardToImport.archived,
+ color: boardToImport.color,
// very old boards won't have a creation activity so no creation date
- createdAt: this._now(wekanBoard.createdAt),
+ createdAt: this._now(boardToImport.createdAt),
labels: [],
members: [{
userId: Meteor.userId(),
- isAdmin: true,
+ wekanId: Meteor.userId(),
isActive: true,
+ isAdmin: true,
isCommentOnly: false,
+ swimlaneId: false,
}],
// Standalone Export has modifiedAt missing, adding modifiedAt to fix it
- modifiedAt: this._now(wekanBoard.modifiedAt),
- permission: wekanBoard.permission,
- slug: getSlug(wekanBoard.title) || 'board',
+ modifiedAt: this._now(boardToImport.modifiedAt),
+ permission: boardToImport.permission,
+ slug: getSlug(boardToImport.title) || 'board',
stars: 0,
- title: wekanBoard.title,
+ title: boardToImport.title,
};
// now add other members
- if(wekanBoard.members) {
- wekanBoard.members.forEach((wekanMember) => {
- const wekanId = wekanMember.userId;
- // do we have a mapping?
- if(this.members[wekanId]) {
- const wekanId = this.members[wekanId];
- // do we already have it in our list?
- const wekanMember = boardToCreate.members.find((wekanMember) => wekanMember.userId === wekanId);
- if(!wekanMember) {
- boardToCreate.members.push({
- userId: wekanId,
- isAdmin: wekanMember.isAdmin,
- isActive: true,
- isCommentOnly: false,
- });
- }
- }
+ if(boardToImport.members) {
+ boardToImport.members.forEach((wekanMember) => {
+ // do we already have it in our list?
+ if(!boardToCreate.members.some((member) => member.wekanId === wekanMember.wekanId))
+ boardToCreate.members.push({
+ ... wekanMember,
+ userId: wekanMember.wekanId,
+ });
});
}
- wekanBoard.labels.forEach((label) => {
+ boardToImport.labels.forEach((label) => {
const labelToCreate = {
_id: Random.id(6),
color: label.color,
@@ -192,7 +200,7 @@ export class WekanCreator {
boardId,
createdAt: this._now(),
source: {
- id: wekanBoard.id,
+ id: boardToImport.id,
system: 'Wekan',
},
// We attribute the import to current user,
@@ -220,11 +228,15 @@ export class WekanCreator {
dateLastActivity: this._now(),
description: card.description,
listId: this.lists[card.listId],
+ swimlaneId: this.swimlanes[card.swimlaneId],
sort: card.sort,
title: card.title,
// we attribute the card to its creator if available
userId: this._user(this.createdBy.cards[card._id]),
+ isOvertime: card.isOvertime || false,
+ startAt: card.startAt ? this._now(card.startAt) : null,
dueAt: card.dueAt ? this._now(card.dueAt) : null,
+ spentTime: card.spentTime || null,
};
// add labels
if (card.labelIds) {
@@ -378,7 +390,7 @@ export class WekanCreator {
}
createLists(wekanLists, boardId) {
- wekanLists.forEach((list) => {
+ wekanLists.forEach((list, listIndex) => {
const listToCreate = {
archived: list.archived,
boardId,
@@ -388,6 +400,7 @@ export class WekanCreator {
// we require.
createdAt: this._now(this.createdAt.lists[list.id]),
title: list.title,
+ sort: list.sort ? list.sort : listIndex,
};
const listId = Lists.direct.insert(listToCreate);
Lists.direct.update(listId, {$set: {'updatedAt': this._now()}});
@@ -409,7 +422,27 @@ export class WekanCreator {
});
}
+ createSwimlanes(wekanSwimlanes, boardId) {
+ wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
+ const swimlaneToCreate = {
+ archived: swimlane.archived,
+ boardId,
+ // We are being defensing here by providing a default date (now) if the
+ // creation date wasn't found on the action log. This happen on old
+ // Wekan boards (eg from 2013) that didn't log the 'createList' action
+ // we require.
+ createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
+ title: swimlane.title,
+ sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
+ };
+ const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
+ Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}});
+ this.swimlanes[swimlane._id] = swimlaneId;
+ });
+ }
+
createChecklists(wekanChecklists) {
+ const result = [];
wekanChecklists.forEach((checklist, checklistIndex) => {
// Create the checklist
const checklistToCreate = {
@@ -419,19 +452,24 @@ export class WekanCreator {
sort: checklist.sort ? checklist.sort : checklistIndex,
};
const checklistId = Checklists.direct.insert(checklistToCreate);
- // keep track of Wekan id => WeKan id
this.checklists[checklist._id] = checklistId;
- // Now add the items to the checklist
- const itemsToCreate = [];
- checklist.items.forEach((item, itemIndex) => {
- itemsToCreate.push({
- _id: checklistId + itemsToCreate.length,
- title: item.title,
- isFinished: item.isFinished,
- sort: item.sort ? item.sort : itemIndex,
- });
- });
- Checklists.direct.update(checklistId, {$set: {items: itemsToCreate}});
+ result.push(checklistId);
+ });
+ return result;
+ }
+
+ createChecklistItems(wekanChecklistItems) {
+ wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
+ // Create the checklistItem
+ const checklistItemTocreate = {
+ title: checklistitem.title,
+ checklistId: this.checklists[checklistitem.checklistId],
+ cardId: this.cards[checklistitem.cardId],
+ sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
+ isFinished: checklistitem.isFinished,
+ };
+ const checklistItemId = ChecklistItems.direct.insert(checklistItemTocreate);
+ this.checklistItems[checklistitem._id] = checklistItemId;
});
}
@@ -445,14 +483,17 @@ export class WekanCreator {
const wekanAttachment = wekanBoard.attachments.filter((attachment) => {
return attachment._id === activity.attachmentId;
})[0];
- if(wekanAttachment.url || wekanAttachment.file) {
+
+ if ( typeof wekanAttachment !== 'undefined' && wekanAttachment ) {
+ if(wekanAttachment.url || wekanAttachment.file) {
// we cannot actually create the Wekan attachment, because we don't yet
// have the cards to attach it to, so we store it in the instance variable.
- const wekanCardId = activity.cardId;
- if(!this.attachments[wekanCardId]) {
- this.attachments[wekanCardId] = [];
+ const wekanCardId = activity.cardId;
+ if(!this.attachments[wekanCardId]) {
+ this.attachments[wekanCardId] = [];
+ }
+ this.attachments[wekanCardId].push(wekanAttachment);
}
- this.attachments[wekanCardId].push(wekanAttachment);
}
break;
}
@@ -481,6 +522,11 @@ export class WekanCreator {
const listId = activity.listId;
this.createdAt.lists[listId] = activity.createdAt;
break;
+ }
+ case 'createSwimlane': {
+ const swimlaneId = activity.swimlaneId;
+ this.createdAt.swimlanes[swimlaneId] = activity.createdAt;
+ break;
}}
});
}
@@ -602,8 +648,10 @@ export class WekanCreator {
this.checkBoard(board);
this.checkLabels(board.labels);
this.checkLists(board.lists);
+ this.checkSwimlanes(board.swimlanes);
this.checkCards(board.cards);
this.checkChecklists(board.checklists);
+ this.checkChecklistItems(board.checklistItems);
} catch (e) {
throw new Meteor.Error('error-json-schema');
}
@@ -620,8 +668,10 @@ export class WekanCreator {
this.parseActivities(board);
const boardId = this.createBoardAndLabels(board);
this.createLists(board.lists, boardId);
+ this.createSwimlanes(board.swimlanes, boardId);
this.createCards(board.cards, boardId);
this.createChecklists(board.checklists);
+ this.createChecklistItems(board.checklistItems);
this.importActivities(board.activities, boardId);
// XXX add members
return boardId;