diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/accountSettings.js | 8 | ||||
-rw-r--r-- | models/activities.js | 11 | ||||
-rw-r--r-- | models/attachments.js | 158 | ||||
-rw-r--r-- | models/boards.js | 214 | ||||
-rw-r--r-- | models/cards.js | 714 | ||||
-rw-r--r-- | models/checklistItems.js | 50 | ||||
-rw-r--r-- | models/checklists.js | 116 | ||||
-rw-r--r-- | models/customFields.js | 132 | ||||
-rw-r--r-- | models/export.js | 8 | ||||
-rw-r--r-- | models/settings.js | 46 | ||||
-rw-r--r-- | models/trelloCreator.js | 19 | ||||
-rw-r--r-- | models/users.js | 49 | ||||
-rw-r--r-- | models/wekanCreator.js | 64 |
13 files changed, 1327 insertions, 262 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 3f1d28ae..5b54759c 100644 --- a/models/activities.js +++ b/models/activities.js @@ -44,6 +44,12 @@ Activities.helpers({ checklistItem() { return ChecklistItems.findOne(this.checklistItemId); }, + subtasks() { + return Cards.findOne(this.subtaskId); + }, + customField() { + return CustomFields.findOne(this.customFieldId); + }, }); Activities.before.insert((userId, doc) => { @@ -60,6 +66,7 @@ if (Meteor.isServer) { 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.after.insert((userId, doc) => { @@ -127,6 +134,10 @@ if (Meteor.isServer) { const checklistItem = activity.checklistItem(); params.checklistItem = checklistItem.title; } + if (activity.customFieldId) { + const customField = activity.customField(); + params.customField = customField.name; + } if (board) { const watchingUsers = _.pluck(_.where(board.watchers, {level: 'watching'}), 'userId'); const trackingUsers = _.pluck(_.where(board.watchers, {level: 'tracking'}), 'userId'); diff --git a/models/attachments.js b/models/attachments.js index 5e5c4926..91dd0dbc 100644 --- a/models/attachments.js +++ b/models/attachments.js @@ -1,90 +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', - }; - } - 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); + // 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 {}; }, + }), + ], +}); - fetch: ['boardId'], - }); - } - - // 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, - }); +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 { - // Don't add activity about adding the attachment as the activity - // be imported and delete source field - Attachments.update({ - _id: doc._id, - }, { - $unset: { - source: '', - }, - }); + return board.hasMember(userId); } - }); + }, + + fetch: ['boardId'], + }); +} - Attachments.files.after.remove((userId, doc) => { - Activities.remove({ +// 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: '', + }, + }); + } + }); + + Attachments.files.after.remove((userId, doc) => { + Activities.remove({ + attachmentId: doc._id, }); - } + }); +} diff --git a/models/boards.js b/models/boards.js index 436a99f5..a017eb3f 100644 --- a/models/boards.js +++ b/models/boards.js @@ -31,14 +31,6 @@ Boards.attachSchema(new SimpleSchema({ } }, }, - view: { - type: String, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert) { - return 'board-view-lists'; - } - }, - }, createdAt: { type: Date, autoValue() { // eslint-disable-line consistent-return @@ -102,6 +94,9 @@ Boards.attachSchema(new SimpleSchema({ allowedValues: [ 'green', 'yellow', 'orange', 'red', 'purple', 'blue', 'sky', 'lime', 'pink', 'black', + 'silver', 'peachpuff', 'crimson', 'plum', 'darkgreen', + 'slateblue', 'magenta', 'gold', 'navy', 'gray', + 'saddlebrown', 'paleturquoise', 'mistyrose', 'indigo', ], }, // XXX We might want to maintain more informations under the member sub- @@ -156,6 +151,54 @@ Boards.attachSchema(new SimpleSchema({ type: String, optional: true, }, + subtasksDefaultBoardId: { + type: String, + optional: true, + defaultValue: null, + }, + subtasksDefaultListId: { + type: String, + optional: true, + defaultValue: null, + }, + allowsSubtasks: { + type: Boolean, + defaultValue: true, + }, + presentParentTask: { + type: String, + allowedValues: [ + 'prefix-with-full-path', + 'prefix-with-parent', + 'subtext-with-full-path', + 'subtext-with-parent', + 'no-parent', + ], + optional: true, + defaultValue: 'no-parent', + }, + startAt: { + type: Date, + optional: true, + }, + dueAt: { + type: Date, + optional: true, + }, + endAt: { + type: Date, + optional: true, + }, + spentTime: { + type: Number, + decimal: true, + optional: true, + }, + isOvertime: { + type: Boolean, + defaultValue: false, + optional: true, + }, })); @@ -191,6 +234,10 @@ Boards.helpers({ return this.permission === 'public'; }, + cards() { + return Cards.find({ boardId: this._id, archived: false }, { sort: { title: 1 } }); + }, + lists() { return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); }, @@ -257,6 +304,10 @@ Boards.helpers({ return `board-color-${this.color}`; }, + customFields() { + return CustomFields.find({ boardId: this._id }, { sort: { name: 1 } }); + }, + // XXX currently mutations return no value so we have an issue when using addLabel in import // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove... pushLabel(name, color) { @@ -265,28 +316,112 @@ Boards.helpers({ return _id; }, - searchCards(term) { + searchCards(term, excludeLinked) { check(term, Match.OneOf(String, null, undefined)); - let query = { boardId: this._id }; + const query = { boardId: this._id }; + if (excludeLinked) { + query.linkedId = null; + } const projection = { limit: 10, sort: { createdAt: -1 } }; if (term) { const regex = new RegExp(term, 'i'); - query = { - boardId: this._id, - $or: [ - { title: regex }, - { description: regex }, - ], - }; + query.$or = [ + { title: regex }, + { description: regex }, + ]; } return Cards.find(query, projection); }, + // A board alwasy has another board where it deposits subtasks of thasks + // that belong to itself. + getDefaultSubtasksBoardId() { + if ((this.subtasksDefaultBoardId === null) || (this.subtasksDefaultBoardId === undefined)) { + this.subtasksDefaultBoardId = Boards.insert({ + title: `^${this.title}^`, + permission: this.permission, + members: this.members, + color: this.color, + description: TAPi18n.__('default-subtasks-board', {board: this.title}), + }); + + Swimlanes.insert({ + title: TAPi18n.__('default'), + boardId: this.subtasksDefaultBoardId, + }); + Boards.update(this._id, {$set: { + subtasksDefaultBoardId: this.subtasksDefaultBoardId, + }}); + } + return this.subtasksDefaultBoardId; + }, + + getDefaultSubtasksBoard() { + return Boards.findOne(this.getDefaultSubtasksBoardId()); + }, + + getDefaultSubtasksListId() { + if ((this.subtasksDefaultListId === null) || (this.subtasksDefaultListId === undefined)) { + this.subtasksDefaultListId = Lists.insert({ + title: TAPi18n.__('queue'), + boardId: this._id, + }); + Boards.update(this._id, {$set: { + subtasksDefaultListId: this.subtasksDefaultListId, + }}); + } + return this.subtasksDefaultListId; + }, + + getDefaultSubtasksList() { + return Lists.findOne(this.getDefaultSubtasksListId()); + }, + + getDefaultSwimline() { + let result = Swimlanes.findOne({boardId: this._id}); + if (result === undefined) { + Swimlanes.insert({ + title: TAPi18n.__('default'), + boardId: this._id, + }); + result = Swimlanes.findOne({boardId: this._id}); + } + return result; + }, + + cardsInInterval(start, end) { + return Cards.find({ + boardId: this._id, + $or: [ + { + startAt: { + $lte: start, + }, endAt: { + $gte: start, + }, + }, { + startAt: { + $lte: end, + }, endAt: { + $gte: end, + }, + }, { + startAt: { + $gte: start, + }, endAt: { + $lte: end, + }, + }, + ], + }); + }, + }); + Boards.mutations({ archive() { return { $set: { archived: true } }; @@ -408,6 +543,22 @@ Boards.mutations({ }, }; }, + + setAllowsSubtasks(allowsSubtasks) { + return { $set: { allowsSubtasks } }; + }, + + setSubtasksDefaultBoardId(subtasksDefaultBoardId) { + return { $set: { subtasksDefaultBoardId } }; + }, + + setSubtasksDefaultListId(subtasksDefaultListId) { + return { $set: { subtasksDefaultListId } }; + }, + + setPresentParentTask(presentParentTask) { + return { $set: { presentParentTask } }; + }, }); if (Meteor.isServer) { @@ -727,4 +878,33 @@ if (Meteor.isServer) { }); } }); + + JsonRoutes.add('PUT', '/api/boards/:id/labels', function (req, res) { + Authentication.checkUserId(req.userId); + const id = req.params.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/cards.js b/models/cards.js index 4a662953..302beddc 100644 --- a/models/cards.js +++ b/models/cards.js @@ -6,6 +6,8 @@ Cards = new Mongo.Collection('cards'); Cards.attachSchema(new SimpleSchema({ title: { type: String, + optional: true, + defaultValue: '', }, archived: { type: Boolean, @@ -15,8 +17,15 @@ Cards.attachSchema(new SimpleSchema({ } }, }, + parentId: { + type: String, + optional: true, + defaultValue: '', + }, listId: { type: String, + optional: true, + defaultValue: '', }, swimlaneId: { type: String, @@ -26,10 +35,14 @@ Cards.attachSchema(new SimpleSchema({ // difficult to manage and less efficient. boardId: { type: String, + optional: true, + defaultValue: '', }, coverId: { type: String, optional: true, + defaultValue: '', + }, createdAt: { type: Date, @@ -41,6 +54,25 @@ Cards.attachSchema(new SimpleSchema({ } }, }, + customFields: { + type: [Object], + optional: true, + defaultValue: [], + }, + 'customFields.$': { + type: new SimpleSchema({ + _id: { + type: String, + optional: true, + defaultValue: '', + }, + value: { + type: Match.OneOf(String, Number, Boolean, Date), + optional: true, + defaultValue: '', + }, + }), + }, dateLastActivity: { type: Date, autoValue() { @@ -50,14 +82,27 @@ Cards.attachSchema(new SimpleSchema({ description: { type: String, optional: true, + defaultValue: '', + }, + requestedBy: { + type: String, + optional: true, + defaultValue: '', + }, + assignedBy: { + type: String, + optional: true, + defaultValue: '', }, labelIds: { type: [String], optional: true, + defaultValue: [], }, members: { type: [String], optional: true, + defaultValue: [], }, receivedAt: { type: Date, @@ -79,6 +124,7 @@ Cards.attachSchema(new SimpleSchema({ type: Number, decimal: true, optional: true, + defaultValue: 0, }, isOvertime: { type: Boolean, @@ -98,6 +144,22 @@ Cards.attachSchema(new SimpleSchema({ sort: { type: Number, decimal: true, + defaultValue: '', + }, + subtaskSort: { + type: Number, + decimal: true, + defaultValue: -1, + optional: true, + }, + type: { + type: String, + defaultValue: '', + }, + linkedId: { + type: String, + optional: true, + defaultValue: '', }, })); @@ -140,19 +202,33 @@ Cards.helpers({ }, isAssigned(memberId) { - return _.contains(this.members, memberId); + return _.contains(this.getMembers(), memberId); }, activities() { - return Activities.find({cardId: this._id}, {sort: {createdAt: -1}}); + if (this.isLinkedCard()) { + return Activities.find({cardId: this.linkedId}, {sort: {createdAt: -1}}); + } else if (this.isLinkedBoard()) { + return Activities.find({boardId: this.linkedId}, {sort: {createdAt: -1}}); + } else { + return Activities.find({cardId: this._id}, {sort: {createdAt: -1}}); + } }, comments() { - return CardComments.find({cardId: this._id}, {sort: {createdAt: -1}}); + if (this.isLinkedCard()) { + return CardComments.find({cardId: this.linkedId}, {sort: {createdAt: -1}}); + } else { + return CardComments.find({cardId: this._id}, {sort: {createdAt: -1}}); + } }, attachments() { - return Attachments.find({cardId: this._id}, {sort: {uploadedAt: -1}}); + if (this.isLinkedCard()) { + return Attachments.find({cardId: this.linkedId}, {sort: {uploadedAt: -1}}); + } else { + return Attachments.find({cardId: this._id}, {sort: {uploadedAt: -1}}); + } }, cover() { @@ -163,7 +239,11 @@ Cards.helpers({ }, checklists() { - return Checklists.find({cardId: this._id}, {sort: { sort: 1 } }); + if (this.isLinkedCard()) { + return Checklists.find({cardId: this.linkedId}, {sort: { sort: 1 } }); + } else { + return Checklists.find({cardId: this._id}, {sort: { sort: 1 } }); + } }, checklistItemCount() { @@ -192,6 +272,82 @@ Cards.helpers({ return this.checklistItemCount() !== 0; }, + subtasks() { + return Cards.find({ + parentId: this._id, + archived: false, + }, {sort: { sort: 1 } }); + }, + + allSubtasks() { + return Cards.find({ + parentId: this._id, + archived: false, + }, {sort: { sort: 1 } }); + }, + + subtasksCount() { + return Cards.find({ + parentId: this._id, + archived: false, + }).count(); + }, + + subtasksFinishedCount() { + return Cards.find({ + parentId: this._id, + archived: true}).count(); + }, + + subtasksFinished() { + const finishCount = this.subtasksFinishedCount(); + return finishCount > 0 && this.subtasksCount() === finishCount; + }, + + allowsSubtasks() { + return this.subtasksCount() !== 0; + }, + + customFieldIndex(customFieldId) { + return _.pluck(this.customFields, '_id').indexOf(customFieldId); + }, + + // customFields with definitions + customFieldsWD() { + + // get all definitions + const definitions = CustomFields.find({ + boardId: this.boardId, + }).fetch(); + + // match right definition to each field + if (!this.customFields) return []; + return this.customFields.map((customField) => { + const definition = definitions.find((definition) => { + return definition._id === customField._id; + }); + //search for "True Value" which is for DropDowns other then the Value (which is the id) + let trueValue = customField.value; + if (definition.settings.dropdownItems && definition.settings.dropdownItems.length > 0) + { + for (let i = 0; i < definition.settings.dropdownItems.length; i++) + { + if (definition.settings.dropdownItems[i]._id === customField.value) + { + trueValue = definition.settings.dropdownItems[i].name; + } + } + } + return { + _id: customField._id, + value: customField.value, + trueValue, + definition, + }; + }); + + }, + absoluteUrl() { const board = this.board(); return FlowRouter.url('card', { @@ -208,119 +364,550 @@ Cards.helpers({ } return true; }, -}); -Cards.mutations({ - archive() { - return {$set: {archived: true}}; + parentCard() { + if (this.parentId === '') { + return null; + } + return Cards.findOne(this.parentId); }, - restore() { - return {$set: {archived: false}}; + parentCardName() { + let result = ''; + if (this.parentId !== '') { + const card = Cards.findOne(this.parentId); + if (card) { + result = card.title; + } + } + return result; }, - setTitle(title) { - return {$set: {title}}; + parentListId() { + const result = []; + let crtParentId = this.parentId; + while (crtParentId !== '') { + const crt = Cards.findOne(crtParentId); + if ((crt === null) || (crt === undefined)) { + // maybe it has been deleted + break; + } + if (crtParentId in result) { + // circular reference + break; + } + result.unshift(crtParentId); + crtParentId = crt.parentId; + } + return result; }, - setDescription(description) { - return {$set: {description}}; + parentList() { + const resultId = []; + const result = []; + let crtParentId = this.parentId; + while (crtParentId !== '') { + const crt = Cards.findOne(crtParentId); + if ((crt === null) || (crt === undefined)) { + // maybe it has been deleted + break; + } + if (crtParentId in resultId) { + // circular reference + break; + } + resultId.unshift(crtParentId); + result.unshift(crt); + crtParentId = crt.parentId; + } + return result; }, - move(swimlaneId, listId, sortIndex) { - const list = Lists.findOne(listId); - const mutatedFields = { - swimlaneId, - listId, - boardId: list.boardId, - sort: sortIndex, - }; + parentString(sep) { + return this.parentList().map(function(elem){ + return elem.title; + }).join(sep); + }, - return {$set: mutatedFields}; + isTopLevel() { + return this.parentId === ''; }, - addLabel(labelId) { - return {$addToSet: {labelIds: labelId}}; + isLinkedCard() { + return this.type === 'cardType-linkedCard'; }, - removeLabel(labelId) { - return {$pull: {labelIds: labelId}}; + isLinkedBoard() { + return this.type === 'cardType-linkedBoard'; }, - toggleLabel(labelId) { - if (this.labelIds && this.labelIds.indexOf(labelId) > -1) { - return this.removeLabel(labelId); + isLinked() { + return this.isLinkedCard() || this.isLinkedBoard(); + }, + + setDescription(description) { + if (this.isLinkedCard()) { + return Cards.update({_id: this.linkedId}, {$set: {description}}); + } else if (this.isLinkedBoard()) { + return Boards.update({_id: this.linkedId}, {$set: {description}}); } else { - return this.addLabel(labelId); + return Cards.update( + {_id: this._id}, + {$set: {description}} + ); + } + }, + + getDescription() { + if (this.isLinkedCard()) { + const card = Cards.findOne({_id: this.linkedId}); + if (card && card.description) + return card.description; + else + return null; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + if (board && board.description) + return board.description; + else + return null; + } else if (this.description) { + return this.description; + } else { + return null; + } + }, + + getMembers() { + if (this.isLinkedCard()) { + const card = Cards.findOne({_id: this.linkedId}); + return card.members; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + return board.activeMembers().map((member) => { + return member.userId; + }); + } else { + return this.members; } }, assignMember(memberId) { - return {$addToSet: {members: memberId}}; + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + { $addToSet: { members: memberId }} + ); + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + return board.addMember(memberId); + } else { + return Cards.update( + { _id: this._id }, + { $addToSet: { members: memberId}} + ); + } }, unassignMember(memberId) { - return {$pull: {members: memberId}}; + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + { $pull: { members: memberId }} + ); + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + return board.removeMember(memberId); + } else { + return Cards.update( + { _id: this._id }, + { $pull: { members: memberId}} + ); + } }, toggleMember(memberId) { - if (this.members && this.members.indexOf(memberId) > -1) { + if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) { return this.unassignMember(memberId); } else { return this.assignMember(memberId); } }, - setCover(coverId) { - return {$set: {coverId}}; - }, - - unsetCover() { - return {$unset: {coverId: ''}}; + getReceived() { + if (this.isLinkedCard()) { + const card = Cards.findOne({_id: this.linkedId}); + return card.receivedAt; + } else { + return this.receivedAt; + } }, setReceived(receivedAt) { - return {$set: {receivedAt}}; + if (this.isLinkedCard()) { + return Cards.update( + {_id: this.linkedId}, + {$set: {receivedAt}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {receivedAt}} + ); + } }, - unsetReceived() { - return {$unset: {receivedAt: ''}}; + getStart() { + if (this.isLinkedCard()) { + const card = Cards.findOne({_id: this.linkedId}); + return card.startAt; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + return board.startAt; + } else { + return this.startAt; + } }, setStart(startAt) { - return {$set: {startAt}}; + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {startAt}} + ); + } else if (this.isLinkedBoard()) { + return Boards.update( + {_id: this.linkedId}, + {$set: {startAt}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {startAt}} + ); + } }, - unsetStart() { - return {$unset: {startAt: ''}}; + getDue() { + if (this.isLinkedCard()) { + const card = Cards.findOne({_id: this.linkedId}); + return card.dueAt; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + return board.dueAt; + } else { + return this.dueAt; + } }, setDue(dueAt) { - return {$set: {dueAt}}; + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {dueAt}} + ); + } else if (this.isLinkedBoard()) { + return Boards.update( + {_id: this.linkedId}, + {$set: {dueAt}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {dueAt}} + ); + } }, - unsetDue() { - return {$unset: {dueAt: ''}}; + getEnd() { + if (this.isLinkedCard()) { + const card = Cards.findOne({_id: this.linkedId}); + return card.endAt; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({_id: this.linkedId}); + return board.endAt; + } else { + return this.endAt; + } }, setEnd(endAt) { - return {$set: {endAt}}; + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {endAt}} + ); + } else if (this.isLinkedBoard()) { + return Boards.update( + {_id: this.linkedId}, + {$set: {endAt}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {endAt}} + ); + } }, - unsetEnd() { - return {$unset: {endAt: ''}}; + getIsOvertime() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + return card.isOvertime; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId}); + return board.isOvertime; + } else { + return this.isOvertime; + } + }, + + setIsOvertime(isOvertime) { + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {isOvertime}} + ); + } else if (this.isLinkedBoard()) { + return Boards.update( + {_id: this.linkedId}, + {$set: {isOvertime}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {isOvertime}} + ); + } }, - setOvertime(isOvertime) { - return {$set: {isOvertime}}; + getSpentTime() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + return card.spentTime; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId}); + return board.spentTime; + } else { + return this.spentTime; + } }, setSpentTime(spentTime) { - return {$set: {spentTime}}; + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {spentTime}} + ); + } else if (this.isLinkedBoard()) { + return Boards.update( + {_id: this.linkedId}, + {$set: {spentTime}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {spentTime}} + ); + } + }, + + getId() { + if (this.isLinked()) { + return this.linkedId; + } else { + return this._id; + } + }, + + getTitle() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + return card.title; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId}); + return board.title; + } else { + return this.title; + } + }, + + getBoardTitle() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + const board = Boards.findOne({ _id: card.boardId }); + return board.title; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId}); + return board.title; + } else { + const board = Boards.findOne({ _id: this.boardId }); + return board.title; + } + }, + + setTitle(title) { + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {title}} + ); + } else if (this.isLinkedBoard()) { + return Boards.update( + {_id: this.linkedId}, + {$set: {title}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {title}} + ); + } + }, + + getArchived() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + return card.archived; + } else if (this.isLinkedBoard()) { + const board = Boards.findOne({ _id: this.linkedId}); + return board.archived; + } else { + return this.archived; + } + }, + + setRequestedBy(requestedBy) { + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {requestedBy}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {requestedBy}} + ); + } }, - unsetSpentTime() { - return {$unset: {spentTime: '', isOvertime: false}}; + getRequestedBy() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + return card.requestedBy; + } else { + return this.requestedBy; + } + }, + + setAssignedBy(assignedBy) { + if (this.isLinkedCard()) { + return Cards.update( + { _id: this.linkedId }, + {$set: {assignedBy}} + ); + } else { + return Cards.update( + {_id: this._id}, + {$set: {assignedBy}} + ); + } + }, + + getAssignedBy() { + if (this.isLinkedCard()) { + const card = Cards.findOne({ _id: this.linkedId }); + return card.assignedBy; + } else { + return this.assignedBy; + } + }, +}); + +Cards.mutations({ + applyToChildren(funct) { + Cards.find({ parentId: this._id }).forEach((card) => { + funct(card); + }); + }, + + archive() { + this.applyToChildren((card) => { return card.archive(); }); + return {$set: {archived: true}}; + }, + + restore() { + this.applyToChildren((card) => { return card.restore(); }); + return {$set: {archived: false}}; + }, + + move(swimlaneId, listId, sortIndex) { + const list = Lists.findOne(listId); + const mutatedFields = { + swimlaneId, + listId, + boardId: list.boardId, + sort: sortIndex, + }; + + return {$set: mutatedFields}; + }, + + addLabel(labelId) { + return {$addToSet: {labelIds: labelId}}; + }, + + removeLabel(labelId) { + return {$pull: {labelIds: labelId}}; + }, + + toggleLabel(labelId) { + if (this.labelIds && this.labelIds.indexOf(labelId) > -1) { + return this.removeLabel(labelId); + } else { + return this.addLabel(labelId); + } + }, + + assignCustomField(customFieldId) { + return {$addToSet: {customFields: {_id: customFieldId, value: null}}}; + }, + + unassignCustomField(customFieldId) { + return {$pull: {customFields: {_id: customFieldId}}}; + }, + + toggleCustomField(customFieldId) { + if (this.customFields && this.customFieldIndex(customFieldId) > -1) { + return this.unassignCustomField(customFieldId); + } else { + return this.assignCustomField(customFieldId); + } + }, + + setCustomField(customFieldId, value) { + // todo + const index = this.customFieldIndex(customFieldId); + if (index > -1) { + const update = {$set: {}}; + update.$set[`customFields.${index}.value`] = value; + return update; + } + // TODO + // Ignatz 18.05.2018: Return null to silence ESLint. No Idea if that is correct + return null; + }, + + setCover(coverId) { + return {$set: {coverId}}; + }, + + unsetCover() { + return {$unset: {coverId: ''}}; + }, + + setParentId(parentId) { + return {$set: {parentId}}; }, }); @@ -413,6 +1000,9 @@ function cardRemover(userId, doc) { Checklists.remove({ cardId: doc._id, }); + Subtasks.remove({ + cardId: doc._id, + }); CardComments.remove({ cardId: doc._id, }); @@ -489,6 +1079,7 @@ if (Meteor.isServer) { 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, @@ -498,7 +1089,7 @@ if (Meteor.isServer) { userId: req.body.authorId, swimlaneId: req.body.swimlaneId, sort: 0, - members: [req.body.authorId], + members, }); JsonRoutes.sendResult(res, { code: 200, @@ -542,6 +1133,11 @@ if (Meteor.isServer) { Cards.direct.update({_id: paramCardId, listId: paramListId, boardId: paramBoardId, archived: false}, {$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, data: { diff --git a/models/checklistItems.js b/models/checklistItems.js index 3c01d476..e075eda2 100644 --- a/models/checklistItems.js +++ b/models/checklistItems.js @@ -93,3 +93,53 @@ if (Meteor.isServer) { 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 9946f98e..c58453ef 100644 --- a/models/checklists.js +++ b/models/checklists.js @@ -34,9 +34,9 @@ Checklists.helpers({ return ChecklistItems.find({ checklistId: this._id }).count(); }, items() { - return ChecklistItems.find(Filter.mongoSelector({ + return ChecklistItems.find({ checklistId: this._id, - }), { sort: ['sort'] }); + }, { sort: ['sort'] }); }, finishedCount() { return ChecklistItems.find({ @@ -106,94 +106,90 @@ if (Meteor.isServer) { if (Meteor.isServer) { JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { - try { - Authentication.checkUserId( req.userId); - const paramCardId = req.params.cardId; + 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.find({ cardId: paramCardId }).map(function (doc) { - return { - _id: doc._id, - title: doc.title, - }; - }), + data: checklists, }); - } - catch (error) { + } else { JsonRoutes.sendResult(res, { - code: 200, - data: error, + code: 500, }); } }); JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { - try { - Authentication.checkUserId( req.userId); - const paramChecklistId = req.params.checklistId; - const paramCardId = req.params.cardId; + 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: Checklists.findOne({ _id: paramChecklistId, cardId: paramCardId }), + data: checklist, }); - } - catch (error) { + } else { JsonRoutes.sendResult(res, { - code: 200, - data: error, + code: 500, }); } }); JsonRoutes.add('POST', '/api/boards/:boardId/cards/:cardId/checklists', function (req, res) { - try { - 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); - + 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, + }); + }); JsonRoutes.sendResult(res, { code: 200, data: { _id: id, }, }); - } - catch (error) { + } else { JsonRoutes.sendResult(res, { - code: 200, - data: error, + code: 400, }); } }); JsonRoutes.add('DELETE', '/api/boards/:boardId/cards/:cardId/checklists/:checklistId', function (req, res) { - try { - Authentication.checkUserId( req.userId); - const paramCommentId = req.params.commentId; - const paramCardId = req.params.cardId; - Checklists.remove({ _id: paramCommentId, cardId: paramCardId }); - JsonRoutes.sendResult(res, { - code: 200, - data: { - _id: paramCardId, - }, - }); - } - catch (error) { - JsonRoutes.sendResult(res, { - code: 200, - data: error, - }); - } + Authentication.checkUserId( req.userId); + const paramChecklistId = req.params.checklistId; + Checklists.remove({ _id: paramChecklistId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramChecklistId, + }, + }); }); } diff --git a/models/customFields.js b/models/customFields.js new file mode 100644 index 00000000..6c5fe7c4 --- /dev/null +++ b/models/customFields.js @@ -0,0 +1,132 @@ +CustomFields = new Mongo.Collection('customFields'); + +CustomFields.attachSchema(new SimpleSchema({ + boardId: { + type: String, + }, + name: { + type: String, + }, + type: { + type: String, + allowedValues: ['text', 'number', 'date', 'dropdown'], + }, + settings: { + type: Object, + }, + 'settings.dropdownItems': { + type: [Object], + optional: true, + }, + 'settings.dropdownItems.$': { + type: new SimpleSchema({ + _id: { + type: String, + }, + name: { + type: String, + }, + }), + }, + showOnCard: { + type: Boolean, + }, +})); + +CustomFields.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)); + }, + fetch: ['userId', 'boardId'], +}); + +// not sure if we need this? +//CustomFields.hookOptions.after.update = { fetchPrevious: false }; + +function customFieldCreation(userId, doc){ + Activities.insert({ + userId, + activityType: 'createCustomField', + boardId: doc.boardId, + customFieldId: doc._id, + }); +} + +if (Meteor.isServer) { + /*Meteor.startup(() => { + CustomFields._collection._ensureIndex({ boardId: 1}); + });*/ + + CustomFields.after.insert((userId, doc) => { + customFieldCreation(userId, doc); + }); + + CustomFields.after.remove((userId, doc) => { + Activities.remove({ + customFieldId: doc._id, + }); + }); +} + +//CUSTOM FIELD REST API +if (Meteor.isServer) { + 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({ boardId: 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, boardId: paramBoardId }), + }); + }); + + 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, + type: req.body.type, + settings: req.body.settings, + showOnCard: req.body.showOnCard, + boardId: paramBoardId, + }); + + const customField = CustomFields.findOne({_id: id, boardId: paramBoardId }); + customFieldCreation(req.body.authorId, customField); + + 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, boardId: paramBoardId }); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + }); +} diff --git a/models/export.js b/models/export.js index c6632198..ed4c52d9 100644 --- a/models/export.js +++ b/models/export.js @@ -45,6 +45,7 @@ class Exporter { build() { const byBoard = { boardId: this._boardId }; + const byBoardNoLinked = { boardId: this._boardId, linkedId: null }; // we do not want to retrieve boardId in related elements const noBoardId = { fields: { boardId: 0 } }; const result = { @@ -52,14 +53,19 @@ 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.cards = Cards.find(byBoardNoLinked, 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.subtaskItems = []; result.cards.forEach((card) => { result.checklists.push(...Checklists.find({ cardId: card._id }).fetch()); + result.checklistItems.push(...ChecklistItems.find({ cardId: card._id }).fetch()); + result.subtaskItems.push(...Cards.find({ parentid: 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/settings.js b/models/settings.js index 34f693d9..3b9b4eae 100644 --- a/models/settings.js +++ b/models/settings.js @@ -96,6 +96,14 @@ if (Meteor.isServer) { return (min + Math.round(rand * range)); } + function getEnvVar(name){ + const value = process.env[name]; + if (value){ + return value; + } + throw new Meteor.Error(['var-not-exist', `The environment variable ${name} does not exist`]); + } + function sendInvitationEmail (_id){ const icode = InvitationCodes.findOne(_id); const author = Users.findOne(Meteor.userId()); @@ -124,20 +132,33 @@ if (Meteor.isServer) { sendInvitation(emails, boards) { check(emails, [String]); check(boards, [String]); + const user = Users.findOne(Meteor.userId()); if(!user.isAdmin){ throw new Meteor.Error('not-allowed'); } emails.forEach((email) => { if (email && SimpleSchema.RegEx.Email.test(email)) { - 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); - } - }); + // 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.`); + } + // 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); + } + }); + } } }); }, @@ -167,5 +188,14 @@ if (Meteor.isServer) { email: user.emails[0].address, }; }, + + getMatomoConf(){ + return { + address: getEnvVar('MATOMO_ADDRESS'), + siteId: getEnvVar('MATOMO_SITE_ID'), + doNotTrack: process.env.MATOMO_DO_NOT_TRACK || false, + withUserName: process.env.MATOMO_WITH_USERNAME || false, + }; + }, }); } diff --git a/models/trelloCreator.js b/models/trelloCreator.js index 89e48a16..30f0bc2b 100644 --- a/models/trelloCreator.js +++ b/models/trelloCreator.js @@ -379,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()}}); @@ -410,6 +411,7 @@ export class TrelloCreator { // we require. createdAt: this._now(), title: 'Default', + sort: 1, }; const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate); Swimlanes.direct.update(swimlaneId, {$set: {'updatedAt': this._now()}}); @@ -429,17 +431,20 @@ export class TrelloCreator { 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 = []; + // Now add the items to the checklistItems + let counter = 0; checklist.checkItems.forEach((item) => { - itemsToCreate.push({ - _id: checklistId + itemsToCreate.length, + counter++; + const checklistItemTocreate = { + _id: checklistId + counter, title: item.name, - isFinished: item.state === 'complete', + 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}}); } }); } diff --git a/models/users.js b/models/users.js index a04021c1..6e83337e 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,15 @@ Users.attachSchema(new SimpleSchema({ type: String, optional: true, }, + 'profile.boardView': { + type: String, + optional: true, + allowedValues: [ + 'board-view-lists', + 'board-view-swimlanes', + 'board-view-cal', + ], + }, services: { type: Object, optional: true, @@ -329,6 +340,14 @@ Users.mutations({ setShowCardsCountAt(limit) { return {$set: {'profile.showCardsCountAt': limit}}; }, + + setBoardView(view) { + return { + $set : { + 'profile.boardView': view, + }, + }; + }, }); Meteor.methods({ @@ -508,9 +527,14 @@ if (Meteor.isServer) { throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist'); } else { user.profile = {icode: options.profile.invitationcode}; - } + user.profile.boardView = 'board-view-lists'; - return user; + // Deletes the invitation code after the user was created successfully. + setTimeout(Meteor.bindEnvironment(() => { + InvitationCodes.remove({'_id': invitationCode._id}); + }), 200); + return user; + } }); } @@ -579,10 +603,11 @@ if (Meteor.isServer) { Swimlanes.insert({ title: TAPi18n.__('welcome-swimlane'), boardId, + sort: 1, }, fakeUser); - ['welcome-list1', 'welcome-list2'].forEach((title) => { - Lists.insert({title: TAPi18n.__(title), boardId}, fakeUser); + ['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => { + Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser); }); }); }); @@ -624,9 +649,20 @@ if (Meteor.isServer) { }); } - // USERS REST API if (Meteor.isServer) { + // Middleware which checks that API is enabled. + JsonRoutes.Middleware.use(function (req, res, next) { + const api = req.url.search('api'); + if (api === 1 && process.env.WITH_API === 'true' || api === -1){ + return next(); + } + else { + res.writeHead(301, {Location: '/'}); + return res.end(); + } + }); + JsonRoutes.add('GET', '/api/user', function(req, res) { try { Authentication.checkLoggedIn(req.userId); @@ -767,4 +803,3 @@ if (Meteor.isServer) { } }); } - diff --git a/models/wekanCreator.js b/models/wekanCreator.js index 99d1df2d..4551979b 100644 --- a/models/wekanCreator.js +++ b/models/wekanCreator.js @@ -36,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 @@ -135,10 +137,13 @@ export class WekanCreator { 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, })]); } @@ -385,7 +390,7 @@ export class WekanCreator { } createLists(wekanLists, boardId) { - wekanLists.forEach((list) => { + wekanLists.forEach((list, listIndex) => { const listToCreate = { archived: list.archived, boardId, @@ -395,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()}}); @@ -417,7 +423,7 @@ export class WekanCreator { } createSwimlanes(wekanSwimlanes, boardId) { - wekanSwimlanes.forEach((swimlane) => { + wekanSwimlanes.forEach((swimlane, swimlaneIndex) => { const swimlaneToCreate = { archived: swimlane.archived, boardId, @@ -427,6 +433,7 @@ export class WekanCreator { // 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()}}); @@ -435,6 +442,7 @@ export class WekanCreator { } createChecklists(wekanChecklists) { + const result = []; wekanChecklists.forEach((checklist, checklistIndex) => { // Create the checklist const checklistToCreate = { @@ -444,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; }); } @@ -470,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; } @@ -635,6 +651,7 @@ export class WekanCreator { 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'); } @@ -654,6 +671,7 @@ export class WekanCreator { 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; |