summaryrefslogtreecommitdiffstats
path: root/models/boards.js
diff options
context:
space:
mode:
Diffstat (limited to 'models/boards.js')
-rw-r--r--models/boards.js604
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,
+ });
+ }
+ });
}