The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
wekan/server/publications/cards.js

737 lines
20 KiB

import moment from 'moment';
import Users from '../../models/users';
import Boards from '../../models/boards';
import Lists from '../../models/lists';
import Swimlanes from '../../models/swimlanes';
import Cards from '../../models/cards';
import CardComments from '../../models/cardComments';
import Attachments from '../../models/attachments';
import Checklists from '../../models/checklists';
import ChecklistItems from '../../models/checklistItems';
import SessionData from '../../models/usersessiondata';
import CustomFields from '../../models/customFields';
import {
DEFAULT_LIMIT,
OPERATOR_ASSIGNEE,
OPERATOR_BOARD,
OPERATOR_COMMENT,
OPERATOR_CREATED_AT,
OPERATOR_CREATOR,
OPERATOR_DEBUG,
OPERATOR_DUE,
OPERATOR_HAS,
OPERATOR_LABEL,
OPERATOR_LIMIT,
OPERATOR_LIST,
OPERATOR_MEMBER,
OPERATOR_MODIFIED_AT,
OPERATOR_SORT,
OPERATOR_STATUS,
OPERATOR_SWIMLANE,
OPERATOR_USER,
ORDER_ASCENDING,
PREDICATE_ALL,
PREDICATE_ARCHIVED,
PREDICATE_ASSIGNEES,
PREDICATE_ATTACHMENT,
PREDICATE_CHECKLIST,
PREDICATE_CREATED_AT,
PREDICATE_DESCRIPTION,
PREDICATE_DUE_AT,
PREDICATE_END_AT,
PREDICATE_ENDED,
PREDICATE_MEMBERS,
PREDICATE_MODIFIED_AT,
PREDICATE_PRIVATE,
PREDICATE_PUBLIC,
PREDICATE_START_AT,
PREDICATE_SYSTEM,
} from '/config/search-const';
import { QueryErrors, QueryParams, Query } from '/config/query-classes';
import { CARD_TYPES } from '../../config/const';
const escapeForRegex = require('escape-string-regexp');
Meteor.publish('card', cardId => {
check(cardId, String);
const ret = Cards.find({ _id: cardId });
return ret;
});
/** publish all data which is necessary to display card details as popup
* @returns array of cursors
*/
Meteor.publishRelations('popupCardData', function(cardId) {
check(cardId, String);
this.cursor(
Cards.find({_id: cardId}),
function(cardId, card) {
this.cursor(Boards.find({_id: card.boardId}));
},
);
return this.ready()
});
Meteor.publish('myCards', function(sessionId) {
check(sessionId, String);
const queryParams = new QueryParams();
queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
queryParams.setPredicate(OPERATOR_LIMIT, 200);
const query = buildQuery(queryParams);
query.projection.sort = {
boardId: 1,
swimlaneId: 1,
listId: 1,
};
return findCards(sessionId, query);
});
// Meteor.publish('dueCards', function(sessionId, allUsers = false) {
// check(sessionId, String);
// check(allUsers, Boolean);
//
// // eslint-disable-next-line no-console
// // console.log('all users:', allUsers);
//
// const queryParams = {
// has: [{ field: 'dueAt', exists: true }],
// limit: 25,
// skip: 0,
// sort: { name: 'dueAt', order: 'des' },
// };
//
// if (!allUsers) {
// queryParams.users = [Meteor.user().username];
// }
//
// return buildQuery(sessionId, queryParams);
// });
Meteor.publish('globalSearch', function(sessionId, params, text) {
check(sessionId, String);
check(params, Object);
check(text, String);
// eslint-disable-next-line no-console
// console.log('queryParams:', params);
return findCards(sessionId, buildQuery(new QueryParams(params, text)));
});
function buildSelector(queryParams) {
const userId = Meteor.userId();
const errors = new QueryErrors();
let selector = {};
// eslint-disable-next-line no-console
// console.log('queryParams:', queryParams);
if (queryParams.selector) {
selector = queryParams.selector;
} else {
const boardsSelector = {};
let archived = false;
let endAt = null;
if (queryParams.hasOperator(OPERATOR_STATUS)) {
queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
if (status === PREDICATE_ARCHIVED) {
archived = true;
} else if (status === PREDICATE_ALL) {
archived = null;
} else if (status === PREDICATE_ENDED) {
endAt = { $nin: [null, ''] };
} else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
boardsSelector.permission = status;
}
});
}
selector = {
type: 'cardType-card',
// boardId: { $in: Boards.userBoardIds(userId) },
$and: [],
};
if (archived !== null) {
if (archived) {
selector.boardId = {
$in: Boards.userBoardIds(userId, null, boardsSelector),
};
selector.$and.push({
$or: [
{
boardId: {
$in: Boards.userBoardIds(userId, archived, boardsSelector),
},
},
{ swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
{ listId: { $in: Lists.archivedListIds() } },
{ archived: true },
],
});
} else {
selector.boardId = {
$in: Boards.userBoardIds(userId, false, boardsSelector),
};
selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
selector.listId = { $nin: Lists.archivedListIds() };
selector.archived = false;
}
} else {
selector.boardId = {
$in: Boards.userBoardIds(userId, null, boardsSelector),
};
}
if (endAt !== null) {
selector.endAt = endAt;
}
if (queryParams.hasOperator(OPERATOR_BOARD)) {
const queryBoards = [];
queryParams.getPredicates(OPERATOR_BOARD).forEach(query => {
const boards = Boards.userSearch(userId, {
title: new RegExp(escapeForRegex(query), 'i'),
});
if (boards.count()) {
boards.forEach(board => {
queryBoards.push(board._id);
});
} else {
errors.addNotFound(OPERATOR_BOARD, query);
}
});
selector.boardId.$in = queryBoards;
}
if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
const querySwimlanes = [];
queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
const swimlanes = Swimlanes.find({
title: new RegExp(escapeForRegex(query), 'i'),
});
if (swimlanes.count()) {
swimlanes.forEach(swim => {
querySwimlanes.push(swim._id);
});
} else {
errors.addNotFound(OPERATOR_SWIMLANE, query);
}
});
// eslint-disable-next-line no-prototype-builtins
if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
selector.swimlaneId = { $in: [] };
}
selector.swimlaneId.$in = querySwimlanes;
}
if (queryParams.hasOperator(OPERATOR_LIST)) {
const queryLists = [];
queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
const lists = Lists.find({
title: new RegExp(escapeForRegex(query), 'i'),
});
if (lists.count()) {
lists.forEach(list => {
queryLists.push(list._id);
});
} else {
errors.addNotFound(OPERATOR_LIST, query);
}
});
// eslint-disable-next-line no-prototype-builtins
if (!selector.hasOwnProperty('listId')) {
selector.listId = { $in: [] };
}
selector.listId.$in = queryLists;
}
if (queryParams.hasOperator(OPERATOR_COMMENT)) {
const cardIds = CardComments.textSearch(
userId,
queryParams.getPredicates(OPERATOR_COMMENT),
com => {
return com.cardId;
},
);
if (cardIds.length) {
selector._id = { $in: cardIds };
} else {
queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
errors.addNotFound(OPERATOR_COMMENT, comment);
});
}
}
[OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].forEach(field => {
if (queryParams.hasOperator(field)) {
selector[field] = {};
const predicate = queryParams.getPredicate(field);
selector[field][predicate.operator] = new Date(predicate.value);
}
});
const queryUsers = {};
queryUsers[OPERATOR_ASSIGNEE] = [];
queryUsers[OPERATOR_MEMBER] = [];
queryUsers[OPERATOR_CREATOR] = [];
if (queryParams.hasOperator(OPERATOR_USER)) {
const users = [];
queryParams.getPredicates(OPERATOR_USER).forEach(username => {
const user = Users.findOne({ username });
if (user) {
users.push(user._id);
} else {
errors.addNotFound(OPERATOR_USER, username);
}
});
if (users.length) {
selector.$and.push({
$or: [{ members: { $in: users } }, { assignees: { $in: users } }],
});
}
}
[OPERATOR_MEMBER, OPERATOR_ASSIGNEE, OPERATOR_CREATOR].forEach(key => {
if (queryParams.hasOperator(key)) {
const users = [];
queryParams.getPredicates(key).forEach(username => {
const user = Users.findOne({ username });
if (user) {
users.push(user._id);
} else {
errors.addNotFound(key, username);
}
});
if (users.length) {
selector[key] = { $in: users };
}
}
});
if (queryParams.hasOperator(OPERATOR_LABEL)) {
const queryLabels = [];
queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
let boards = Boards.userBoards(userId, null, {
labels: { $elemMatch: { color: label.toLowerCase() } },
});
if (boards.count()) {
boards.forEach(board => {
// eslint-disable-next-line no-console
// console.log('board:', board);
// eslint-disable-next-line no-console
// console.log('board.labels:', board.labels);
board.labels
.filter(boardLabel => {
return boardLabel.color === label.toLowerCase();
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
// eslint-disable-next-line no-console
// console.log('label:', label);
const reLabel = new RegExp(escapeForRegex(label), 'i');
// eslint-disable-next-line no-console
// console.log('reLabel:', reLabel);
boards = Boards.userBoards(userId, null, {
labels: { $elemMatch: { name: reLabel } },
});
if (boards.count()) {
boards.forEach(board => {
board.labels
.filter(boardLabel => {
if (!boardLabel.name) {
return false;
}
return boardLabel.name.match(reLabel);
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
errors.addNotFound(OPERATOR_LABEL, label);
}
}
});
if (queryLabels.length) {
// eslint-disable-next-line no-console
// console.log('queryLabels:', queryLabels);
selector.labelIds = { $in: _.uniq(queryLabels) };
}
}
if (queryParams.hasOperator(OPERATOR_HAS)) {
queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
switch (has.field) {
case PREDICATE_ATTACHMENT:
selector.$and.push({
_id: {
$in: Attachments.find({}, { fields: { cardId: 1 } }).map(
a => a.cardId,
),
},
});
break;
case PREDICATE_CHECKLIST:
selector.$and.push({
_id: {
$in: Checklists.find({}, { fields: { cardId: 1 } }).map(
a => a.cardId,
),
},
});
break;
case PREDICATE_DESCRIPTION:
case PREDICATE_START_AT:
case PREDICATE_DUE_AT:
case PREDICATE_END_AT:
if (has.exists) {
selector[has.field] = { $exists: true, $nin: [null, ''] };
} else {
selector[has.field] = { $in: [null, ''] };
}
break;
case PREDICATE_ASSIGNEES:
case PREDICATE_MEMBERS:
if (has.exists) {
selector[has.field] = { $exists: true, $nin: [null, []] };
} else {
selector[has.field] = { $in: [null, []] };
}
break;
}
});
}
if (queryParams.text) {
const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
const items = ChecklistItems.find(
{ title: regex },
{ fields: { cardId: 1, checklistId: 1 } },
);
const checklists = Checklists.find(
{
$or: [
{ title: regex },
{ _id: { $in: items.map(item => item.checklistId) } },
],
},
{ fields: { cardId: 1 } },
);
const attachments = Attachments.find({ 'original.name': regex });
const comments = CardComments.find(
{ text: regex },
{ fields: { cardId: 1 } },
);
let cardsSelector = [
{ title: regex },
{ description: regex },
{ customFields: { $elemMatch: { value: regex } } },
{ _id: { $in: checklists.map(list => list.cardId) } },
{ _id: { $in: attachments.map(attach => attach.cardId) } },
{ _id: { $in: comments.map(com => com.cardId) } },
];
if (queryParams.text === "false" || queryParams.text === "true") {
cardsSelector.push({ customFields: { $elemMatch: { value: queryParams.text === "true" } } } );
}
selector.$and.push({ $or: cardsSelector });
}
if (selector.$and.length === 0) {
delete selector.$and;
}
}
// eslint-disable-next-line no-console
// console.log('cards selector:', JSON.stringify(selector, null, 2));
const query = new Query();
query.selector = selector;
query.setQueryParams(queryParams);
query._errors = errors;
return query;
}
function buildProjection(query) {
// eslint-disable-next-line no-console
// console.log('query:', query);
let skip = 0;
if (query.getQueryParams().skip) {
skip = query.getQueryParams().skip;
}
let limit = DEFAULT_LIMIT;
const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10);
if (!isNaN(configLimit) && configLimit > 0) {
limit = configLimit;
}
if (query.getQueryParams().hasOperator(OPERATOR_LIMIT)) {
limit = query.getQueryParams().getPredicate(OPERATOR_LIMIT);
}
const projection = {
fields: {
_id: 1,
archived: 1,
boardId: 1,
swimlaneId: 1,
listId: 1,
title: 1,
type: 1,
sort: 1,
members: 1,
assignees: 1,
colors: 1,
dueAt: 1,
createdAt: 1,
modifiedAt: 1,
labelIds: 1,
customFields: 1,
userId: 1,
description: 1,
},
sort: {
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
},
skip,
limit,
};
if (query.getQueryParams().hasOperator(OPERATOR_SORT)) {
const order =
query.getQueryParams().getPredicate(OPERATOR_SORT).order ===
ORDER_ASCENDING
? 1
: -1;
switch (query.getQueryParams().getPredicate(OPERATOR_SORT).name) {
case PREDICATE_DUE_AT:
projection.sort = {
dueAt: order,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
break;
case PREDICATE_MODIFIED_AT:
projection.sort = {
modifiedAt: order,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
break;
case PREDICATE_CREATED_AT:
projection.sort = {
createdAt: order,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
break;
case PREDICATE_SYSTEM:
projection.sort = {
boardId: order,
swimlaneId: order,
listId: order,
modifiedAt: order,
sort: order,
};
break;
}
}
// eslint-disable-next-line no-console
// console.log('projection:', projection);
query.projection = projection;
return query;
}
function buildQuery(queryParams) {
const query = buildSelector(queryParams);
return buildProjection(query);
}
Meteor.publish('brokenCards', function(sessionId) {
check(sessionId, String);
const params = new QueryParams();
params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
const query = buildQuery(params);
query.selector.$or = [
{ boardId: { $in: [null, ''] } },
{ swimlaneId: { $in: [null, ''] } },
{ listId: { $in: [null, ''] } },
{ type: { $nin: CARD_TYPES } },
];
// console.log('brokenCards selector:', query.selector);
return findCards(sessionId, query);
});
Meteor.publish('nextPage', function(sessionId) {
check(sessionId, String);
const session = SessionData.findOne({ sessionId });
const projection = session.getProjection();
projection.skip = session.lastHit;
return findCards(sessionId, new Query(session.getSelector(), projection));
});
Meteor.publish('previousPage', function(sessionId) {
check(sessionId, String);
const session = SessionData.findOne({ sessionId });
const projection = session.getProjection();
projection.skip = session.lastHit - session.resultsCount - projection.limit;
return findCards(sessionId, new Query(session.getSelector(), projection));
});
function findCards(sessionId, query) {
const userId = Meteor.userId();
// eslint-disable-next-line no-console
// console.log('selector:', query.selector);
// console.log('selector.$and:', query.selector.$and);
// eslint-disable-next-line no-console
// console.log('projection:', query.projection);
const cards = Cards.find(query.selector, query.projection);
// eslint-disable-next-line no-console
// console.log('count:', cards.count());
const update = {
$set: {
totalHits: 0,
lastHit: 0,
resultsCount: 0,
cards: [],
selector: SessionData.pickle(query.selector),
projection: SessionData.pickle(query.projection),
errors: query.errors(),
debug: query.getQueryParams().getPredicate(OPERATOR_DEBUG)
},
};
if (cards) {
update.$set.totalHits = cards.count();
update.$set.lastHit =
query.projection.skip + query.projection.limit < cards.count()
? query.projection.skip + query.projection.limit
: cards.count();
update.$set.cards = cards.map(card => {
return card._id;
});
update.$set.resultsCount = update.$set.cards.length;
}
// eslint-disable-next-line no-console
// console.log('sessionId:', sessionId);
// eslint-disable-next-line no-console
// console.log('userId:', userId);
// eslint-disable-next-line no-console
// console.log('update:', update);
SessionData.upsert({ userId, sessionId }, update);
// remove old session data
SessionData.remove({
userId,
modifiedAt: {
$lt: new Date(
moment()
.subtract(1, 'day')
.format(),
),
},
});
if (cards) {
const boards = [];
const swimlanes = [];
const lists = [];
const customFieldIds = [];
const users = [this.userId];
cards.forEach(card => {
if (card.boardId) boards.push(card.boardId);
if (card.swimlaneId) swimlanes.push(card.swimlaneId);
if (card.listId) lists.push(card.listId);
if (card.userId) {
users.push(card.userId);
}
if (card.members) {
card.members.forEach(userId => {
users.push(userId);
});
}
if (card.assignees) {
card.assignees.forEach(userId => {
users.push(userId);
});
}
if (card.customFields) {
card.customFields.forEach(field => {
customFieldIds.push(field._id);
});
}
});
const fields = {
_id: 1,
title: 1,
archived: 1,
sort: 1,
type: 1,
};
return [
cards,
Boards.find(
{ _id: { $in: boards } },
{ fields: { ...fields, labels: 1, color: 1 } },
),
Swimlanes.find(
{ _id: { $in: swimlanes } },
{ fields: { ...fields, color: 1 } },
),
Lists.find({ _id: { $in: lists } }, { fields }),
CustomFields.find({ _id: { $in: customFieldIds } }),
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
ChecklistItems.find({ cardId: { $in: cards.map(c => c._id) } }),
Attachments.find({ cardId: { $in: cards.map(c => c._id) } }),
CardComments.find({ cardId: { $in: cards.map(c => c._id) } }),
SessionData.find({ userId, sessionId }),
];
}
return [SessionData.find({ userId, sessionId })];
}