mirror of
https://github.com/wekan/wekan.git
synced 2025-09-22 01:50:48 +02:00

Some pages use relative links such as boards link at the home page. Others use absolute url such as cards in boards' lists. This commits goal is to allow for consistent use of relative urls. Origin relative URLs also helps decoupling Wekan from the infrastructure it's deployed on. i.e if it's being served, it should work.
1975 lines
47 KiB
JavaScript
1975 lines
47 KiB
JavaScript
const escapeForRegex = require('escape-string-regexp');
|
|
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,
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
// In some cases (Chinese and Japanese for instance) the `getSlug` function
|
|
// return an empty string. This is causes bugs in our application so we set
|
|
// a default slug in this case.
|
|
// Improvment would be to change client URL after slug is changed
|
|
const title = this.field('title');
|
|
if (title.isSet && !this.isSet) {
|
|
let slug = 'board';
|
|
slug = getSlug(title.value) || slug;
|
|
return slug;
|
|
}
|
|
},
|
|
},
|
|
archived: {
|
|
/**
|
|
* Is the board archived?
|
|
*/
|
|
type: Boolean,
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert && !this.isSet) {
|
|
return false;
|
|
}
|
|
},
|
|
},
|
|
archivedAt: {
|
|
/**
|
|
* Latest archiving time of the board
|
|
*/
|
|
type: Date,
|
|
optional: true,
|
|
},
|
|
createdAt: {
|
|
/**
|
|
* Creation time of the board
|
|
*/
|
|
type: Date,
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert) {
|
|
return new Date();
|
|
} else if (this.isUpsert) {
|
|
return { $setOnInsert: new Date() };
|
|
} else {
|
|
this.unset();
|
|
}
|
|
},
|
|
},
|
|
// XXX Inconsistent field naming
|
|
modifiedAt: {
|
|
/**
|
|
* Last modification time of the board
|
|
*/
|
|
type: Date,
|
|
optional: true,
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert || this.isUpsert || this.isUpdate) {
|
|
return new Date();
|
|
} else {
|
|
this.unset();
|
|
}
|
|
},
|
|
},
|
|
// De-normalized number of users that have starred this board
|
|
stars: {
|
|
/**
|
|
* How many stars the board has
|
|
*/
|
|
type: Number,
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert) {
|
|
return 0;
|
|
}
|
|
},
|
|
},
|
|
// De-normalized label system
|
|
labels: {
|
|
/**
|
|
* List of labels attached to a board
|
|
*/
|
|
type: [Object],
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert && !this.isSet) {
|
|
const colors = Boards.simpleSchema()._schema['labels.$.color']
|
|
.allowedValues;
|
|
const defaultLabelsColors = _.clone(colors).splice(0, 6);
|
|
return defaultLabelsColors.map(color => ({
|
|
color,
|
|
_id: Random.id(6),
|
|
name: '',
|
|
}));
|
|
}
|
|
},
|
|
},
|
|
'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.
|
|
// XXX Actually if we create a new label, the `_id` is set on the client
|
|
// without being overwritten by the server, could it be a problem?
|
|
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],
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert && !this.isSet) {
|
|
return [
|
|
{
|
|
userId: this.userId,
|
|
isAdmin: true,
|
|
isActive: true,
|
|
isNoComments: false,
|
|
isCommentOnly: false,
|
|
isWorker: 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,
|
|
},
|
|
'members.$.isWorker': {
|
|
/**
|
|
* Is the member only allowed to move card, assign himself to card and comment
|
|
*/
|
|
type: Boolean,
|
|
optional: true,
|
|
},
|
|
permission: {
|
|
/**
|
|
* visibility of the board
|
|
*/
|
|
type: String,
|
|
allowedValues: ['public', 'private'],
|
|
},
|
|
color: {
|
|
/**
|
|
* The color of the board.
|
|
*/
|
|
type: String,
|
|
allowedValues: [
|
|
'belize',
|
|
'nephritis',
|
|
'pomegranate',
|
|
'pumpkin',
|
|
'wisteria',
|
|
'moderatepink',
|
|
'strongcyan',
|
|
'limegreen',
|
|
'midnight',
|
|
'dark',
|
|
'relax',
|
|
'corteza',
|
|
'clearblue',
|
|
'natural',
|
|
'modern',
|
|
'moderndark',
|
|
],
|
|
// eslint-disable-next-line consistent-return
|
|
autoValue() {
|
|
if (this.isInsert && !this.isSet) {
|
|
return Boards.simpleSchema()._schema.color.allowedValues[0];
|
|
}
|
|
},
|
|
},
|
|
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,
|
|
},
|
|
|
|
dateSettingsDefaultBoardId: {
|
|
type: String,
|
|
optional: true,
|
|
defaultValue: null,
|
|
},
|
|
|
|
dateSettingsDefaultListId: {
|
|
type: String,
|
|
optional: true,
|
|
defaultValue: null,
|
|
},
|
|
|
|
allowsSubtasks: {
|
|
/**
|
|
* Does the board allows subtasks?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsAttachments: {
|
|
/**
|
|
* Does the board allows attachments?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsChecklists: {
|
|
/**
|
|
* Does the board allows checklists?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsComments: {
|
|
/**
|
|
* Does the board allows comments?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsDescriptionTitle: {
|
|
/**
|
|
* Does the board allows description title?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsDescriptionText: {
|
|
/**
|
|
* Does the board allows description text?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsActivities: {
|
|
/**
|
|
* Does the board allows comments?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsLabels: {
|
|
/**
|
|
* Does the board allows labels?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsAssignee: {
|
|
/**
|
|
* Does the board allows assignee?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsMembers: {
|
|
/**
|
|
* Does the board allows members?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsRequestedBy: {
|
|
/**
|
|
* Does the board allows requested by?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsAssignedBy: {
|
|
/**
|
|
* Does the board allows requested by?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsReceivedDate: {
|
|
/**
|
|
* Does the board allows received date?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsStartDate: {
|
|
/**
|
|
* Does the board allows start date?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsEndDate: {
|
|
/**
|
|
* Does the board allows end date?
|
|
*/
|
|
type: Boolean,
|
|
defaultValue: true,
|
|
},
|
|
|
|
allowsDueDate: {
|
|
/**
|
|
* Does the board allows due date?
|
|
*/
|
|
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',
|
|
},
|
|
sort: {
|
|
/**
|
|
* Sort value
|
|
*/
|
|
type: Number,
|
|
decimal: true,
|
|
defaultValue: -1,
|
|
},
|
|
}),
|
|
);
|
|
|
|
Boards.helpers({
|
|
copy() {
|
|
const oldId = this._id;
|
|
delete this._id;
|
|
delete this.slug;
|
|
this.title = this.copyTitle();
|
|
const _id = Boards.insert(this);
|
|
|
|
// Copy all swimlanes in board
|
|
Swimlanes.find({
|
|
boardId: oldId,
|
|
archived: false,
|
|
}).forEach(swimlane => {
|
|
swimlane.type = 'swimlane';
|
|
swimlane.copy(_id);
|
|
});
|
|
|
|
// copy custom field definitions
|
|
const cfMap = {};
|
|
CustomFields.find({ boardIds: oldId }).forEach(cf => {
|
|
const id = cf._id;
|
|
delete cf._id;
|
|
cf.boardIds = [_id];
|
|
cfMap[id] = CustomFields.insert(cf);
|
|
});
|
|
Cards.find({ boardId: _id }).forEach(card => {
|
|
Cards.update(card._id, {
|
|
$set: {
|
|
customFields: card.customFields.map(cf => {
|
|
cf._id = cfMap[cf._id];
|
|
return cf;
|
|
}),
|
|
},
|
|
});
|
|
});
|
|
|
|
// copy rules, actions, and triggers
|
|
const actionsMap = {};
|
|
Actions.find({ boardId: oldId }).forEach(action => {
|
|
const id = action._id;
|
|
delete action._id;
|
|
action.boardId = _id;
|
|
actionsMap[id] = Actions.insert(action);
|
|
});
|
|
const triggersMap = {};
|
|
Triggers.find({ boardId: oldId }).forEach(trigger => {
|
|
const id = trigger._id;
|
|
delete trigger._id;
|
|
trigger.boardId = _id;
|
|
triggersMap[id] = Triggers.insert(trigger);
|
|
});
|
|
Rules.find({ boardId: oldId }).forEach(rule => {
|
|
delete rule._id;
|
|
rule.boardId = _id;
|
|
rule.actionId = actionsMap[rule.actionId];
|
|
rule.triggerId = triggersMap[rule.triggerId];
|
|
Rules.insert(rule);
|
|
});
|
|
},
|
|
/**
|
|
* Return a unique title based on the current title
|
|
*
|
|
* @returns {string|null}
|
|
*/
|
|
copyTitle() {
|
|
return Boards.uniqueTitle(this.title);
|
|
},
|
|
|
|
/**
|
|
* Is supplied user authorized to view this board?
|
|
*/
|
|
isVisibleBy(user) {
|
|
if (this.isPublic()) {
|
|
// public boards are visible to everyone
|
|
return true;
|
|
} else {
|
|
// otherwise you have to be logged-in and active member
|
|
return user && this.isActiveMember(user._id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Is the user one of the active members of the board?
|
|
*
|
|
* @param userId
|
|
* @returns {boolean} the member that matches, or undefined/false
|
|
*/
|
|
isActiveMember(userId) {
|
|
if (userId) {
|
|
return this.members.find(
|
|
member => member.userId === userId && member.isActive,
|
|
);
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
isPublic() {
|
|
return this.permission === 'public';
|
|
},
|
|
|
|
cards() {
|
|
return Cards.find(
|
|
{ boardId: this._id, archived: false },
|
|
{ sort: { title: 1 } },
|
|
);
|
|
},
|
|
|
|
lists() {
|
|
//currentUser = Meteor.user();
|
|
//if (currentUser) {
|
|
// enabled = Meteor.user().hasSortBy();
|
|
//}
|
|
//return enabled ? this.newestLists() : this.draggableLists();
|
|
return this.draggableLists();
|
|
},
|
|
|
|
newestLists() {
|
|
// sorted lists from newest to the oldest, by its creation date or its cards' last modification date
|
|
const value = Meteor.user()._getListSortBy();
|
|
const sortKey = { starred: -1, [value[0]]: value[1] }; // [["starred",-1],value];
|
|
return Lists.find(
|
|
{
|
|
boardId: this._id,
|
|
archived: false,
|
|
},
|
|
{ sort: sortKey },
|
|
);
|
|
},
|
|
draggableLists() {
|
|
return Lists.find({ boardId: this._id }, { 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;
|
|
},
|
|
|
|
hasSpentTimeCards() {
|
|
const card = Cards.findOne({
|
|
spentTime: { $gt: 0 },
|
|
boardId: this._id,
|
|
archived: false,
|
|
});
|
|
return card !== undefined;
|
|
},
|
|
|
|
activities() {
|
|
return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 } });
|
|
},
|
|
|
|
activeMembers() {
|
|
return _.where(this.members, { isActive: true });
|
|
},
|
|
|
|
activeAdmins() {
|
|
return _.where(this.members, { isActive: true, isAdmin: true });
|
|
},
|
|
|
|
memberUsers() {
|
|
return Users.find({ _id: { $in: _.pluck(this.members, 'userId') } });
|
|
},
|
|
|
|
getLabel(name, color) {
|
|
return _.findWhere(this.labels, { name, color });
|
|
},
|
|
|
|
getLabelById(labelId) {
|
|
return _.findWhere(this.labels, { _id: labelId });
|
|
},
|
|
|
|
labelIndex(labelId) {
|
|
return _.pluck(this.labels, '_id').indexOf(labelId);
|
|
},
|
|
|
|
memberIndex(memberId) {
|
|
return _.pluck(this.members, 'userId').indexOf(memberId);
|
|
},
|
|
|
|
hasMember(memberId) {
|
|
return !!_.findWhere(this.members, { userId: memberId, isActive: true });
|
|
},
|
|
|
|
hasAdmin(memberId) {
|
|
return !!_.findWhere(this.members, {
|
|
userId: memberId,
|
|
isActive: true,
|
|
isAdmin: true,
|
|
});
|
|
},
|
|
|
|
hasNoComments(memberId) {
|
|
return !!_.findWhere(this.members, {
|
|
userId: memberId,
|
|
isActive: true,
|
|
isAdmin: false,
|
|
isNoComments: true,
|
|
isWorker: false,
|
|
});
|
|
},
|
|
|
|
hasCommentOnly(memberId) {
|
|
return !!_.findWhere(this.members, {
|
|
userId: memberId,
|
|
isActive: true,
|
|
isAdmin: false,
|
|
isCommentOnly: true,
|
|
isWorker: false,
|
|
});
|
|
},
|
|
|
|
hasWorker(memberId) {
|
|
return !!_.findWhere(this.members, {
|
|
userId: memberId,
|
|
isActive: true,
|
|
isAdmin: false,
|
|
isCommentOnly: false,
|
|
isWorker: true,
|
|
});
|
|
},
|
|
|
|
absoluteUrl() {
|
|
return FlowRouter.url('board', { id: this._id, slug: this.slug });
|
|
},
|
|
originRelativeUrl() {
|
|
return FlowRouter.path('board', { id: this._id, slug: this.slug });
|
|
},
|
|
|
|
colorClass() {
|
|
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) {
|
|
const _id = Random.id(6);
|
|
Boards.direct.update(this._id, { $push: { labels: { _id, name, color } } });
|
|
return _id;
|
|
},
|
|
|
|
searchBoards(term) {
|
|
check(term, Match.OneOf(String, null, undefined));
|
|
|
|
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.$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 },
|
|
{ customFields: { $elemMatch: { value: 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());
|
|
},
|
|
|
|
//Date Settings option such as received date, start date and so on.
|
|
getDefaultDateSettingsBoardId() {
|
|
if (
|
|
this.dateSettingsDefaultBoardId === null ||
|
|
this.dateSettingsDefaultBoardId === undefined
|
|
) {
|
|
this.dateSettingsDefaultBoardId = Boards.insert({
|
|
title: `^${this.title}^`,
|
|
permission: this.permission,
|
|
members: this.members,
|
|
color: this.color,
|
|
description: TAPi18n.__('default-dates-board', {
|
|
board: this.title,
|
|
}),
|
|
});
|
|
|
|
Swimlanes.insert({
|
|
title: TAPi18n.__('default'),
|
|
boardId: this.dateSettingsDefaultBoardId,
|
|
});
|
|
Boards.update(this._id, {
|
|
$set: {
|
|
dateSettingsDefaultBoardId: this.dateSettingsDefaultBoardId,
|
|
},
|
|
});
|
|
}
|
|
return this.dateSettingsDefaultBoardId;
|
|
},
|
|
|
|
getDefaultDateSettingsBoard() {
|
|
return Boards.findOne(this.getDefaultDateSettingsBoardId());
|
|
},
|
|
|
|
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());
|
|
},
|
|
|
|
getDefaultDateSettingsListId() {
|
|
if (
|
|
this.dateSettingsDefaultListId === null ||
|
|
this.dateSettingsDefaultListId === undefined
|
|
) {
|
|
this.dateSettingsDefaultListId = Lists.insert({
|
|
title: TAPi18n.__('queue'),
|
|
boardId: this._id,
|
|
});
|
|
this.setDateSettingsDefaultListId(this.dateSettingsDefaultListId);
|
|
}
|
|
return this.dateSettingsDefaultListId;
|
|
},
|
|
|
|
getDefaultDateSettingsList() {
|
|
return Lists.findOne(this.getDefaultDateSettingsListId());
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
cardsDueInBetween(start, end) {
|
|
return Cards.find({
|
|
boardId: this._id,
|
|
dueAt: { $gte: start, $lte: end },
|
|
});
|
|
},
|
|
|
|
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, archivedAt: new Date() } };
|
|
},
|
|
|
|
restore() {
|
|
return { $set: { archived: false } };
|
|
},
|
|
|
|
rename(title) {
|
|
return { $set: { title } };
|
|
},
|
|
|
|
setDescription(description) {
|
|
return { $set: { description } };
|
|
},
|
|
|
|
setColor(color) {
|
|
return { $set: { color } };
|
|
},
|
|
|
|
setVisibility(visibility) {
|
|
return { $set: { permission: visibility } };
|
|
},
|
|
|
|
addLabel(name, color) {
|
|
// If label with the same name and color already exists we don't want to
|
|
// create another one because they would be indistinguishable in the UI
|
|
// (they would still have different `_id` but that is not exposed to the
|
|
// user).
|
|
if (!this.getLabel(name, color)) {
|
|
const _id = Random.id(6);
|
|
return { $push: { labels: { _id, name, color } } };
|
|
}
|
|
return {};
|
|
},
|
|
|
|
editLabel(labelId, name, color) {
|
|
if (!this.getLabel(name, color)) {
|
|
const labelIndex = this.labelIndex(labelId);
|
|
return {
|
|
$set: {
|
|
[`labels.${labelIndex}.name`]: name,
|
|
[`labels.${labelIndex}.color`]: color,
|
|
},
|
|
};
|
|
}
|
|
return {};
|
|
},
|
|
|
|
removeLabel(labelId) {
|
|
return { $pull: { labels: { _id: labelId } } };
|
|
},
|
|
|
|
changeOwnership(fromId, toId) {
|
|
const memberIndex = this.memberIndex(fromId);
|
|
return {
|
|
$set: {
|
|
[`members.${memberIndex}.userId`]: toId,
|
|
},
|
|
};
|
|
},
|
|
|
|
addMember(memberId) {
|
|
const memberIndex = this.memberIndex(memberId);
|
|
if (memberIndex >= 0) {
|
|
return {
|
|
$set: {
|
|
[`members.${memberIndex}.isActive`]: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
$push: {
|
|
members: {
|
|
userId: memberId,
|
|
isAdmin: false,
|
|
isActive: true,
|
|
isNoComments: false,
|
|
isCommentOnly: false,
|
|
isWorker: false,
|
|
},
|
|
},
|
|
};
|
|
},
|
|
|
|
removeMember(memberId) {
|
|
const memberIndex = this.memberIndex(memberId);
|
|
|
|
// we do not allow the only one admin to be removed
|
|
const allowRemove =
|
|
!this.members[memberIndex].isAdmin || this.activeAdmins().length > 1;
|
|
if (!allowRemove) {
|
|
return {
|
|
$set: {
|
|
[`members.${memberIndex}.isActive`]: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
$set: {
|
|
[`members.${memberIndex}.isActive`]: false,
|
|
[`members.${memberIndex}.isAdmin`]: false,
|
|
},
|
|
};
|
|
},
|
|
|
|
setMemberPermission(
|
|
memberId,
|
|
isAdmin,
|
|
isNoComments,
|
|
isCommentOnly,
|
|
isWorker,
|
|
currentUserId = Meteor.userId(),
|
|
) {
|
|
const memberIndex = this.memberIndex(memberId);
|
|
// do not allow change permission of self
|
|
if (memberId === currentUserId) {
|
|
isAdmin = this.members[memberIndex].isAdmin;
|
|
}
|
|
|
|
return {
|
|
$set: {
|
|
[`members.${memberIndex}.isAdmin`]: isAdmin,
|
|
[`members.${memberIndex}.isNoComments`]: isNoComments,
|
|
[`members.${memberIndex}.isCommentOnly`]: isCommentOnly,
|
|
[`members.${memberIndex}.isWorker`]: isWorker,
|
|
},
|
|
};
|
|
},
|
|
|
|
setAllowsSubtasks(allowsSubtasks) {
|
|
return { $set: { allowsSubtasks } };
|
|
},
|
|
|
|
setAllowsMembers(allowsMembers) {
|
|
return { $set: { allowsMembers } };
|
|
},
|
|
|
|
setAllowsChecklists(allowsChecklists) {
|
|
return { $set: { allowsChecklists } };
|
|
},
|
|
|
|
setAllowsAssignee(allowsAssignee) {
|
|
return { $set: { allowsAssignee } };
|
|
},
|
|
|
|
setAllowsAssignedBy(allowsAssignedBy) {
|
|
return { $set: { allowsAssignedBy } };
|
|
},
|
|
|
|
setAllowsRequestedBy(allowsRequestedBy) {
|
|
return { $set: { allowsRequestedBy } };
|
|
},
|
|
|
|
setAllowsAttachments(allowsAttachments) {
|
|
return { $set: { allowsAttachments } };
|
|
},
|
|
|
|
setAllowsLabels(allowsLabels) {
|
|
return { $set: { allowsLabels } };
|
|
},
|
|
|
|
setAllowsComments(allowsComments) {
|
|
return { $set: { allowsComments } };
|
|
},
|
|
|
|
setAllowsDescriptionTitle(allowsDescriptionTitle) {
|
|
return { $set: { allowsDescriptionTitle } };
|
|
},
|
|
|
|
setAllowsDescriptionText(allowsDescriptionText) {
|
|
return { $set: { allowsDescriptionText } };
|
|
},
|
|
|
|
setAllowsActivities(allowsActivities) {
|
|
return { $set: { allowsActivities } };
|
|
},
|
|
|
|
setAllowsReceivedDate(allowsReceivedDate) {
|
|
return { $set: { allowsReceivedDate } };
|
|
},
|
|
|
|
setAllowsStartDate(allowsStartDate) {
|
|
return { $set: { allowsStartDate } };
|
|
},
|
|
|
|
setAllowsEndDate(allowsEndDate) {
|
|
return { $set: { allowsEndDate } };
|
|
},
|
|
|
|
setAllowsDueDate(allowsDueDate) {
|
|
return { $set: { allowsDueDate } };
|
|
},
|
|
|
|
setSubtasksDefaultBoardId(subtasksDefaultBoardId) {
|
|
return { $set: { subtasksDefaultBoardId } };
|
|
},
|
|
|
|
setSubtasksDefaultListId(subtasksDefaultListId) {
|
|
return { $set: { subtasksDefaultListId } };
|
|
},
|
|
|
|
setPresentParentTask(presentParentTask) {
|
|
return { $set: { presentParentTask } };
|
|
},
|
|
|
|
move(sortIndex) {
|
|
return { $set: { sort: sortIndex } };
|
|
},
|
|
});
|
|
|
|
function boardRemover(userId, doc) {
|
|
[Cards, Lists, Swimlanes, Integrations, Rules, Activities].forEach(
|
|
element => {
|
|
element.remove({ boardId: doc._id });
|
|
},
|
|
);
|
|
}
|
|
|
|
Boards.uniqueTitle = title => {
|
|
const m = title.match(
|
|
new RegExp('^(?<title>.*?)\\s*(\\[(?<num>\\d+)]\\s*$|\\s*$)'),
|
|
);
|
|
const base = escapeForRegex(m.groups.title);
|
|
let num = 0;
|
|
Boards.find({ title: new RegExp(`^${base}\\s*\\[\\d+]\\s*$`) }).forEach(
|
|
board => {
|
|
const m = board.title.match(
|
|
new RegExp('^(?<title>.*?)\\s*\\[(?<num>\\d+)]\\s*$'),
|
|
);
|
|
if (m) {
|
|
const n = parseInt(m.groups.num, 10);
|
|
num = num < n ? n : num;
|
|
}
|
|
},
|
|
);
|
|
|
|
if (num > 0) {
|
|
return `${base} [${num + 1}]`;
|
|
}
|
|
|
|
return title;
|
|
};
|
|
|
|
Boards.userSearch = (
|
|
userId,
|
|
selector = {},
|
|
projection = {},
|
|
// includeArchived = false,
|
|
) => {
|
|
// if (!includeArchived) {
|
|
// selector.archived = false;
|
|
// }
|
|
selector.$or = [{ permission: 'public' }];
|
|
|
|
if (userId) {
|
|
selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } });
|
|
}
|
|
return Boards.find(selector, projection);
|
|
};
|
|
|
|
Boards.userBoards = (userId, archived = false, selector = {}) => {
|
|
if (typeof archived === 'boolean') {
|
|
selector.archived = archived;
|
|
}
|
|
selector.$or = [{ permission: 'public' }];
|
|
|
|
if (userId) {
|
|
selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } });
|
|
}
|
|
return Boards.find(selector);
|
|
};
|
|
|
|
Boards.userBoardIds = (userId, archived = false, selector = {}) => {
|
|
return Boards.userBoards(userId, archived, selector).map(board => {
|
|
return board._id;
|
|
});
|
|
};
|
|
|
|
Boards.colorMap = () => {
|
|
const colors = {};
|
|
for (const color of Boards.labelColors()) {
|
|
colors[TAPi18n.__(`color-${color}`)] = color;
|
|
}
|
|
return colors;
|
|
};
|
|
|
|
Boards.labelColors = () => {
|
|
return _.clone(Boards.simpleSchema()._schema['labels.$.color'].allowedValues);
|
|
};
|
|
|
|
if (Meteor.isServer) {
|
|
Boards.allow({
|
|
insert: Meteor.userId,
|
|
update: allowIsBoardAdmin,
|
|
remove: allowIsBoardAdmin,
|
|
fetch: ['members'],
|
|
});
|
|
|
|
// All logged in users are allowed to reorder boards by dragging at All Boards page and Public Boards page.
|
|
Boards.allow({
|
|
update(userId, board, fieldNames) {
|
|
return _.contains(fieldNames, 'sort');
|
|
},
|
|
fetch: [],
|
|
});
|
|
|
|
// The number of users that have starred this board is managed by trusted code
|
|
// and the user is not allowed to update it
|
|
Boards.deny({
|
|
update(userId, board, fieldNames) {
|
|
return _.contains(fieldNames, 'stars');
|
|
},
|
|
fetch: [],
|
|
});
|
|
|
|
// We can't remove a member if it is the last administrator
|
|
Boards.deny({
|
|
update(userId, doc, fieldNames, modifier) {
|
|
if (!_.contains(fieldNames, 'members')) return false;
|
|
|
|
// We only care in case of a $pull operation, ie remove a member
|
|
if (!_.isObject(modifier.$pull && modifier.$pull.members)) return false;
|
|
|
|
// If there is more than one admin, it's ok to remove anyone
|
|
const nbAdmins = _.where(doc.members, { isActive: true, isAdmin: true })
|
|
.length;
|
|
if (nbAdmins > 1) return false;
|
|
|
|
// If all the previous conditions were verified, we can't remove
|
|
// a user if it's an admin
|
|
const removedMemberId = modifier.$pull.members.userId;
|
|
return Boolean(
|
|
_.findWhere(doc.members, {
|
|
userId: removedMemberId,
|
|
isAdmin: true,
|
|
}),
|
|
);
|
|
},
|
|
fetch: ['members'],
|
|
});
|
|
|
|
Meteor.methods({
|
|
quitBoard(boardId) {
|
|
check(boardId, String);
|
|
const board = Boards.findOne(boardId);
|
|
if (board) {
|
|
const userId = Meteor.userId();
|
|
const index = board.memberIndex(userId);
|
|
if (index >= 0) {
|
|
board.removeMember(userId);
|
|
return true;
|
|
} else throw new Meteor.Error('error-board-notAMember');
|
|
} else throw new Meteor.Error('error-board-doesNotExist');
|
|
},
|
|
acceptInvite(boardId) {
|
|
check(boardId, String);
|
|
const board = Boards.findOne(boardId);
|
|
if (!board) {
|
|
throw new Meteor.Error('error-board-doesNotExist');
|
|
}
|
|
|
|
Meteor.users.update(Meteor.userId(), {
|
|
$pull: {
|
|
'profile.invitedBoards': boardId,
|
|
},
|
|
});
|
|
},
|
|
myLabelNames() {
|
|
let names = [];
|
|
Boards.userBoards(Meteor.userId(), false, { type: 'board' }).forEach(
|
|
board => {
|
|
names = names.concat(
|
|
board.labels
|
|
.filter(label => !!label.name)
|
|
.map(label => {
|
|
return label.name;
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
return _.uniq(names).sort();
|
|
},
|
|
myBoardNames() {
|
|
return _.uniq(
|
|
Boards.userBoards(Meteor.userId()).map(board => {
|
|
return board.title;
|
|
}),
|
|
).sort();
|
|
},
|
|
});
|
|
|
|
Meteor.methods({
|
|
archiveBoard(boardId) {
|
|
check(boardId, String);
|
|
const board = Boards.findOne(boardId);
|
|
if (board) {
|
|
const userId = Meteor.userId();
|
|
const index = board.memberIndex(userId);
|
|
if (index >= 0) {
|
|
board.archive();
|
|
return true;
|
|
} else throw new Meteor.Error('error-board-notAMember');
|
|
} else throw new Meteor.Error('error-board-doesNotExist');
|
|
},
|
|
});
|
|
}
|
|
|
|
// Insert new board at last position in sort order.
|
|
Boards.before.insert((userId, doc) => {
|
|
const lastBoard = Boards.findOne(
|
|
{ sort: { $exists: true } },
|
|
{ sort: { sort: -1 } },
|
|
);
|
|
if (lastBoard && typeof lastBoard.sort !== 'undefined') {
|
|
doc.sort = lastBoard.sort + 1;
|
|
}
|
|
});
|
|
|
|
if (Meteor.isServer) {
|
|
// Let MongoDB ensure that a member is not included twice in the same board
|
|
Meteor.startup(() => {
|
|
Boards._collection._ensureIndex({ modifiedAt: -1 });
|
|
Boards._collection._ensureIndex(
|
|
{
|
|
_id: 1,
|
|
'members.userId': 1,
|
|
},
|
|
{ unique: true },
|
|
);
|
|
Boards._collection._ensureIndex({ 'members.userId': 1 });
|
|
});
|
|
|
|
// Genesis: the first activity of the newly created board
|
|
Boards.after.insert((userId, doc) => {
|
|
Activities.insert({
|
|
userId,
|
|
type: 'board',
|
|
activityTypeId: doc._id,
|
|
activityType: 'createBoard',
|
|
boardId: doc._id,
|
|
});
|
|
});
|
|
|
|
// If the user remove one label from a board, we cant to remove reference of
|
|
// this label in any card of this board.
|
|
Boards.after.update((userId, doc, fieldNames, modifier) => {
|
|
if (
|
|
!_.contains(fieldNames, 'labels') ||
|
|
!modifier.$pull ||
|
|
!modifier.$pull.labels ||
|
|
!modifier.$pull.labels._id
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const removedLabelId = modifier.$pull.labels._id;
|
|
Cards.update(
|
|
{ boardId: doc._id },
|
|
{
|
|
$pull: {
|
|
labelIds: removedLabelId,
|
|
},
|
|
},
|
|
{ multi: true },
|
|
);
|
|
});
|
|
|
|
const foreachRemovedMember = (doc, modifier, callback) => {
|
|
Object.keys(modifier).forEach(set => {
|
|
if (modifier[set] !== false) {
|
|
return;
|
|
}
|
|
|
|
const parts = set.split('.');
|
|
if (
|
|
parts.length === 3 &&
|
|
parts[0] === 'members' &&
|
|
parts[2] === 'isActive'
|
|
) {
|
|
callback(doc.members[parts[1]].userId);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Remove a member from all objects of the board before leaving the board
|
|
Boards.before.update((userId, doc, fieldNames, modifier) => {
|
|
if (!_.contains(fieldNames, 'members')) {
|
|
return;
|
|
}
|
|
|
|
if (modifier.$set) {
|
|
const boardId = doc._id;
|
|
foreachRemovedMember(doc, modifier.$set, memberId => {
|
|
Cards.update(
|
|
{ boardId },
|
|
{
|
|
$pull: {
|
|
members: memberId,
|
|
watchers: memberId,
|
|
},
|
|
},
|
|
{ multi: true },
|
|
);
|
|
|
|
Lists.update(
|
|
{ boardId },
|
|
{
|
|
$pull: {
|
|
watchers: memberId,
|
|
},
|
|
},
|
|
{ multi: true },
|
|
);
|
|
|
|
const board = Boards._transform(doc);
|
|
board.setWatcher(memberId, false);
|
|
|
|
// Remove board from users starred list
|
|
if (!board.isPublic()) {
|
|
Users.update(memberId, {
|
|
$pull: {
|
|
'profile.starredBoards': boardId,
|
|
},
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
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')) {
|
|
return;
|
|
}
|
|
|
|
// Say hello to the new member
|
|
if (modifier.$push && modifier.$push.members) {
|
|
const memberId = modifier.$push.members.userId;
|
|
Activities.insert({
|
|
userId,
|
|
memberId,
|
|
type: 'member',
|
|
activityType: 'addBoardMember',
|
|
boardId: doc._id,
|
|
});
|
|
}
|
|
|
|
// Say goodbye to the former member
|
|
if (modifier.$set) {
|
|
foreachRemovedMember(doc, modifier.$set, memberId => {
|
|
Activities.insert({
|
|
userId,
|
|
memberId,
|
|
type: 'member',
|
|
activityType: 'removeBoardMember',
|
|
boardId: doc._id,
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
//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);
|
|
const paramUserId = req.params.userId;
|
|
// A normal user should be able to see their own boards,
|
|
// admins can access boards of any user
|
|
Authentication.checkAdminOrCondition(
|
|
req.userId,
|
|
req.userId === paramUserId,
|
|
);
|
|
|
|
const data = Boards.find(
|
|
{
|
|
archived: false,
|
|
'members.userId': paramUserId,
|
|
},
|
|
{
|
|
sort: { sort: 1 /* boards default sorting */ },
|
|
},
|
|
).map(function(board) {
|
|
return {
|
|
_id: board._id,
|
|
title: board.title,
|
|
};
|
|
});
|
|
|
|
JsonRoutes.sendResult(res, { code: 200, data });
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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);
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: Boards.find(
|
|
{ permission: 'public' },
|
|
{
|
|
sort: { sort: 1 /* boards default sorting */ },
|
|
},
|
|
).map(function(doc) {
|
|
return {
|
|
_id: doc._id,
|
|
title: doc.title,
|
|
};
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @operation get_boards_count
|
|
* @summary Get public and private boards count
|
|
*
|
|
* @return_type {private: integer, public: integer}
|
|
*/
|
|
JsonRoutes.add('GET', '/api/boards_count', function(req, res) {
|
|
try {
|
|
Authentication.checkUserId(req.userId);
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: {
|
|
private: Boards.find({ permission: 'private' }).count(),
|
|
public: Boards.find({ permission: 'public' }).count(),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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.boardId;
|
|
Authentication.checkBoardAccess(req.userId, id);
|
|
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: Boards.findOne({ _id: id }),
|
|
});
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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`, `moderatepink`, `strongcyan`,
|
|
* `limegreen`, `midnight`, `dark`, `relax`, `corteza`:
|
|
*
|
|
* <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 {boolean} [isWorker] only move cards, assign himself to card and comment (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);
|
|
const id = Boards.insert({
|
|
title: req.body.title,
|
|
members: [
|
|
{
|
|
userId: req.body.owner,
|
|
isAdmin: req.body.isAdmin || true,
|
|
isActive: req.body.isActive || true,
|
|
isNoComments: req.body.isNoComments || false,
|
|
isCommentOnly: req.body.isCommentOnly || false,
|
|
isWorker: req.body.isWorker || false,
|
|
},
|
|
],
|
|
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,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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.boardId;
|
|
Boards.remove({ _id: id });
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: {
|
|
_id: id,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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
|
|
* @param {boolean} isWorker Worker 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, isWorker } = 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),
|
|
isTrue(isWorker),
|
|
req.userId,
|
|
);
|
|
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: query,
|
|
});
|
|
} catch (error) {
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: error,
|
|
});
|
|
}
|
|
});
|
|
|
|
//ATTACHMENTS REST API
|
|
/**
|
|
* @operation get_board_attachments
|
|
* @summary Get the list of attachments of a board
|
|
*
|
|
* @param {string} boardId the board ID
|
|
* @return_type [{attachmentId: string,
|
|
* attachmentName: string,
|
|
* attachmentType: string,
|
|
* cardId: string,
|
|
* listId: string,
|
|
* swimlaneId: string}]
|
|
*/
|
|
JsonRoutes.add('GET', '/api/boards/:boardId/attachments', function(req, res) {
|
|
const paramBoardId = req.params.boardId;
|
|
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
|
JsonRoutes.sendResult(res, {
|
|
code: 200,
|
|
data: Attachments.files
|
|
.find({ boardId: paramBoardId }, { fields: { boardId: 0 } })
|
|
.map(function(doc) {
|
|
return {
|
|
attachmentId: doc._id,
|
|
attachmentName: doc.original.name,
|
|
attachmentType: doc.original.type,
|
|
url: FlowRouter.url(doc.url()),
|
|
urlDownload: `${FlowRouter.url(doc.url())}?download=true&token=`,
|
|
cardId: doc.cardId,
|
|
listId: doc.listId,
|
|
swimlaneId: doc.swimlaneId,
|
|
};
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
export default Boards;
|