wekan/models/cards.js
2023-06-21 21:59:48 +02:00

4022 lines
100 KiB
JavaScript

import moment from 'moment/min/moment-with-locales';
import {
ALLOWED_COLORS,
TYPE_CARD,
TYPE_LINKED_BOARD,
TYPE_LINKED_CARD,
} from '../config/const';
import Attachments, { fileStoreStrategyFactory } from "./attachments";
import { copyFile } from './lib/fileStoreStrategy.js';
Cards = new Mongo.Collection('cards');
// XXX To improve pub/sub performances a card document should include a
// de-normalized number of comments so we don't have to publish the whole list
// of comments just to display the number of them in the board view.
Cards.attachSchema(
new SimpleSchema({
title: {
/**
* the title of the card
*/
type: String,
optional: true,
defaultValue: '',
},
archived: {
/**
* is the card archived
*/
type: Boolean,
// eslint-disable-next-line consistent-return
autoValue() {
// eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return false;
}
},
},
archivedAt: {
/**
* latest archiving date
*/
type: Date,
optional: true,
},
parentId: {
/**
* ID of the parent card
*/
type: String,
optional: true,
defaultValue: '',
},
listId: {
/**
* List ID where the card is
*/
type: String,
optional: true,
defaultValue: '',
},
swimlaneId: {
/**
* Swimlane ID where the card is
*/
type: String,
},
// The system could work without this `boardId` information (we could deduce
// the board identifier from the card), but it would make the system more
// difficult to manage and less efficient.
boardId: {
/**
* Board ID of the card
*/
type: String,
optional: true,
defaultValue: '',
},
coverId: {
/**
* Cover ID of the card
*/
type: String,
optional: true,
defaultValue: '',
},
color: {
type: String,
optional: true,
allowedValues: ALLOWED_COLORS,
},
createdAt: {
/**
* creation date
*/
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();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
customFields: {
/**
* list of custom fields
*/
type: [Object],
optional: true,
defaultValue: [],
},
'customFields.$': {
type: new SimpleSchema({
_id: {
/**
* the ID of the related custom field
*/
type: String,
optional: true,
defaultValue: '',
},
value: {
/**
* value attached to the custom field
*/
type: Match.OneOf(String, Number, Boolean, Date, [String]),
optional: true,
defaultValue: '',
},
'value.$': {
type: String,
optional: true,
},
}),
},
dateLastActivity: {
/**
* Date of last activity
*/
type: Date,
autoValue() {
return new Date();
},
},
description: {
/**
* description of the card
*/
type: String,
optional: true,
defaultValue: '',
},
requestedBy: {
/**
* who requested the card (ID of the user)
*/
type: String,
optional: true,
defaultValue: '',
},
assignedBy: {
/**
* who assigned the card (ID of the user)
*/
type: String,
optional: true,
defaultValue: '',
},
labelIds: {
/**
* list of labels ID the card has
*/
type: [String],
optional: true,
defaultValue: [],
},
members: {
/**
* list of members (user IDs)
*/
type: [String],
optional: true,
defaultValue: [],
},
assignees: {
/**
* who is assignee of the card (user ID),
* maximum one ID of assignee in array.
*/
type: [String],
optional: true,
defaultValue: [],
},
receivedAt: {
/**
* Date the card was received
*/
type: Date,
optional: true,
},
startAt: {
/**
* Date the card was started to be worked on
*/
type: Date,
optional: true,
},
dueAt: {
/**
* Date the card is due
*/
type: Date,
optional: true,
},
endAt: {
/**
* Date the card ended
*/
type: Date,
optional: true,
},
spentTime: {
/**
* How much time has been spent on this
*/
type: Number,
decimal: true,
optional: true,
defaultValue: 0,
},
isOvertime: {
/**
* is the card over time?
*/
type: Boolean,
defaultValue: false,
optional: true,
},
// XXX Should probably be called `authorId`. Is it even needed since we have
// the `members` field?
userId: {
/**
* user ID of the author of the card
*/
type: String,
// eslint-disable-next-line consistent-return
autoValue() {
// eslint-disable-line consistent-return
if (this.isInsert && !this.isSet) {
return this.userId;
}
},
},
sort: {
/**
* Sort value
*/
type: Number,
decimal: true,
defaultValue: 0,
optional: true,
},
subtaskSort: {
/**
* subtask sort value
*/
type: Number,
decimal: true,
defaultValue: -1,
optional: true,
},
type: {
/**
* type of the card
*/
type: String,
defaultValue: TYPE_CARD,
// allowedValues: [TYPE_CARD, TYPE_LINKED_CARD, TYPE_LINKED_BOARD, TYPE_TEMPLATE_CARD],
},
linkedId: {
/**
* ID of the linked card
*/
type: String,
optional: true,
defaultValue: '',
},
vote: {
/**
* vote object, see below
*/
type: Object,
optional: true,
},
'vote.question': {
type: String,
defaultValue: '',
},
'vote.positive': {
/**
* list of members (user IDs)
*/
type: [String],
optional: true,
defaultValue: [],
},
'vote.negative': {
/**
* list of members (user IDs)
*/
type: [String],
optional: true,
defaultValue: [],
},
'vote.end': {
type: Date,
optional: true,
defaultValue: null,
},
'vote.public': {
type: Boolean,
defaultValue: false,
},
'vote.allowNonBoardMembers': {
type: Boolean,
defaultValue: false,
},
poker: {
/**
* poker object, see below
*/
type: Object,
optional: true,
},
'poker.question': {
type: Boolean,
defaultValue: false,
},
'poker.one': {
/**
* poker card one
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.two': {
/**
* poker card two
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.three': {
/**
* poker card three
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.five': {
/**
* poker card five
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.eight': {
/**
* poker card eight
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.thirteen': {
/**
* poker card thirteen
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.twenty': {
/**
* poker card twenty
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.forty': {
/**
* poker card forty
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.oneHundred': {
/**
* poker card oneHundred
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.unsure': {
/**
* poker card unsure
*/
type: [String],
optional: true,
defaultValue: [],
},
'poker.end': {
type: Date,
optional: true,
defaultValue: null,
},
'poker.allowNonBoardMembers': {
type: Boolean,
defaultValue: false,
},
'poker.estimation': {
/**
* poker estimation value
*/
type: Number,
optional: true,
},
targetId_gantt: {
/**
* ID of card which is the child link in gantt view
*/
type: [String],
optional: true,
defaultValue: [],
},
linkType_gantt: {
/**
* ID of card which is the parent link in gantt view
*/
type: [Number],
decimal: false,
optional: true,
defaultValue: [],
},
linkId_gantt: {
/**
* ID of card which is the parent link in gantt view
*/
type: [String],
optional: true,
defaultValue: [],
},
cardNumber: {
/**
* A boardwise sequentially increasing number that is assigned
* to every newly created card
*/
type: Number,
decimal: true,
optional: true,
defaultValue: 0,
},
}),
);
Cards.allow({
insert(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
},
update(userId, doc, fields) {
// Allow board members or logged in users if only vote get's changed
return (
allowIsBoardMember(userId, Boards.findOne(doc.boardId)) ||
(_.isEqual(fields, ['vote', 'modifiedAt', 'dateLastActivity']) &&
!!userId)
);
},
remove(userId, doc) {
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
},
fetch: ['boardId'],
});
Cards.helpers({
// Gantt https://github.com/wekan/wekan/issues/2870#issuecomment-857171127
setGanttTargetId(sourceId, targetId, linkType, linkId){
return Cards.update({ _id: sourceId}, {
$push: {
targetId_gantt: targetId,
linkType_gantt : linkType,
linkId_gantt: linkId
}
});
},
removeGanttTargetId(sourceId, targetId, linkType, linkId){
return Cards.update({ _id: sourceId}, {
$pull: {
targetId_gantt: targetId,
linkType_gantt : linkType,
linkId_gantt: linkId
}
});
},
mapCustomFieldsToBoard(boardId) {
// Map custom fields to new board
return this.customFields.map(cf => {
const oldCf = CustomFields.findOne(cf._id);
const newCf = CustomFields.findOne({
boardIds: boardId,
name: oldCf.name,
type: oldCf.type,
});
if (newCf) {
cf._id = newCf._id;
} else if (!_.contains(oldCf.boardIds, boardId)) {
oldCf.addBoard(boardId);
}
return cf;
});
},
copy(boardId, swimlaneId, listId) {
const oldId = this._id;
const oldCard = Cards.findOne(oldId);
// we must only copy the labels and custom fields if the target board
// differs from the source board
if (this.boardId !== boardId) {
const oldBoard = Boards.findOne(this.boardId);
const oldBoardLabels = oldBoard.labels;
// Get old label names
const oldCardLabels = _.pluck(
_.filter(oldBoardLabels, label => {
return _.contains(this.labelIds, label._id);
}),
'name',
);
const newBoard = Boards.findOne(boardId);
const newBoardLabels = newBoard.labels;
const newCardLabels = _.pluck(
_.filter(newBoardLabels, label => {
return _.contains(oldCardLabels, label.name);
}),
'_id',
);
// now set the new label ids
delete this.labelIds;
this.labelIds = newCardLabels;
this.customFields = this.mapCustomFieldsToBoard(newBoard._id);
}
delete this._id;
this.boardId = boardId;
this.cardNumber = Boards.findOne(boardId).getNextCardNumber();
this.swimlaneId = swimlaneId;
this.listId = listId;
const _id = Cards.insert(this);
// Copy attachments
oldCard.attachments()
.map(att => att.get())
.forEach(att => {
copyFile(att, _id, fileStoreStrategyFactory);
});
// copy checklists
Checklists.find({ cardId: oldId }).forEach(ch => {
ch.copy(_id);
});
// copy subtasks
Cards.find({ parentId: oldId }).forEach(subtask => {
subtask.parentId = _id;
subtask._id = null;
Cards.insert(subtask);
});
// copy card comments
CardComments.find({ cardId: oldId }).forEach(cmt => {
cmt.copy(_id);
});
// restore the id, otherwise new copies will fail
this._id = oldId;
return _id;
},
link(boardId, swimlaneId, listId) {
// TODO is there a better method to create a deepcopy?
linkCard = JSON.parse(JSON.stringify(this));
// TODO is this how it is meant to be?
linkCard.linkedId = linkCard.linkedId || linkCard._id;
linkCard.boardId = boardId;
linkCard.swimlaneId = swimlaneId;
linkCard.listId = listId;
linkCard.type = 'cardType-linkedCard';
delete linkCard._id;
// TODO shall we copy the labels for a linked card?!
delete linkCard.labelIds;
return Cards.insert(linkCard);
},
list() {
return Lists.findOne(this.listId);
},
swimlane() {
return Swimlanes.findOne(this.swimlaneId);
},
board() {
return Boards.findOne(this.boardId);
},
getList() {
const list = this.list();
if (!list) {
return {
_id: this.listId,
title: 'Undefined List',
archived: false,
colorClass: '',
};
}
return list;
},
getSwimlane() {
const swimlane = this.swimlane();
if (!swimlane) {
return {
_id: this.swimlaneId,
title: 'Undefined Swimlane',
archived: false,
colorClass: '',
};
}
return swimlane;
},
getBoard() {
const board = this.board();
if (!board) {
return {
_id: this.boardId,
title: 'Undefined Board',
archived: false,
colorClass: '',
};
}
return board;
},
labels() {
const boardLabels = this.board().labels;
const cardLabels = _.filter(boardLabels, label => {
return _.contains(this.labelIds, label._id);
});
return cardLabels;
},
hasLabel(labelId) {
return _.contains(this.labelIds, labelId);
},
/** returns the sort number of a list
* @param listId a list id
* @param swimlaneId a swimlane id
* top sorting of the card at the top if true, or from the bottom if false
*/
getSort(listId, swimlaneId, top) {
if (!_.isBoolean(top)) {
top = true;
}
if (!listId) {
listId = this.listId;
}
if (!swimlaneId) {
swimlaneId = this.swimlaneId;
}
const selector = {
listId: listId,
swimlaneId: swimlaneId,
archived: false,
};
const sorting = top ? 1 : -1;
const card = Cards.findOne(selector, { sort: { sort: sorting } });
let ret = null
if (card) {
ret = card.sort;
}
return ret;
},
/** returns the sort number of a list from the card at the top
* @param listId a list id
* @param swimlaneId a swimlane id
*/
getMinSort(listId, swimlaneId) {
const ret = this.getSort(listId, swimlaneId, true);
return ret;
},
/** returns the sort number of a list from the card at the bottom
* @param listId a list id
* @param swimlaneId a swimlane id
*/
getMaxSort(listId, swimlaneId) {
const ret = this.getSort(listId, swimlaneId, false);
return ret;
},
user() {
return Users.findOne(this.userId);
},
isAssigned(memberId) {
return _.contains(this.getMembers(), memberId);
},
isAssignee(assigneeId) {
return _.contains(this.getAssignees(), assigneeId);
},
activities() {
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() {
if (this.isLinkedCard()) {
return CardComments.find(
{ cardId: this.linkedId },
{ sort: { createdAt: -1 } },
);
} else if (this.isLinkedBoard()) {
return CardComments.find(
{ boardId: this.linkedId },
{ sort: { createdAt: -1 } },
);
} else {
return CardComments.find(
{ cardId: this._id },
{ sort: { createdAt: -1 } },
);
}
},
attachments() {
let id = this._id;
if (this.isLinkedCard()) {
id = this.linkedId;
}
let ret = Attachments.find(
{ 'meta.cardId': id },
{ sort: { uploadedAt: -1 } },
).each();
return ret;
},
cover() {
if (!this.coverId) return false;
const cover = Attachments.findOne(this.coverId);
// if we return a cover before it is fully stored, we will get errors when we try to display it
// todo XXX we could return a default "upload pending" image in the meantime?
return cover && cover.link() && cover;
},
checklists() {
if (this.isLinkedCard()) {
return Checklists.find({ cardId: this.linkedId }, { sort: { sort: 1 } });
} else {
return Checklists.find({ cardId: this._id }, { sort: { sort: 1 } });
}
},
firstChecklist() {
const checklists = this.checklists().fetch();
const ret = _.first(checklists);
return ret;
},
lastChecklist() {
const checklists = this.checklists().fetch();
const ret = _.last(checklists);
return ret;
},
checklistItemCount() {
const checklists = this.checklists().fetch();
return checklists
.map(checklist => {
return checklist.itemCount();
})
.reduce((prev, next) => {
return prev + next;
}, 0);
},
checklistFinishedCount() {
const checklists = this.checklists().fetch();
return checklists
.map(checklist => {
return checklist.finishedCount();
})
.reduce((prev, next) => {
return prev + next;
}, 0);
},
checklistFinished() {
return (
this.hasChecklist() &&
this.checklistItemCount() === this.checklistFinishedCount()
);
},
hasChecklist() {
return this.checklistItemCount() !== 0;
},
subtasks() {
return Cards.find(
{
parentId: this._id,
archived: false,
},
{
sort: {
sort: 1,
},
},
);
},
subtasksFinished() {
return Cards.find({
parentId: this._id,
archived: true,
});
},
allSubtasks() {
return Cards.find({
parentId: this._id,
});
},
subtasksCount() {
const subtasks = this.subtasks();
return subtasks.count();
},
subtasksFinishedCount() {
const subtasksArchived = this.subtasksFinished();
return subtasksArchived.count();
},
allSubtasksCount() {
const allSubtasks = this.allSubtasks();
return allSubtasks.count();
},
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({
boardIds: { $in: [this.boardId] },
}).fetch();
if (!definitions) {
return {};
}
// match right definition to each field
if (!this.customFields) return [];
const ret = this.customFields.map(customField => {
const definition = definitions.find(definition => {
return definition._id === customField._id;
});
if (!definition) {
return {};
}
//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,
};
});
// at linked cards custom fields definition is not found
ret.sort(
(a, b) =>
a.definition !== undefined &&
b.definition !== undefined &&
a.definition.name !== undefined &&
b.definition.name !== undefined &&
a.definition.name.localeCompare(b.definition.name),
);
return ret;
},
colorClass() {
if (this.color) return this.color;
return '';
},
absoluteUrl() {
const board = this.board();
return FlowRouter.url('card', {
boardId: board._id,
slug: board.slug,
cardId: this._id,
});
},
originRelativeUrl() {
const board = this.board();
return FlowRouter.path('card', {
boardId: board._id,
slug: board.slug,
cardId: this._id,
});
},
canBeRestored() {
const list = Lists.findOne({
_id: this.listId,
});
if (
!list.getWipLimit('soft') &&
list.getWipLimit('enabled') &&
list.getWipLimit('value') === list.cards().count()
) {
return false;
}
return true;
},
parentCard() {
if (this.parentId === '') {
return null;
}
return Cards.findOne(this.parentId);
},
parentCardName() {
let result = '';
if (this.parentId !== '') {
const card = Cards.findOne(this.parentId);
if (card) {
result = card.title;
}
}
return result;
},
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;
},
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;
},
parentString(sep) {
return this.parentList()
.map(function(elem) {
return elem.title;
})
.join(sep);
},
isTopLevel() {
return this.parentId === '';
},
isLinkedCard() {
return this.type === 'cardType-linkedCard';
},
isLinkedBoard() {
return this.type === 'cardType-linkedBoard';
},
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 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 });
if (card === undefined) {
return null;
} else {
return card.members;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.activeMembers().map(member => {
return member.userId;
});
}
} else {
return this.members;
}
},
getAssignees() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.assignees;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.activeMembers().map(assignee => {
return assignee.userId;
});
}
} else {
return this.assignees;
}
},
assignMember(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 } },
);
}
},
assignAssignee(assigneeId) {
if (this.isLinkedCard()) {
return Cards.update(
{ _id: this.linkedId },
{ $addToSet: { assignees: assigneeId } },
);
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
return board.addAssignee(assigneeId);
} else {
return Cards.update(
{ _id: this._id },
{ $addToSet: { assignees: assigneeId } },
);
}
},
unassignMember(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 } });
}
},
unassignAssignee(assigneeId) {
if (this.isLinkedCard()) {
return Cards.update(
{ _id: this.linkedId },
{ $pull: { assignees: assigneeId } },
);
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
return board.removeAssignee(assigneeId);
} else {
return Cards.update(
{ _id: this._id },
{ $pull: { assignees: assigneeId } },
);
}
},
toggleMember(memberId) {
if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) {
return this.unassignMember(memberId);
} else {
return this.assignMember(memberId);
}
},
toggleAssignee(assigneeId) {
if (this.getAssignees() && this.getAssignees().indexOf(assigneeId) > -1) {
return this.unassignAssignee(assigneeId);
} else {
return this.assignAssignee(assigneeId);
}
},
getReceived() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.receivedAt;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.receivedAt;
}
} else {
return this.receivedAt;
}
},
setReceived(receivedAt) {
if (this.isLinkedCard()) {
return Cards.update({ _id: this.linkedId }, { $set: { receivedAt } });
} else if (this.isLinkedBoard()) {
return Boards.update({ _id: this.linkedId }, { $set: { receivedAt } });
} else {
return Cards.update({ _id: this._id }, { $set: { receivedAt } });
}
},
getStart() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.startAt;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.startAt;
}
} else {
return this.startAt;
}
},
setStart(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 } });
}
},
getDue() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.dueAt;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.dueAt;
}
} else {
return this.dueAt;
}
},
setDue(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 } });
}
},
getEnd() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.endAt;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.endAt;
}
} else {
return this.endAt;
}
},
setEnd(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 } });
}
},
getIsOvertime() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.isOvertime;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
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 } });
}
},
getSpentTime() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.spentTime;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.spentTime;
}
} else {
return this.spentTime;
}
},
setSpentTime(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 } });
}
},
getVoteQuestion() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.vote) {
return card.vote.question;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.vote) {
return board.vote.question;
} else {
return null;
}
} else if (this.vote) {
return this.vote.question;
} else {
return null;
}
},
getVotePublic() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.vote) {
return card.vote.public;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.vote) {
return board.vote.public;
} else {
return null;
}
} else if (this.vote) {
return this.vote.public;
} else {
return null;
}
},
getVoteEnd() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.vote) {
return card.vote.end;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.vote) {
return board.vote.end;
} else {
return null;
}
} else if (this.vote) {
return this.vote.end;
} else {
return null;
}
},
expiredVote() {
let end = this.getVoteEnd();
if (end) {
end = moment(end);
return end.isBefore(new Date());
}
return false;
},
voteMemberPositive() {
if (this.vote && this.vote.positive)
return Users.find({ _id: { $in: this.vote.positive } });
return [];
},
voteMemberNegative() {
if (this.vote && this.vote.negative)
return Users.find({ _id: { $in: this.vote.negative } });
return [];
},
voteState() {
const userId = Meteor.userId();
let state;
if (this.vote) {
if (this.vote.positive) {
state = _.contains(this.vote.positive, userId);
if (state === true) return true;
}
if (this.vote.negative) {
state = _.contains(this.vote.negative, userId);
if (state === true) return false;
}
}
return null;
},
getPokerQuestion() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.poker) {
return card.poker.question;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.poker) {
return board.poker.question;
} else {
return null;
}
} else if (this.poker) {
return this.poker.question;
} else {
return null;
}
},
getPokerEstimation() {
if (this.poker) {
return this.poker.estimation;
} else {
return null;
}
},
getPokerEnd() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else if (card && card.poker) {
return card.poker.end;
} else {
return null;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else if (board && board.poker) {
return board.poker.end;
} else {
return null;
}
} else if (this.poker) {
return this.poker.end;
} else {
return null;
}
},
expiredPoker() {
let end = this.getPokerEnd();
if (end) {
end = moment(end);
return end.isBefore(new Date());
}
return false;
},
pokerMemberOne() {
if (this.poker && this.poker.one)
return Users.find({ _id: { $in: this.poker.one } });
return [];
},
pokerMemberTwo() {
if (this.poker && this.poker.two)
return Users.find({ _id: { $in: this.poker.two } });
return [];
},
pokerMemberThree() {
if (this.poker && this.poker.three)
return Users.find({ _id: { $in: this.poker.three } });
return [];
},
pokerMemberFive() {
if (this.poker && this.poker.five)
return Users.find({ _id: { $in: this.poker.five } });
return [];
},
pokerMemberEight() {
if (this.poker && this.poker.eight)
return Users.find({ _id: { $in: this.poker.eight } });
return [];
},
pokerMemberThirteen() {
if (this.poker && this.poker.thirteen)
return Users.find({ _id: { $in: this.poker.thirteen } });
return [];
},
pokerMemberTwenty() {
if (this.poker && this.poker.twenty)
return Users.find({ _id: { $in: this.poker.twenty } });
return [];
},
pokerMemberForty() {
if (this.poker && this.poker.forty)
return Users.find({ _id: { $in: this.poker.forty } });
return [];
},
pokerMemberOneHundred() {
if (this.poker && this.poker.oneHundred)
return Users.find({ _id: { $in: this.poker.oneHundred } });
return [];
},
pokerMemberUnsure() {
if (this.poker && this.poker.unsure)
return Users.find({ _id: { $in: this.poker.unsure } });
return [];
},
pokerState() {
const userId = Meteor.userId();
let state;
if (this.poker) {
if (this.poker.one) {
state = _.contains(this.poker.one, userId);
if (state === true) {
return 'one';
}
}
if (this.poker.two) {
state = _.contains(this.poker.two, userId);
if (state === true) {
return 'two';
}
}
if (this.poker.three) {
state = _.contains(this.poker.three, userId);
if (state === true) {
return 'three';
}
}
if (this.poker.five) {
state = _.contains(this.poker.five, userId);
if (state === true) {
return 'five';
}
}
if (this.poker.eight) {
state = _.contains(this.poker.eight, userId);
if (state === true) {
return 'eight';
}
}
if (this.poker.thirteen) {
state = _.contains(this.poker.thirteen, userId);
if (state === true) {
return 'thirteen';
}
}
if (this.poker.twenty) {
state = _.contains(this.poker.twenty, userId);
if (state === true) {
return 'twenty';
}
}
if (this.poker.forty) {
state = _.contains(this.poker.forty, userId);
if (state === true) {
return 'forty';
}
}
if (this.poker.oneHundred) {
state = _.contains(this.poker.oneHundred, userId);
if (state === true) {
return 'oneHundred';
}
}
if (this.poker.unsure) {
state = _.contains(this.poker.unsure, userId);
if (state === true) {
return 'unsure';
}
}
}
return null;
},
getId() {
if (this.isLinked()) {
return this.linkedId;
} else {
return this._id;
}
},
getTitle() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
return card.title;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.title;
}
} else if (this.title === undefined) {
return null;
} else {
return this.title;
}
},
getCardNumber() {
return this.cardNumber;
},
getBoardTitle() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
}
const board = Boards.findOne({ _id: card.boardId });
if (board === undefined) {
return null;
} else {
return board.title;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
return board.title;
}
} else {
const board = Boards.findOne({ _id: this.boardId });
if (board === undefined) {
return null;
} else {
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 });
if (card === undefined) {
return null;
} else {
return card.archived;
}
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board === undefined) {
return null;
} else {
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 } });
}
},
getRequestedBy() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card === undefined) {
return null;
} else {
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 });
if (card === undefined) {
return null;
} else {
return card.assignedBy;
}
} else {
return this.assignedBy;
}
},
isTemplateCard() {
return this.type === 'template-card';
},
votePublic() {
if (this.vote) return this.vote.public;
return null;
},
voteAllowNonBoardMembers() {
if (this.vote) return this.vote.allowNonBoardMembers;
return null;
},
voteCountNegative() {
if (this.vote && this.vote.negative) return this.vote.negative.length;
return null;
},
voteCountPositive() {
if (this.vote && this.vote.positive) return this.vote.positive.length;
return null;
},
voteCount() {
return this.voteCountPositive() + this.voteCountNegative();
},
pokerAllowNonBoardMembers() {
if (this.poker) return this.poker.allowNonBoardMembers;
return null;
},
pokerCountOne() {
if (this.poker && this.poker.one) return this.poker.one.length;
return null;
},
pokerCountTwo() {
if (this.poker && this.poker.two) return this.poker.two.length;
return null;
},
pokerCountThree() {
if (this.poker && this.poker.three) return this.poker.three.length;
return null;
},
pokerCountFive() {
if (this.poker && this.poker.five) return this.poker.five.length;
return null;
},
pokerCountEight() {
if (this.poker && this.poker.eight) return this.poker.eight.length;
return null;
},
pokerCountThirteen() {
if (this.poker && this.poker.thirteen) return this.poker.thirteen.length;
return null;
},
pokerCountTwenty() {
if (this.poker && this.poker.twenty) return this.poker.twenty.length;
return null;
},
pokerCountForty() {
if (this.poker && this.poker.forty) return this.poker.forty.length;
return null;
},
pokerCountOneHundred() {
if (this.poker && this.poker.oneHundred) return this.poker.oneHundred.length;
return null;
},
pokerCountUnsure() {
if (this.poker && this.poker.unsure) return this.poker.unsure.length;
return null;
},
pokerCount() {
return (
this.pokerCountOne() +
this.pokerCountTwo() +
this.pokerCountThree() +
this.pokerCountFive() +
this.pokerCountEight() +
this.pokerCountThirteen() +
this.pokerCountTwenty() +
this.pokerCountForty() +
this.pokerCountOneHundred() +
this.pokerCountUnsure()
);
},
pokerWinner() {
const pokerListMaps = [];
let pokerWinnersListMap = [];
if (this.expiredPoker()) {
const one = { count: this.pokerCountOne(), pokerCard: 1 };
const two = { count: this.pokerCountTwo(), pokerCard: 2 };
const three = { count: this.pokerCountThree(), pokerCard: 3 };
const five = { count: this.pokerCountFive(), pokerCard: 5 };
const eight = { count: this.pokerCountEight(), pokerCard: 8 };
const thirteen = { count: this.pokerCountThirteen(), pokerCard: 13 };
const twenty = { count: this.pokerCountTwenty(), pokerCard: 20 };
const forty = { count: this.pokerCountForty(), pokerCard: 40 };
const oneHundred = { count: this.pokerCountOneHundred(), pokerCard: 100 };
const unsure = { count: this.pokerCountUnsure(), pokerCard: 'Unsure' };
pokerListMaps.push(one);
pokerListMaps.push(two);
pokerListMaps.push(three);
pokerListMaps.push(five);
pokerListMaps.push(eight);
pokerListMaps.push(thirteen);
pokerListMaps.push(twenty);
pokerListMaps.push(forty);
pokerListMaps.push(oneHundred);
pokerListMaps.push(unsure);
pokerListMaps.sort(function(a, b) {
return b.count - a.count;
});
const max = pokerListMaps[0].count;
pokerWinnersListMap = pokerListMaps.filter(task => task.count >= max);
pokerWinnersListMap.sort(function(a, b) {
return b.pokerCard - a.pokerCard;
});
}
return pokerWinnersListMap[0].pokerCard;
},
});
Cards.mutations({
applyToChildren(funct) {
Cards.find({
parentId: this._id,
}).forEach(card => {
funct(card);
});
},
archive() {
this.applyToChildren(card => {
return card.archive();
});
return {
$set: {
archived: true,
archivedAt: new Date(),
},
};
},
restore() {
this.applyToChildren(card => {
return card.restore();
});
return {
$set: {
archived: false,
},
};
},
moveToEndOfList({ listId } = {}) {
let swimlaneId = this.swimlaneId;
const boardId = this.boardId;
let sortIndex = 0;
// This should never happen, but there was a bug that was fixed in commit
// ea0239538a68e225c867411a4f3e0d27c158383.
if (!swimlaneId) {
const board = Boards.findOne(boardId);
swimlaneId = board.getDefaultSwimline()._id;
}
// Move the minicard to the end of the target list
let parentElementDom = $(`#swimlane-${this.swimlaneId}`).get(0);
if (!parentElementDom) parentElementDom = $(':root');
const lastCardDom = $(parentElementDom)
.find(`#js-list-${listId} .js-minicard:last`)
.get(0);
if (lastCardDom) sortIndex = Utils.calculateIndex(lastCardDom, null).base;
return this.moveOptionalArgs({
boardId,
swimlaneId,
listId,
sort: sortIndex,
});
},
moveOptionalArgs({ boardId, swimlaneId, listId, sort } = {}) {
boardId = boardId || this.boardId;
swimlaneId = swimlaneId || this.swimlaneId;
// This should never happen, but there was a bug that was fixed in commit
// ea0239538a68e225c867411a4f3e0d27c158383.
if (!swimlaneId) {
const board = Boards.findOne(boardId);
swimlaneId = board.getDefaultSwimline()._id;
}
listId = listId || this.listId;
if (sort === undefined || sort === null) sort = this.sort;
return this.move(boardId, swimlaneId, listId, sort);
},
move(boardId, swimlaneId, listId, sort = null) {
const mutatedFields = {
boardId,
swimlaneId,
listId,
};
if (sort !== null) {
mutatedFields.sort = sort;
}
// we must only copy the labels and custom fields if the target board
// differs from the source board
if (this.boardId !== boardId) {
// Get label names
const oldBoard = Boards.findOne(this.boardId);
const oldBoardLabels = oldBoard.labels;
const oldCardLabels = _.pluck(
_.filter(oldBoardLabels, label => {
return _.contains(this.labelIds, label._id);
}),
'name',
);
const newBoard = Boards.findOne(boardId);
const newBoardLabels = newBoard.labels;
const newCardLabelIds = _.pluck(
_.filter(newBoardLabels, label => {
return label.name && _.contains(oldCardLabels, label.name);
}),
'_id',
);
// assign the new card number from the target board
const newCardNumber = newBoard.getNextCardNumber();
Object.assign(mutatedFields, {
labelIds: newCardLabelIds,
cardNumber: newCardNumber
});
mutatedFields.customFields = this.mapCustomFieldsToBoard(newBoard._id);
}
Cards.update(this._id, {
$set: mutatedFields,
});
},
addLabel(labelId) {
this.labelIds.push(labelId);
return {
$addToSet: {
labelIds: labelId,
},
};
},
removeLabel(labelId) {
this.labelIds = _.without(this.labelIds, labelId);
return {
$pull: {
labelIds: labelId,
},
};
},
toggleLabel(labelId) {
if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
return this.removeLabel(labelId);
} else {
return this.addLabel(labelId);
}
},
setColor(newColor) {
if (newColor === 'white') {
newColor = null;
}
return {
$set: {
color: newColor,
},
};
},
assignMember(memberId) {
return {
$addToSet: {
members: memberId,
},
};
},
assignAssignee(assigneeId) {
// If there is not any assignee, allow one assignee, not more.
/*
if (this.getAssignees().length === 0) {
return {
$addToSet: {
assignees: assigneeId,
},
};
*/
// Allow more that one assignee:
// https://github.com/wekan/wekan/issues/3302
return {
$addToSet: {
assignees: assigneeId,
},
};
//} else {
// return false,
//}
},
unassignMember(memberId) {
return {
$pull: {
members: memberId,
},
};
},
unassignAssignee(assigneeId) {
return {
$pull: {
assignees: assigneeId,
},
};
},
toggleMember(memberId) {
if (this.members && this.members.indexOf(memberId) > -1) {
return this.unassignMember(memberId);
} else {
return this.assignMember(memberId);
}
},
toggleAssignee(assigneeId) {
if (this.assignees && this.assignees.indexOf(assigneeId) > -1) {
return this.unassignAssignee(assigneeId);
} else {
return this.assignAssignee(assigneeId);
}
},
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: '',
},
};
},
//setReceived(receivedAt) {
// return {
// $set: {
// receivedAt,
// },
// };
//},
unsetReceived() {
return {
$unset: {
receivedAt: '',
},
};
},
//setStart(startAt) {
// return {
// $set: {
// startAt,
// },
// };
//},
unsetStart() {
return {
$unset: {
startAt: '',
},
};
},
//setDue(dueAt) {
// return {
// $set: {
// dueAt,
// },
// };
//},
unsetDue() {
return {
$unset: {
dueAt: '',
},
};
},
//setEnd(endAt) {
// return {
// $set: {
// endAt,
// },
// };
//},
unsetEnd() {
return {
$unset: {
endAt: '',
},
};
},
setOvertime(isOvertime) {
return {
$set: {
isOvertime,
},
};
},
setSpentTime(spentTime) {
return {
$set: {
spentTime,
},
};
},
unsetSpentTime() {
return {
$unset: {
spentTime: '',
isOvertime: false,
},
};
},
setParentId(parentId) {
return {
$set: {
parentId,
},
};
},
setVoteQuestion(question, publicVote, allowNonBoardMembers) {
return {
$set: {
vote: {
question,
public: publicVote,
allowNonBoardMembers,
positive: [],
negative: [],
},
},
};
},
unsetVote() {
return {
$unset: {
vote: '',
},
};
},
setVoteEnd(end) {
return {
$set: { 'vote.end': end },
};
},
unsetVoteEnd() {
return {
$unset: { 'vote.end': '' },
};
},
setVote(userId, forIt) {
switch (forIt) {
case true:
// vote for it
return {
$pull: {
'vote.negative': userId,
},
$addToSet: {
'vote.positive': userId,
},
};
case false:
// vote against
return {
$pull: {
'vote.positive': userId,
},
$addToSet: {
'vote.negative': userId,
},
};
default:
// Remove votes
return {
$pull: {
'vote.positive': userId,
'vote.negative': userId,
},
};
}
},
setPokerQuestion(question, allowNonBoardMembers) {
return {
$set: {
poker: {
question,
allowNonBoardMembers,
one: [],
two: [],
three: [],
five: [],
eight: [],
thirteen: [],
twenty: [],
forty: [],
oneHundred: [],
unsure: [],
},
},
};
},
setPokerEstimation(estimation) {
return {
$set: { 'poker.estimation': estimation },
};
},
unsetPokerEstimation() {
return {
$unset: { 'poker.estimation': '' },
};
},
unsetPoker() {
return {
$unset: {
poker: '',
},
};
},
setPokerEnd(end) {
return {
$set: { 'poker.end': end },
};
},
unsetPokerEnd() {
return {
$unset: { 'poker.end': '' },
};
},
setPoker(userId, state) {
switch (state) {
case 'one':
// poker one
return {
$pull: {
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.one': userId,
},
};
case 'two':
// poker two
return {
$pull: {
'poker.one': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.two': userId,
},
};
case 'three':
// poker three
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.three': userId,
},
};
case 'five':
// poker five
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.five': userId,
},
};
case 'eight':
// poker eight
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.eight': userId,
},
};
case 'thirteen':
// poker thirteen
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.thirteen': userId,
},
};
case 'twenty':
// poker twenty
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.twenty': userId,
},
};
case 'forty':
// poker forty
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.forty': userId,
},
};
case 'oneHundred':
// poker one hundred
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.unsure': userId,
},
$addToSet: {
'poker.oneHundred': userId,
},
};
case 'unsure':
// poker unsure
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
},
$addToSet: {
'poker.unsure': userId,
},
};
default:
// Remove pokers
return {
$pull: {
'poker.one': userId,
'poker.two': userId,
'poker.three': userId,
'poker.five': userId,
'poker.eight': userId,
'poker.thirteen': userId,
'poker.twenty': userId,
'poker.forty': userId,
'poker.oneHundred': userId,
'poker.unsure': userId,
},
};
}
},
replayPoker() {
return {
$set: {
'poker.one': [],
'poker.two': [],
'poker.three': [],
'poker.five': [],
'poker.eight': [],
'poker.thirteen': [],
'poker.twelve': [],
'poker.forty': [],
'poker.oneHundred': [],
'poker.unsure': [],
},
};
},
});
//FUNCTIONS FOR creation of Activities
function updateActivities(doc, fieldNames, modifier) {
if (_.contains(fieldNames, 'labelIds') && _.contains(fieldNames, 'boardId')) {
Activities.find({
activityType: 'addedLabel',
cardId: doc._id,
}).forEach(a => {
const lidx = doc.labelIds.indexOf(a.labelId);
if (lidx !== -1 && modifier.$set.labelIds.length > lidx) {
Activities.update(a._id, {
$set: {
labelId: modifier.$set.labelIds[doc.labelIds.indexOf(a.labelId)],
boardId: modifier.$set.boardId,
},
});
} else {
Activities.remove(a._id);
}
});
} else if (_.contains(fieldNames, 'boardId')) {
Activities.remove({
activityType: 'addedLabel',
cardId: doc._id,
});
}
}
function cardMove(
userId,
doc,
fieldNames,
oldListId,
oldSwimlaneId,
oldBoardId,
) {
if (_.contains(fieldNames, 'boardId') && doc.boardId !== oldBoardId) {
Activities.insert({
userId,
activityType: 'moveCardBoard',
boardName: Boards.findOne(doc.boardId).title,
boardId: doc.boardId,
oldBoardId,
oldBoardName: Boards.findOne(oldBoardId).title,
cardId: doc._id,
swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
swimlaneId: doc.swimlaneId,
oldSwimlaneId,
});
} else if (
(_.contains(fieldNames, 'listId') && doc.listId !== oldListId) ||
(_.contains(fieldNames, 'swimlaneId') && doc.swimlaneId !== oldSwimlaneId)
) {
Activities.insert({
userId,
oldListId,
activityType: 'moveCard',
listName: Lists.findOne(doc.listId).title,
listId: doc.listId,
boardId: doc.boardId,
cardId: doc._id,
cardTitle: doc.title,
swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
swimlaneId: doc.swimlaneId,
oldSwimlaneId,
});
}
}
function cardState(userId, doc, fieldNames) {
if (_.contains(fieldNames, 'archived')) {
if (doc.archived) {
Activities.insert({
userId,
activityType: 'archivedCard',
listName: Lists.findOne(doc.listId).title,
boardId: doc.boardId,
listId: doc.listId,
cardId: doc._id,
swimlaneId: doc.swimlaneId,
});
} else {
Activities.insert({
userId,
activityType: 'restoredCard',
boardId: doc.boardId,
listName: Lists.findOne(doc.listId).title,
listId: doc.listId,
cardId: doc._id,
swimlaneId: doc.swimlaneId,
});
}
}
}
function cardMembers(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'members')) return;
let memberId;
// Say hello to the new member
if (modifier.$addToSet && modifier.$addToSet.members) {
memberId = modifier.$addToSet.members;
const username = Users.findOne(memberId).username;
if (!_.contains(doc.members, memberId)) {
Activities.insert({
userId,
username,
activityType: 'joinMember',
boardId: doc.boardId,
cardId: doc._id,
memberId,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
});
}
}
// Say goodbye to the former member
if (modifier.$pull && modifier.$pull.members) {
memberId = modifier.$pull.members;
const username = Users.findOne(memberId).username;
// Check that the former member is member of the card
if (_.contains(doc.members, memberId)) {
Activities.insert({
userId,
username,
activityType: 'unjoinMember',
boardId: doc.boardId,
cardId: doc._id,
memberId,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
});
}
}
}
function cardAssignees(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'assignees')) return;
let assigneeId;
// Say hello to the new assignee
if (modifier.$addToSet && modifier.$addToSet.assignees) {
assigneeId = modifier.$addToSet.assignees;
const username = Users.findOne(assigneeId).username;
if (!_.contains(doc.assignees, assigneeId)) {
Activities.insert({
userId,
username,
activityType: 'joinAssignee',
boardId: doc.boardId,
cardId: doc._id,
assigneeId,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
});
}
}
// Say goodbye to the former assignee
if (modifier.$pull && modifier.$pull.assignees) {
assigneeId = modifier.$pull.assignees;
const username = Users.findOne(assigneeId).username;
// Check that the former assignee is assignee of the card
if (_.contains(doc.assignees, assigneeId)) {
Activities.insert({
userId,
username,
activityType: 'unjoinAssignee',
boardId: doc.boardId,
cardId: doc._id,
assigneeId,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
});
}
}
}
function cardLabels(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'labelIds')) return;
let labelId;
// Say hello to the new label
if (modifier.$addToSet && modifier.$addToSet.labelIds) {
labelId = modifier.$addToSet.labelIds;
if (!_.contains(doc.labelIds, labelId)) {
const act = {
userId,
labelId,
activityType: 'addedLabel',
boardId: doc.boardId,
cardId: doc._id,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
};
Activities.insert(act);
}
}
// Say goodbye to the label
if (modifier.$pull && modifier.$pull.labelIds) {
labelId = modifier.$pull.labelIds;
// Check that the former member is member of the card
if (_.contains(doc.labelIds, labelId)) {
Activities.insert({
userId,
labelId,
activityType: 'removedLabel',
boardId: doc.boardId,
cardId: doc._id,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
});
}
}
}
function cardCustomFields(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'customFields')) return;
// Say hello to the new customField value
if (modifier.$set) {
_.each(modifier.$set, (value, key) => {
if (key.startsWith('customFields')) {
const dotNotation = key.split('.');
// only individual changes are registered
if (dotNotation.length > 1) {
const customFieldId = doc.customFields[dotNotation[1]]._id;
const act = {
userId,
customFieldId,
value,
activityType: 'setCustomField',
boardId: doc.boardId,
cardId: doc._id,
listId: doc.listId,
swimlaneId: doc.swimlaneId,
};
Activities.insert(act);
}
}
});
}
// Say goodbye to the former customField value
if (modifier.$unset) {
_.each(modifier.$unset, (value, key) => {
if (key.startsWith('customFields')) {
const dotNotation = key.split('.');
// only individual changes are registered
if (dotNotation.length > 1) {
const customFieldId = doc.customFields[dotNotation[1]]._id;
const act = {
userId,
customFieldId,
activityType: 'unsetCustomField',
boardId: doc.boardId,
cardId: doc._id,
};
Activities.insert(act);
}
}
});
}
}
function cardCreation(userId, doc) {
Activities.insert({
userId,
activityType: 'createCard',
boardId: doc.boardId,
listName: Lists.findOne(doc.listId).title,
listId: doc.listId,
cardId: doc._id,
cardTitle: doc.title,
swimlaneName: Swimlanes.findOne(doc.swimlaneId).title,
swimlaneId: doc.swimlaneId,
});
}
Meteor.methods({
createCardWithDueDate: function(boardId, listId, title, dueDate, swimlaneId) {
check(boardId, String);
check(listId, String);
check(title, String);
check(dueDate, Date);
check(swimlaneId, String);
const card = {
title,
listId,
boardId,
swimlaneId,
createdAt: new Date(),
dueAt: dueDate,
sort: 0,
};
const cardId = Cards.insert(card);
return cardId;
},
});
function cardRemover(userId, doc) {
ChecklistItems.remove({
cardId: doc._id,
});
Checklists.remove({
cardId: doc._id,
});
Cards.remove({
parentId: doc._id,
});
CardComments.remove({
cardId: doc._id,
});
Attachments.remove({
cardId: doc._id,
});
}
const findDueCards = days => {
const seekDue = ($from, $to, activityType) => {
Cards.find({
archived: false,
dueAt: { $gte: $from, $lt: $to },
}).forEach(card => {
const username = Users.findOne(card.userId).username;
const activity = {
userId: card.userId,
username,
activityType,
boardId: card.boardId,
cardId: card._id,
cardTitle: card.title,
listId: card.listId,
timeValue: card.dueAt,
swimlaneId: card.swimlaneId,
};
Activities.insert(activity);
});
};
const now = new Date(),
aday = 3600 * 24 * 1e3,
then = day => new Date(now.setHours(0, 0, 0, 0) + day * aday);
if (!days) return;
if (!days.map) days = [days];
days.map(day => {
let args = [];
if (day === 0) {
args = [then(0), then(1), 'duenow'];
} else if (day > 0) {
args = [then(1), then(day), 'almostdue'];
} else {
args = [then(day), now, 'pastdue'];
}
seekDue(...args);
});
};
const addCronJob = _.debounce(
Meteor.bindEnvironment(function findDueCardsDebounced() {
const envValue = process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER;
if (!envValue) {
return;
}
const notifydays = envValue
.split(',')
.map(value => {
const iValue = parseInt(value, 10);
if (!(iValue > 0 && iValue < 15)) {
// notifying due is disabled
return false;
} else {
return iValue;
}
})
.filter(Boolean);
const notifyitvl = process.env.NOTIFY_DUE_AT_HOUR_OF_DAY; //passed in the itvl has to be a number standing for the hour of current time
const defaultitvl = 8; // default every morning at 8am, if the passed env variable has parsing error use default
const itvl = parseInt(notifyitvl, 10) || defaultitvl;
const scheduler = (job => () => {
const now = new Date();
const hour = 3600 * 1e3;
if (now.getHours() === itvl) {
if (typeof job === 'function') {
job();
}
}
Meteor.setTimeout(scheduler, hour);
})(() => {
findDueCards(notifydays);
});
scheduler();
}),
500,
);
if (Meteor.isServer) {
Meteor.methods({
/** copies a card
* <li> this method is needed on the server because attachments can only be copied on the server (access to file system)
* @param card id to copy
* @param boardId copy to this board
* @param swimlandeId copy to this swimlane id
* @param listId copy to this list id
* @param insertAtTop insert the card at the top?
* @param mergeCardValues this values into the copied card
* @return the new card id
*/
copyCard(cardId, boardId, swimlaneId, listId, insertAtTop, mergeCardValues) {
check(cardId, String);
check(boardId, String);
check(swimlaneId, String);
check(listId, String);
check(insertAtTop, Boolean);
check(mergeCardValues, Object);
const card = Cards.findOne({_id: cardId});
Object.assign(card, mergeCardValues);
const sort = card.getSort(listId, swimlaneId, insertAtTop);
if (insertAtTop) {
card.sort = sort - 1;
} else
{
card.sort = sort + 1;
}
const ret = card.copy(boardId, swimlaneId, listId);
return ret;
},
});
// Cards are often fetched within a board, so we create an index to make these
// queries more efficient.
Meteor.startup(() => {
Cards._collection.createIndex({ modifiedAt: -1 });
Cards._collection.createIndex({ boardId: 1, createdAt: -1 });
// https://github.com/wekan/wekan/issues/1863
// Swimlane added a new field in the cards collection of mongodb named parentId.
// When loading a board, mongodb is searching for every cards, the id of the parent (in the swinglanes collection).
// With a huge database, this result in a very slow app and high CPU on the mongodb side.
// To correct it, add Index to parentId:
Cards._collection.createIndex({ parentId: 1 });
// let notifydays = parseInt(process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER) || 2; // default as 2 days b4 and after
// let notifyitvl = parseInt(process.env.NOTIFY_DUE_AT_HOUR_OF_DAY) || 3600 * 24 * 1e3; // default interval as one day
// Meteor.call("findDueCards",notifydays,notifyitvl);
Meteor.defer(() => {
addCronJob();
});
});
Cards.after.insert((userId, doc) => {
cardCreation(userId, doc);
});
// New activity for card (un)archivage
Cards.after.update((userId, doc, fieldNames) => {
cardState(userId, doc, fieldNames);
});
//New activity for card moves
Cards.after.update(function(userId, doc, fieldNames) {
const oldListId = this.previous.listId;
const oldSwimlaneId = this.previous.swimlaneId;
const oldBoardId = this.previous.boardId;
cardMove(userId, doc, fieldNames, oldListId, oldSwimlaneId, oldBoardId);
});
// Add a new activity if we add or remove a member to the card
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardMembers(userId, doc, fieldNames, modifier);
updateActivities(doc, fieldNames, modifier);
});
// Add a new activity if we add or remove a assignee to the card
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardAssignees(userId, doc, fieldNames, modifier);
updateActivities(doc, fieldNames, modifier);
});
// Add a new activity if we add or remove a label to the card
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardLabels(userId, doc, fieldNames, modifier);
});
// Add a new activity if we edit a custom field
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardCustomFields(userId, doc, fieldNames, modifier);
});
// Add a new activity if modify time related field like dueAt startAt etc
Cards.before.update((userId, doc, fieldNames, modifier) => {
const dla = 'dateLastActivity';
const fields = fieldNames.filter(name => name !== dla);
const timingaction = ['receivedAt', 'dueAt', 'startAt', 'endAt'];
const action = fields[0];
if (fields.length > 0 && _.contains(timingaction, action)) {
// add activities for user change these attributes
const value = modifier.$set[action];
const oldvalue = doc[action] || '';
const activityType = `a-${action}`;
const card = Cards.findOne(doc._id);
const list = card.list();
if (list) {
// change list modifiedAt, when user modified the key values in
// timingaction array, if it's endAt, put the modifiedAt of list
// back to one year ago for sorting purpose
const modifiedAt = moment()
.subtract(1, 'year')
.toISOString();
const boardId = list.boardId;
Lists.direct.update(
{
_id: list._id,
},
{
$set: {
modifiedAt,
boardId,
},
},
);
}
const username = Users.findOne(userId).username;
const activity = {
userId,
username,
activityType,
boardId: doc.boardId,
cardId: doc._id,
cardTitle: doc.title,
timeKey: action,
timeValue: value,
timeOldValue: oldvalue,
listId: card.listId,
swimlaneId: card.swimlaneId,
};
Activities.insert(activity);
}
});
// Remove all activities associated with a card if we remove the card
// Remove also card_comments / checklists / attachments
Cards.before.remove((userId, doc) => {
cardRemover(userId, doc);
});
}
//SWIMLANES REST API
if (Meteor.isServer) {
/**
* @operation get_swimlane_cards
* @summary get all cards attached to a swimlane
*
* @param {string} boardId the board ID
* @param {string} swimlaneId the swimlane ID
* @return_type [{_id: string,
* title: string,
* description: string,
* listId: string}]
*/
JsonRoutes.add(
'GET',
'/api/boards/:boardId/swimlanes/:swimlaneId/cards',
function(req, res) {
const paramBoardId = req.params.boardId;
const paramSwimlaneId = req.params.swimlaneId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Cards.find({
boardId: paramBoardId,
swimlaneId: paramSwimlaneId,
archived: false,
}).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
description: doc.description,
listId: doc.listId,
receivedAt: doc.receivedAt,
startAt: doc.startAt,
dueAt: doc.dueAt,
endAt: doc.endAt,
assignees: doc.assignees,
};
}),
});
},
);
}
//LISTS REST API
if (Meteor.isServer) {
/**
* @operation get_all_cards
* @summary Get all Cards attached to a List
*
* @param {string} boardId the board ID
* @param {string} listId the list ID
* @return_type [{_id: string,
* title: string,
* description: string}]
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards', function(
req,
res,
) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Cards.find({
boardId: paramBoardId,
listId: paramListId,
archived: false,
}).map(function(doc) {
return {
_id: doc._id,
title: doc.title,
description: doc.description,
receivedAt: doc.receivedAt,
startAt: doc.startAt,
dueAt: doc.dueAt,
endAt: doc.endAt,
assignees: doc.assignees,
};
}),
});
});
/**
* @operation get_card
* @summary Get a Card
*
* @param {string} boardId the board ID
* @param {string} listId the list ID of the card
* @param {string} cardId the card ID
* @return_type Cards
*/
JsonRoutes.add(
'GET',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
function(req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Cards.findOne({
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
}),
});
},
);
/**
* @operation new_card
* @summary Create a new Card
*
* @param {string} boardId the board ID of the new card
* @param {string} listId the list ID of the new card
* @param {string} authorID the user ID of the person owning the card
* @param {string} parentId the parent ID of the new card
* @param {string} title the title of the new card
* @param {string} description the description of the new card
* @param {string} swimlaneId the swimlane ID of the new card
* @param {string} [members] the member IDs list of the new card
* @param {string} [assignees] the array of maximum one ID of assignee of the new card
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(
req,
res,
) {
// Check user is logged in
Authentication.checkLoggedIn(req.userId);
const paramBoardId = req.params.boardId;
// Check user has permission to add card to the board
const board = Boards.findOne({
_id: paramBoardId,
});
const addPermission = allowIsBoardMemberCommentOnly(req.userId, board);
Authentication.checkAdminOrCondition(req.userId, addPermission);
const paramListId = req.params.listId;
const paramParentId = req.params.parentId;
const nextCardNumber = board.getNextCardNumber();
let customFieldsArr = [];
_.forEach(
CustomFields.find({'boardIds': paramBoardId}).fetch(),
function (field) {
if (field.automaticallyOnCard || field.alwaysOnCard)
customFieldsArr.push({ _id: field._id, value: null });
},
);
const currentCards = Cards.find(
{
listId: paramListId,
archived: false,
},
{ sort: ['sort'] },
);
const check = Users.findOne({
_id: req.body.authorId,
});
const members = req.body.members;
const assignees = req.body.assignees;
if (typeof check !== 'undefined') {
const id = Cards.direct.insert({
title: req.body.title,
boardId: paramBoardId,
listId: paramListId,
parentId: paramParentId,
description: req.body.description,
userId: req.body.authorId,
swimlaneId: req.body.swimlaneId,
sort: currentCards.count(),
cardNumber: nextCardNumber,
customFields: customFieldsArr,
members,
assignees,
});
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: id,
},
});
const card = Cards.findOne({
_id: id,
});
cardCreation(req.body.authorId, card);
} else {
JsonRoutes.sendResult(res, {
code: 401,
});
}
});
/**
* @operation get_board_cards_count
* @summary Get a cards count to a board
*
* @param {string} boardId the board ID
* @return_type {board_cards_count: integer}
*/
JsonRoutes.add('GET', '/api/boards/:boardId/cards_count', function(
req,
res,
) {
try {
const paramBoardId = req.params.boardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: {
board_cards_count: Cards.find({
boardId: paramBoardId,
archived: false,
}).count(),
}
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
/**
* @operation get_list_cards_count
* @summary Get a cards count to a list
*
* @param {string} boardId the board ID
* @param {string} listId the List ID
* @return_type {list_cards_count: integer}
*/
JsonRoutes.add('GET', '/api/boards/:boardId/lists/:listId/cards_count', function(
req,
res,
) {
try {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: {
list_cards_count: Cards.find({
boardId: paramBoardId,
listId: paramListId,
archived: false,
}).count(),
}
});
} catch (error) {
JsonRoutes.sendResult(res, {
code: 200,
data: error,
});
}
});
/*
* Note for the JSDoc:
* 'list' will be interpreted as the path parameter
* 'listID' will be interpreted as the body parameter
*/
/**
* @operation edit_card
* @summary Edit Fields in a Card
*
* @description Edit a card
*
* The color has to be chosen between `white`, `green`, `yellow`, `orange`,
* `red`, `purple`, `blue`, `sky`, `lime`, `pink`, `black`, `silver`,
* `peachpuff`, `crimson`, `plum`, `darkgreen`, `slateblue`, `magenta`,
* `gold`, `navy`, `gray`, `saddlebrown`, `paleturquoise`, `mistyrose`,
* `indigo`:
*
* <img src="/card-colors.png" width="40%" alt="Wekan card colors" />
*
* Note: setting the color to white has the same effect than removing it.
*
* @param {string} boardId the board ID of the card
* @param {string} list the list ID of the card
* @param {string} cardId the ID of the card
* @param {string} [title] the new title of the card
* @param {string} [sort] the new sort value of the card
* @param {string} [listId] the new list ID of the card (move operation)
* @param {string} [description] the new description of the card
* @param {string} [authorId] change the owner of the card
* @param {string} [parentId] change the parent of the card
* @param {string} [labelIds] the new list of label IDs attached to the card
* @param {string} [swimlaneId] the new swimlane ID of the card
* @param {string} [members] the new list of member IDs attached to the card
* @param {string} [assignees] the array of maximum one ID of assignee attached to the card
* @param {string} [requestedBy] the new requestedBy field of the card
* @param {string} [assignedBy] the new assignedBy field of the card
* @param {string} [receivedAt] the new receivedAt field of the card
* @param {string} [assignBy] the new assignBy field of the card
* @param {string} [startAt] the new startAt field of the card
* @param {string} [dueAt] the new dueAt field of the card
* @param {string} [endAt] the new endAt field of the card
* @param {string} [spentTime] the new spentTime field of the card
* @param {boolean} [isOverTime] the new isOverTime field of the card
* @param {string} [customFields] the new customFields value of the card
* @param {string} [color] the new color of the card
* @param {Object} [vote] the vote object
* @param {string} vote.question the vote question
* @param {boolean} vote.public show who voted what
* @param {boolean} vote.allowNonBoardMembers allow all logged in users to vote?
* @param {Object} [poker] the poker object
* @param {string} poker.question the vote question
* @param {boolean} poker.allowNonBoardMembers allow all logged in users to vote?
* @return_type {_id: string}
*/
JsonRoutes.add(
'PUT',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
function(req, res) {
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
const paramListId = req.params.listId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
if (req.body.title) {
const newTitle = req.body.title;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{
$set: {
title: newTitle,
},
},
);
}
if (req.body.sort) {
const newSort = req.body.sort;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{
$set: {
sort: newSort,
},
},
);
}
if (req.body.parentId) {
const newParentId = req.body.parentId;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{
$set: {
parentId: newParentId,
},
},
);
}
if (req.body.description) {
const newDescription = req.body.description;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{
$set: {
description: newDescription,
},
},
);
}
if (req.body.color) {
const newColor = req.body.color;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { color: newColor } },
);
}
if (req.body.vote) {
const newVote = req.body.vote;
newVote.positive = [];
newVote.negative = [];
if (!newVote.hasOwnProperty('public')) newVote.public = false;
if (!newVote.hasOwnProperty('allowNonBoardMembers'))
newVote.allowNonBoardMembers = false;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { vote: newVote } },
);
}
if (req.body.poker) {
const newPoker = req.body.poker;
newPoker.one = [];
newPoker.two = [];
newPoker.three = [];
newPoker.five = [];
newPoker.eight = [];
newPoker.thirteen = [];
newPoker.twenty = [];
newPoker.forty = [];
newPoker.oneHundred = [];
newPoker.unsure = [];
if (!newPoker.hasOwnProperty('allowNonBoardMembers'))
newPoker.allowNonBoardMembers = false;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { poker: newPoker } },
);
}
if (req.body.labelIds) {
let newlabelIds = req.body.labelIds;
if (_.isString(newlabelIds)) {
if (newlabelIds === '') {
newlabelIds = null;
} else {
newlabelIds = [newlabelIds];
}
}
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{
$set: {
labelIds: newlabelIds,
},
},
);
}
if (req.body.requestedBy) {
const newrequestedBy = req.body.requestedBy;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { requestedBy: newrequestedBy } },
);
}
if (req.body.assignedBy) {
const newassignedBy = req.body.assignedBy;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { assignedBy: newassignedBy } },
);
}
if (req.body.receivedAt) {
const newreceivedAt = req.body.receivedAt;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { receivedAt: newreceivedAt } },
);
}
if (req.body.startAt) {
const newstartAt = req.body.startAt;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { startAt: newstartAt } },
);
}
if (req.body.dueAt) {
const newdueAt = req.body.dueAt;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { dueAt: newdueAt } },
);
}
if (req.body.endAt) {
const newendAt = req.body.endAt;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { endAt: newendAt } },
);
}
if (req.body.spentTime) {
const newspentTime = req.body.spentTime;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { spentTime: newspentTime } },
);
}
if (req.body.isOverTime) {
const newisOverTime = req.body.isOverTime;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { isOverTime: newisOverTime } },
);
}
if (req.body.customFields) {
const newcustomFields = req.body.customFields;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { customFields: newcustomFields } },
);
}
if (req.body.members) {
let newmembers = req.body.members;
if (_.isString(newmembers)) {
if (newmembers === '') {
newmembers = null;
} else {
newmembers = [newmembers];
}
}
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { members: newmembers } },
);
}
if (req.body.assignees) {
let newassignees = req.body.assignees;
if (_.isString(newassignees)) {
if (newassignees === '') {
newassignees = null;
} else {
newassignees = [newassignees];
}
}
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { assignees: newassignees } },
);
}
if (req.body.swimlaneId) {
const newParamSwimlaneId = req.body.swimlaneId;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { swimlaneId: newParamSwimlaneId } },
);
}
if (req.body.listId) {
const newParamListId = req.body.listId;
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{
$set: {
listId: newParamListId,
},
},
);
const card = Cards.findOne({
_id: paramCardId,
});
cardMove(
req.body.authorId,
card,
{
fieldName: 'listId',
},
paramListId,
);
}
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
},
});
},
);
/**
* @operation delete_card
* @summary Delete a card from a board
*
* @description This operation **deletes** a card, and therefore the card
* is not put in the recycle bin.
*
* @param {string} boardId the board ID of the card
* @param {string} list the list ID of the card
* @param {string} cardId the ID of the card
* @return_type {_id: string}
*/
JsonRoutes.add(
'DELETE',
'/api/boards/:boardId/lists/:listId/cards/:cardId',
function(req, res) {
const paramBoardId = req.params.boardId;
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const card = Cards.findOne({
_id: paramCardId,
});
Cards.direct.remove({
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
});
cardRemover(req.body.authorId, card);
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
},
});
},
);
/**
* @operation get_cards_by_custom_field
* @summary Get all Cards that matchs a value of a specific custom field
*
* @param {string} boardId the board ID
* @param {string} customFieldId the list ID
* @param {string} customFieldValue the value to look for
* @return_type [{_id: string,
* title: string,
* description: string,
* listId: string,
* swinlaneId: string}]
*/
JsonRoutes.add(
'GET',
'/api/boards/:boardId/cardsByCustomField/:customFieldId/:customFieldValue',
function(req, res) {
const paramBoardId = req.params.boardId;
const paramCustomFieldId = req.params.customFieldId;
const paramCustomFieldValue = req.params.customFieldValue;
Authentication.checkBoardAccess(req.userId, paramBoardId);
JsonRoutes.sendResult(res, {
code: 200,
data: Cards.find({
boardId: paramBoardId,
customFields: {
$elemMatch: {
_id: paramCustomFieldId,
value: paramCustomFieldValue,
},
},
archived: false,
}).fetch(),
});
},
);
/**
* @operation edit_card_custom_field
* @summary Edit Custom Field in a Card
*
* @description Edit a custom field value in a card
* @param {string} boardId the board ID of the card
* @param {string} listId the list ID of the card
* @param {string} cardId the ID of the card
* @param {string} customFieldId the ID of the custom field
* @param {string} value the new custom field value
* @return_type {_id: string, customFields: object}
*/
JsonRoutes.add(
'POST',
'/api/boards/:boardId/lists/:listId/cards/:cardId/customFields/:customFieldId',
function(req, res) {
const paramBoardId = req.params.boardId;
const paramCardId = req.params.cardId;
const paramListId = req.params.listId;
const paramCustomFieldId = req.params.customFieldId;
const paramCustomFieldValue = req.body.value;
Authentication.checkBoardAccess(req.userId, paramBoardId);
const card = Cards.findOne({
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
});
if (!card) {
throw new Meteor.Error(404, 'Card not found');
}
const customFields = card.customFields || [];
const updatedCustomFields = customFields.map(cf => {
if (cf._id === paramCustomFieldId) {
return {
_id: cf._id,
value: paramCustomFieldValue,
};
}
return cf;
});
Cards.direct.update(
{
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
archived: false,
},
{ $set: { customFields: updatedCustomFields } },
);
JsonRoutes.sendResult(res, {
code: 200,
data: {
_id: paramCardId,
customFields: updatedCustomFields,
},
});
},
);
}
export default Cards;