diff options
Diffstat (limited to 'models/boards.js')
-rw-r--r-- | models/boards.js | 604 |
1 files changed, 583 insertions, 21 deletions
diff --git a/models/boards.js b/models/boards.js index 3e05b499..b07d9e27 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1,10 +1,19 @@ Boards = new Mongo.Collection('boards'); +/** + * This is a Board. + */ Boards.attachSchema(new SimpleSchema({ title: { + /** + * The title of the board + */ type: String, }, slug: { + /** + * The title slugified. + */ type: String, autoValue() { // eslint-disable-line consistent-return // XXX We need to improve slug management. Only the id should be necessary @@ -24,6 +33,9 @@ Boards.attachSchema(new SimpleSchema({ }, }, archived: { + /** + * Is the board archived? + */ type: Boolean, autoValue() { // eslint-disable-line consistent-return if (this.isInsert && !this.isSet) { @@ -32,6 +44,9 @@ Boards.attachSchema(new SimpleSchema({ }, }, createdAt: { + /** + * Creation time of the board + */ type: Date, autoValue() { // eslint-disable-line consistent-return if (this.isInsert) { @@ -43,6 +58,9 @@ Boards.attachSchema(new SimpleSchema({ }, // XXX Inconsistent field naming modifiedAt: { + /** + * Last modification time of the board + */ type: Date, optional: true, autoValue() { // eslint-disable-line consistent-return @@ -55,6 +73,9 @@ Boards.attachSchema(new SimpleSchema({ }, // De-normalized number of users that have starred this board stars: { + /** + * How many stars the board has + */ type: Number, autoValue() { // eslint-disable-line consistent-return if (this.isInsert) { @@ -64,6 +85,9 @@ Boards.attachSchema(new SimpleSchema({ }, // De-normalized label system 'labels': { + /** + * List of labels attached to a board + */ type: [Object], autoValue() { // eslint-disable-line consistent-return if (this.isInsert && !this.isSet) { @@ -78,6 +102,9 @@ Boards.attachSchema(new SimpleSchema({ }, }, 'labels.$._id': { + /** + * Unique id of a label + */ // We don't specify that this field must be unique in the board because that // will cause performance penalties and is not necessary since this field is // always set on the server. @@ -86,20 +113,38 @@ Boards.attachSchema(new SimpleSchema({ type: String, }, 'labels.$.name': { + /** + * Name of a label + */ type: String, optional: true, }, 'labels.$.color': { + /** + * color of a label. + * + * Can be amongst `green`, `yellow`, `orange`, `red`, `purple`, + * `blue`, `sky`, `lime`, `pink`, `black`, + * `silver`, `peachpuff`, `crimson`, `plum`, `darkgreen`, + * `slateblue`, `magenta`, `gold`, `navy`, `gray`, + * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo` + */ type: String, 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- // documents like de-normalized meta-data (the date the member joined the // board, the number of contributions, etc.). 'members': { + /** + * List of members of a board + */ type: [Object], autoValue() { // eslint-disable-line consistent-return if (this.isInsert && !this.isSet) { @@ -107,28 +152,55 @@ Boards.attachSchema(new SimpleSchema({ userId: this.userId, isAdmin: true, isActive: true, + isNoComments: false, isCommentOnly: false, }]; } }, }, 'members.$.userId': { + /** + * The uniq ID of the member + */ type: String, }, 'members.$.isAdmin': { + /** + * Is the member an admin of the board? + */ type: Boolean, }, 'members.$.isActive': { + /** + * Is the member active? + */ type: Boolean, }, + 'members.$.isNoComments': { + /** + * Is the member not allowed to make comments + */ + type: Boolean, + optional: true, + }, 'members.$.isCommentOnly': { + /** + * Is the member only allowed to comment on the board + */ type: Boolean, + optional: true, }, permission: { + /** + * visibility of the board + */ type: String, allowedValues: ['public', 'private'], }, color: { + /** + * The color of the board. + */ type: String, allowedValues: [ 'belize', @@ -145,13 +217,118 @@ Boards.attachSchema(new SimpleSchema({ }, }, description: { + /** + * The description of the board + */ + type: String, + optional: true, + }, + subtasksDefaultBoardId: { + /** + * The default board ID assigned to subtasks. + */ + type: String, + optional: true, + defaultValue: null, + }, + subtasksDefaultListId: { + /** + * The default List ID assigned to subtasks. + */ + type: String, + optional: true, + defaultValue: null, + }, + allowsSubtasks: { + /** + * Does the board allows subtasks? + */ + type: Boolean, + defaultValue: true, + }, + presentParentTask: { + /** + * Controls how to present the parent task: + * + * - `prefix-with-full-path`: add a prefix with the full path + * - `prefix-with-parent`: add a prefisx with the parent name + * - `subtext-with-full-path`: add a subtext with the full path + * - `subtext-with-parent`: add a subtext with the parent name + * - `no-parent`: does not show the parent at all + */ 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: { + /** + * Starting date of the board. + */ + type: Date, + optional: true, + }, + dueAt: { + /** + * Due date of the board. + */ + type: Date, + optional: true, + }, + endAt: { + /** + * End date of the board. + */ + type: Date, optional: true, }, + spentTime: { + /** + * Time spent in the board. + */ + type: Number, + decimal: true, + optional: true, + }, + isOvertime: { + /** + * Is the board overtimed? + */ + type: Boolean, + defaultValue: false, + optional: true, + }, + type: { + /** + * The type of board + */ + type: String, + defaultValue: 'board', + }, })); Boards.helpers({ + copy() { + const oldId = this._id; + delete this._id; + const _id = Boards.insert(this); + + // Copy all swimlanes in board + Swimlanes.find({ + boardId: oldId, + archived: false, + }).forEach((swimlane) => { + swimlane.type = 'swimlane'; + swimlane.copy(_id); + }); + }, /** * Is supplied user authorized to view this board? */ @@ -183,14 +360,45 @@ 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 } }); }, + nullSortLists() { + return Lists.find({ + boardId: this._id, + archived: false, + sort: { $eq: null }, + }); + }, + swimlanes() { return Swimlanes.find({ boardId: this._id, archived: false }, { sort: { sort: 1 } }); }, + nextSwimlane(swimlane) { + return Swimlanes.findOne({ + boardId: this._id, + archived: false, + sort: { $gte: swimlane.sort }, + _id: { $ne: swimlane._id }, + }, { + sort: { sort: 1 }, + }); + }, + + nullSortSwimlanes() { + return Swimlanes.find({ + boardId: this._id, + archived: false, + sort: { $eq: null }, + }); + }, + hasOvertimeCards(){ const card = Cards.findOne({isOvertime: true, boardId: this._id, archived: false} ); return card !== undefined; @@ -221,6 +429,10 @@ Boards.helpers({ return _.findWhere(this.labels, { name, color }); }, + getLabelById(labelId){ + return _.findWhere(this.labels, { _id: labelId }); + }, + labelIndex(labelId) { return _.pluck(this.labels, '_id').indexOf(labelId); }, @@ -237,6 +449,10 @@ Boards.helpers({ return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: true }); }, + hasNoComments(memberId) { + return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isNoComments: true }); + }, + hasCommentOnly(memberId) { return !!_.findWhere(this.members, { userId: memberId, isActive: true, isAdmin: false, isCommentOnly: true }); }, @@ -249,6 +465,10 @@ Boards.helpers({ return `board-color-${this.color}`; }, + customFields() { + return CustomFields.find({ boardIds: {$in: [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) { @@ -257,28 +477,192 @@ Boards.helpers({ return _id; }, - searchCards(term) { + searchBoards(term) { check(term, Match.OneOf(String, null, undefined)); - let query = { boardId: this._id }; + const query = { boardId: this._id }; + query.type = 'cardType-linkedBoard'; + query.archived = false; + 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); }, + + searchSwimlanes(term) { + check(term, Match.OneOf(String, null, undefined)); + + const query = { boardId: this._id }; + if (this.isTemplatesBoard()) { + query.type = 'template-swimlane'; + query.archived = false; + } else { + query.type = {$nin: ['template-swimlane']}; + } + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + query.$or = [ + { title: regex }, + { description: regex }, + ]; + } + + return Swimlanes.find(query, projection); + }, + + searchLists(term) { + check(term, Match.OneOf(String, null, undefined)); + + const query = { boardId: this._id }; + if (this.isTemplatesBoard()) { + query.type = 'template-list'; + query.archived = false; + } else { + query.type = {$nin: ['template-list']}; + } + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + query.$or = [ + { title: regex }, + { description: regex }, + ]; + } + + return Lists.find(query, projection); + }, + + searchCards(term, excludeLinked) { + check(term, Match.OneOf(String, null, undefined)); + + const query = { boardId: this._id }; + if (excludeLinked) { + query.linkedId = null; + } + if (this.isTemplatesBoard()) { + query.type = 'template-card'; + query.archived = false; + } else { + query.type = {$nin: ['template-card']}; + } + const projection = { limit: 10, sort: { createdAt: -1 } }; + + if (term) { + const regex = new RegExp(term, 'i'); + + 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, + }); + this.setSubtasksDefaultListId(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, + }, + }, + ], + }); + }, + + isTemplateBoard() { + return this.type === 'template-board'; + }, + + isTemplatesBoard() { + return this.type === 'template-container'; + }, }); + Boards.mutations({ archive() { return { $set: { archived: true } }; @@ -358,6 +742,7 @@ Boards.mutations({ userId: memberId, isAdmin: false, isActive: true, + isNoComments: false, isCommentOnly: false, }, }, @@ -385,23 +770,46 @@ Boards.mutations({ }; }, - setMemberPermission(memberId, isAdmin, isCommentOnly) { + setMemberPermission(memberId, isAdmin, isNoComments, isCommentOnly, currentUserId = Meteor.userId()) { const memberIndex = this.memberIndex(memberId); - // do not allow change permission of self - if (memberId === Meteor.userId()) { + if (memberId === currentUserId) { isAdmin = this.members[memberIndex].isAdmin; } return { $set: { [`members.${memberIndex}.isAdmin`]: isAdmin, + [`members.${memberIndex}.isNoComments`]: isNoComments, [`members.${memberIndex}.isCommentOnly`]: isCommentOnly, }, }; }, + + setAllowsSubtasks(allowsSubtasks) { + return { $set: { allowsSubtasks } }; + }, + + setSubtasksDefaultBoardId(subtasksDefaultBoardId) { + return { $set: { subtasksDefaultBoardId } }; + }, + + setSubtasksDefaultListId(subtasksDefaultListId) { + return { $set: { subtasksDefaultListId } }; + }, + + setPresentParentTask(presentParentTask) { + return { $set: { presentParentTask } }; + }, }); +function boardRemover(userId, doc) { + [Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach((element) => { + element.remove({ boardId: doc._id }); + }); +} + + if (Meteor.isServer) { Boards.allow({ insert: Meteor.userId, @@ -565,6 +973,18 @@ if (Meteor.isServer) { } }); + Boards.before.remove((userId, doc) => { + boardRemover(userId, doc); + // Add removeBoard activity to keep it + Activities.insert({ + userId, + type: 'board', + activityTypeId: doc._id, + activityType: 'removeBoard', + boardId: doc._id, + }); + }); + // Add a new activity if we add or remove a member to the board Boards.after.update((userId, doc, fieldNames, modifier) => { if (!_.contains(fieldNames, 'members')) { @@ -600,6 +1020,14 @@ if (Meteor.isServer) { //BOARDS REST API if (Meteor.isServer) { + /** + * @operation get_boards_from_user + * @summary Get all boards attached to a user + * + * @param {string} userId the ID of the user to retrieve the data + * @return_type [{_id: string, + title: string}] + */ JsonRoutes.add('GET', '/api/users/:userId/boards', function (req, res) { try { Authentication.checkLoggedIn(req.userId); @@ -630,6 +1058,13 @@ if (Meteor.isServer) { } }); + /** + * @operation get_public_boards + * @summary Get all public boards + * + * @return_type [{_id: string, + title: string}] + */ JsonRoutes.add('GET', '/api/boards', function (req, res) { try { Authentication.checkUserId(req.userId); @@ -651,9 +1086,16 @@ if (Meteor.isServer) { } }); - JsonRoutes.add('GET', '/api/boards/:id', function (req, res) { + /** + * @operation get_board + * @summary Get the board with that particular ID + * + * @param {string} boardId the ID of the board to retrieve the data + * @return_type Boards + */ + JsonRoutes.add('GET', '/api/boards/:boardId', function (req, res) { try { - const id = req.params.id; + const id = req.params.boardId; Authentication.checkBoardAccess(req.userId, id); JsonRoutes.sendResult(res, { @@ -669,6 +1111,31 @@ if (Meteor.isServer) { } }); + /** + * @operation new_board + * @summary Create a board + * + * @description This allows to create a board. + * + * The color has to be chosen between `belize`, `nephritis`, `pomegranate`, + * `pumpkin`, `wisteria`, `midnight`: + * + * <img src="https://wekan.github.io/board-colors.png" width="40%" alt="Wekan logo" /> + * + * @param {string} title the new title of the board + * @param {string} owner "ABCDE12345" <= User ID in Wekan. + * (Not username or email) + * @param {boolean} [isAdmin] is the owner an admin of the board (default true) + * @param {boolean} [isActive] is the board active (default true) + * @param {boolean} [isNoComments] disable comments (default false) + * @param {boolean} [isCommentOnly] only enable comments (default false) + * @param {string} [permission] "private" board <== Set to "public" if you + * want public Wekan board + * @param {string} [color] the color of the board + * + * @return_type {_id: string, + defaultSwimlaneId: string} + */ JsonRoutes.add('POST', '/api/boards', function (req, res) { try { Authentication.checkUserId(req.userId); @@ -677,18 +1144,24 @@ if (Meteor.isServer) { members: [ { userId: req.body.owner, - isAdmin: true, - isActive: true, - isCommentOnly: false, + isAdmin: req.body.isAdmin || true, + isActive: req.body.isActive || true, + isNoComments: req.body.isNoComments || false, + isCommentOnly: req.body.isCommentOnly || false, }, ], - permission: 'public', - color: 'belize', + permission: req.body.permission || 'private', + color: req.body.color || 'belize', + }); + const swimlaneId = Swimlanes.insert({ + title: TAPi18n.__('default'), + boardId: id, }); JsonRoutes.sendResult(res, { code: 200, data: { _id: id, + defaultSwimlaneId: swimlaneId, }, }); } @@ -700,10 +1173,16 @@ if (Meteor.isServer) { } }); - JsonRoutes.add('DELETE', '/api/boards/:id', function (req, res) { + /** + * @operation delete_board + * @summary Delete a board + * + * @param {string} boardId the ID of the board + */ + JsonRoutes.add('DELETE', '/api/boards/:boardId', function (req, res) { try { Authentication.checkUserId(req.userId); - const id = req.params.id; + const id = req.params.boardId; Boards.remove({ _id: id }); JsonRoutes.sendResult(res, { code: 200, @@ -719,4 +1198,87 @@ if (Meteor.isServer) { }); } }); + + /** + * @operation add_board_label + * @summary Add a label to a board + * + * @description If the board doesn't have the name/color label, this function + * adds the label to the board. + * + * @param {string} boardId the board + * @param {string} color the color of the new label + * @param {string} name the name of the new label + * + * @return_type string + */ + JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function (req, res) { + Authentication.checkUserId(req.userId); + const id = req.params.boardId; + 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, + }); + } + }); + + /** + * @operation set_board_member_permission + * @tag Users + * @summary Change the permission of a member of a board + * + * @param {string} boardId the ID of the board that we are changing + * @param {string} memberId the ID of the user to change permissions + * @param {boolean} isAdmin admin capability + * @param {boolean} isNoComments NoComments capability + * @param {boolean} isCommentOnly CommentsOnly capability + */ + JsonRoutes.add('POST', '/api/boards/:boardId/members/:memberId', function (req, res) { + try { + const boardId = req.params.boardId; + const memberId = req.params.memberId; + const {isAdmin, isNoComments, isCommentOnly} = req.body; + Authentication.checkBoardAccess(req.userId, boardId); + const board = Boards.findOne({ _id: boardId }); + function isTrue(data){ + try { + return data.toLowerCase() === 'true'; + } + catch (error) { + return data; + } + } + const query = board.setMemberPermission(memberId, isTrue(isAdmin), isTrue(isNoComments), isTrue(isCommentOnly), req.userId); + + JsonRoutes.sendResult(res, { + code: 200, + data: query, + }); + } + catch (error) { + JsonRoutes.sendResult(res, { + code: 200, + data: error, + }); + } + }); } |