cards.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import escapeForRegex from 'escape-string-regexp';
  3. import Users from '../../models/users';
  4. import {
  5. formatDateTime,
  6. formatDate,
  7. formatTime,
  8. getISOWeek,
  9. isValidDate,
  10. isBefore,
  11. isAfter,
  12. isSame,
  13. add,
  14. subtract,
  15. startOf,
  16. endOf,
  17. format,
  18. parseDate,
  19. now,
  20. createDate,
  21. fromNow,
  22. calendar
  23. } from '/imports/lib/dateUtils';
  24. import Boards from '../../models/boards';
  25. import Lists from '../../models/lists';
  26. import Swimlanes from '../../models/swimlanes';
  27. import Cards from '../../models/cards';
  28. import CardComments from '../../models/cardComments';
  29. import Attachments from '../../models/attachments';
  30. import Checklists from '../../models/checklists';
  31. import ChecklistItems from '../../models/checklistItems';
  32. import SessionData from '../../models/usersessiondata';
  33. import CustomFields from '../../models/customFields';
  34. import {
  35. DEFAULT_LIMIT,
  36. OPERATOR_ASSIGNEE,
  37. OPERATOR_BOARD,
  38. OPERATOR_COMMENT,
  39. OPERATOR_CREATED_AT,
  40. OPERATOR_CREATOR,
  41. OPERATOR_DEBUG,
  42. OPERATOR_DUE,
  43. OPERATOR_HAS,
  44. OPERATOR_LABEL,
  45. OPERATOR_LIMIT,
  46. OPERATOR_LIST,
  47. OPERATOR_MEMBER,
  48. OPERATOR_MODIFIED_AT, OPERATOR_ORG,
  49. OPERATOR_SORT,
  50. OPERATOR_STATUS,
  51. OPERATOR_SWIMLANE, OPERATOR_TEAM,
  52. OPERATOR_USER,
  53. ORDER_ASCENDING,
  54. PREDICATE_ALL,
  55. PREDICATE_ARCHIVED,
  56. PREDICATE_ASSIGNEES,
  57. PREDICATE_ATTACHMENT,
  58. PREDICATE_CHECKLIST,
  59. PREDICATE_CREATED_AT,
  60. PREDICATE_DESCRIPTION,
  61. PREDICATE_DUE_AT,
  62. PREDICATE_END_AT,
  63. PREDICATE_ENDED,
  64. PREDICATE_MEMBERS,
  65. PREDICATE_MODIFIED_AT,
  66. PREDICATE_PRIVATE,
  67. PREDICATE_PUBLIC,
  68. PREDICATE_START_AT,
  69. PREDICATE_SYSTEM,
  70. } from '/config/search-const';
  71. import { QueryErrors, QueryParams, Query } from '/config/query-classes';
  72. import { CARD_TYPES } from '../../config/const';
  73. import Org from "../../models/org";
  74. import Team from "../../models/team";
  75. Meteor.publish('card', cardId => {
  76. check(cardId, String);
  77. const ret = ReactiveCache.getCards(
  78. { _id: cardId },
  79. {},
  80. true,
  81. );
  82. return ret;
  83. });
  84. /** publish all data which is necessary to display card details as popup
  85. * @returns array of cursors
  86. */
  87. Meteor.publishRelations('popupCardData', function(cardId) {
  88. check(cardId, String);
  89. this.cursor(
  90. ReactiveCache.getCards(
  91. { _id: cardId },
  92. {},
  93. true,
  94. ),
  95. function(cardId, card) {
  96. this.cursor(ReactiveCache.getBoards({_id: card.boardId}, {}, true));
  97. this.cursor(ReactiveCache.getLists({boardId: card.boardId}, {}, true));
  98. },
  99. );
  100. const ret = this.ready()
  101. return ret;
  102. });
  103. Meteor.publish('myCards', function(sessionId) {
  104. check(sessionId, String);
  105. const queryParams = new QueryParams();
  106. queryParams.addPredicate(OPERATOR_USER, ReactiveCache.getCurrentUser().username);
  107. queryParams.setPredicate(OPERATOR_LIMIT, 200);
  108. const query = buildQuery(queryParams);
  109. query.projection.sort = {
  110. boardId: 1,
  111. swimlaneId: 1,
  112. listId: 1,
  113. };
  114. const ret = findCards(sessionId, query);
  115. return ret;
  116. });
  117. // Optimized due cards publication for better performance
  118. Meteor.publish('dueCards', function(allUsers = false) {
  119. check(allUsers, Boolean);
  120. const userId = this.userId;
  121. if (!userId) {
  122. return this.ready();
  123. }
  124. if (process.env.DEBUG === 'true') {
  125. console.log('dueCards publication called for user:', userId, 'allUsers:', allUsers);
  126. }
  127. // Get user's board memberships for efficient filtering
  128. const userBoards = ReactiveCache.getBoards({
  129. $or: [
  130. { permission: 'public' },
  131. { members: { $elemMatch: { userId, isActive: true } } }
  132. ]
  133. }).map(board => board._id);
  134. if (process.env.DEBUG === 'true') {
  135. console.log('dueCards userBoards:', userBoards);
  136. console.log('dueCards userBoards count:', userBoards.length);
  137. // Also check if there are any cards with due dates in the system at all
  138. const allCardsWithDueDates = Cards.find({
  139. type: 'cardType-card',
  140. archived: false,
  141. dueAt: { $exists: true, $nin: [null, ''] }
  142. }).count();
  143. console.log('dueCards: total cards with due dates in system:', allCardsWithDueDates);
  144. }
  145. if (userBoards.length === 0) {
  146. if (process.env.DEBUG === 'true') {
  147. console.log('dueCards: No boards found for user, returning ready');
  148. }
  149. return this.ready();
  150. }
  151. // Build optimized selector
  152. const selector = {
  153. type: 'cardType-card',
  154. archived: false,
  155. dueAt: { $exists: true, $nin: [null, ''] },
  156. boardId: { $in: userBoards }
  157. };
  158. // Add user filtering if not showing all users
  159. if (!allUsers) {
  160. selector.$or = [
  161. { members: userId },
  162. { assignees: userId },
  163. { userId: userId }
  164. ];
  165. }
  166. const options = {
  167. sort: { dueAt: 1 }, // Sort by due date ascending (oldest first)
  168. limit: 100, // Limit results for performance
  169. fields: {
  170. title: 1,
  171. dueAt: 1,
  172. boardId: 1,
  173. listId: 1,
  174. swimlaneId: 1,
  175. members: 1,
  176. assignees: 1,
  177. userId: 1,
  178. archived: 1,
  179. type: 1
  180. }
  181. };
  182. if (process.env.DEBUG === 'true') {
  183. console.log('dueCards selector:', JSON.stringify(selector, null, 2));
  184. console.log('dueCards options:', JSON.stringify(options, null, 2));
  185. }
  186. const result = Cards.find(selector, options);
  187. if (process.env.DEBUG === 'true') {
  188. const count = result.count();
  189. console.log('dueCards publication: returning', count, 'cards');
  190. if (count > 0) {
  191. const sampleCards = result.fetch().slice(0, 3);
  192. console.log('dueCards publication: sample cards:', sampleCards.map(c => ({
  193. id: c._id,
  194. title: c.title,
  195. dueAt: c.dueAt,
  196. boardId: c.boardId
  197. })));
  198. }
  199. }
  200. return result;
  201. });
  202. Meteor.publish('globalSearch', function(sessionId, params, text) {
  203. check(sessionId, String);
  204. check(params, Object);
  205. check(text, String);
  206. if (process.env.DEBUG === 'true') {
  207. console.log('globalSearch publication called with:', { sessionId, params, text });
  208. }
  209. const ret = findCards(sessionId, buildQuery(new QueryParams(params, text)));
  210. if (process.env.DEBUG === 'true') {
  211. console.log('globalSearch publication returning:', ret);
  212. }
  213. return ret;
  214. });
  215. Meteor.publish('sessionData', function(sessionId) {
  216. check(sessionId, String);
  217. const userId = Meteor.userId();
  218. if (process.env.DEBUG === 'true') {
  219. console.log('sessionData publication called with:', { sessionId, userId });
  220. }
  221. const cursor = SessionData.find({ userId, sessionId });
  222. if (process.env.DEBUG === 'true') {
  223. console.log('sessionData publication returning cursor with count:', cursor.count());
  224. }
  225. return cursor;
  226. });
  227. function buildSelector(queryParams) {
  228. const userId = Meteor.userId();
  229. const errors = new QueryErrors();
  230. let selector = {};
  231. // eslint-disable-next-line no-console
  232. // console.log('queryParams:', queryParams);
  233. if (queryParams.selector) {
  234. selector = queryParams.selector;
  235. } else {
  236. const boardsSelector = {};
  237. let archived = false;
  238. let endAt = null;
  239. if (queryParams.hasOperator(OPERATOR_STATUS)) {
  240. queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
  241. if (status === PREDICATE_ARCHIVED) {
  242. archived = true;
  243. } else if (status === PREDICATE_ALL) {
  244. archived = null;
  245. } else if (status === PREDICATE_ENDED) {
  246. endAt = { $nin: [null, ''] };
  247. } else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
  248. boardsSelector.permission = status;
  249. }
  250. });
  251. }
  252. if (queryParams.hasOperator(OPERATOR_ORG)) {
  253. const orgs = [];
  254. queryParams.getPredicates(OPERATOR_ORG).forEach(name => {
  255. const org = ReactiveCache.getOrg({
  256. $or: [
  257. { orgDisplayName: name },
  258. { orgShortName: name }
  259. ]
  260. });
  261. if (org) {
  262. orgs.push(org._id);
  263. } else {
  264. errors.addNotFound(OPERATOR_ORG, name);
  265. }
  266. });
  267. if (orgs.length) {
  268. boardsSelector.orgs = {
  269. $elemMatch: { orgId: { $in: orgs }, isActive: true }
  270. };
  271. }
  272. }
  273. if (queryParams.hasOperator(OPERATOR_TEAM)) {
  274. const teams = [];
  275. queryParams.getPredicates(OPERATOR_TEAM).forEach(name => {
  276. const team = ReactiveCache.getTeam({
  277. $or: [
  278. { teamDisplayName: name },
  279. { teamShortName: name }
  280. ]
  281. });
  282. if (team) {
  283. teams.push(team._id);
  284. } else {
  285. errors.addNotFound(OPERATOR_TEAM, name);
  286. }
  287. });
  288. if (teams.length) {
  289. boardsSelector.teams = {
  290. $elemMatch: { teamId: { $in: teams }, isActive: true }
  291. };
  292. }
  293. }
  294. selector = {
  295. type: 'cardType-card',
  296. // boardId: { $in: Boards.userBoardIds(userId) },
  297. $and: [],
  298. };
  299. if (archived !== null) {
  300. if (archived) {
  301. selector.boardId = {
  302. $in: Boards.userBoardIds(userId, null, boardsSelector),
  303. };
  304. selector.$and.push({
  305. $or: [
  306. {
  307. boardId: {
  308. $in: Boards.userBoardIds(userId, archived, boardsSelector),
  309. },
  310. },
  311. { swimlaneId: { $in: Swimlanes.userArchivedSwimlaneIds(userId) } },
  312. { listId: { $in: Lists.userArchivedListIds(userId) } },
  313. { archived: true },
  314. ],
  315. });
  316. } else {
  317. selector.boardId = {
  318. $in: Boards.userBoardIds(userId, false, boardsSelector),
  319. };
  320. selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
  321. selector.listId = { $nin: Lists.archivedListIds() };
  322. selector.archived = false;
  323. }
  324. } else {
  325. const userBoardIds = Boards.userBoardIds(userId, null, boardsSelector);
  326. if (process.env.DEBUG === 'true') {
  327. console.log('buildSelector - userBoardIds:', userBoardIds);
  328. }
  329. selector.boardId = {
  330. $in: userBoardIds,
  331. };
  332. }
  333. if (endAt !== null) {
  334. selector.endAt = endAt;
  335. }
  336. if (queryParams.hasOperator(OPERATOR_BOARD)) {
  337. const queryBoards = [];
  338. queryParams.getPredicates(OPERATOR_BOARD).forEach(query => {
  339. const boards = Boards.userSearch(userId, {
  340. title: new RegExp(escapeForRegex(query), 'i'),
  341. });
  342. if (boards.length) {
  343. boards.forEach(board => {
  344. queryBoards.push(board._id);
  345. });
  346. } else {
  347. errors.addNotFound(OPERATOR_BOARD, query);
  348. }
  349. });
  350. selector.boardId.$in = queryBoards;
  351. }
  352. if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
  353. const querySwimlanes = [];
  354. queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
  355. const swimlanes = ReactiveCache.getSwimlanes({
  356. title: new RegExp(escapeForRegex(query), 'i'),
  357. });
  358. if (swimlanes.length) {
  359. swimlanes.forEach(swim => {
  360. querySwimlanes.push(swim._id);
  361. });
  362. } else {
  363. errors.addNotFound(OPERATOR_SWIMLANE, query);
  364. }
  365. });
  366. // eslint-disable-next-line no-prototype-builtins
  367. if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
  368. selector.swimlaneId = { $in: [] };
  369. }
  370. selector.swimlaneId.$in = querySwimlanes;
  371. }
  372. if (queryParams.hasOperator(OPERATOR_LIST)) {
  373. const queryLists = [];
  374. queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
  375. const lists = ReactiveCache.getLists({
  376. title: new RegExp(escapeForRegex(query), 'i'),
  377. });
  378. if (lists.length) {
  379. lists.forEach(list => {
  380. queryLists.push(list._id);
  381. });
  382. } else {
  383. errors.addNotFound(OPERATOR_LIST, query);
  384. }
  385. });
  386. // eslint-disable-next-line no-prototype-builtins
  387. if (!selector.hasOwnProperty('listId')) {
  388. selector.listId = { $in: [] };
  389. }
  390. selector.listId.$in = queryLists;
  391. }
  392. if (queryParams.hasOperator(OPERATOR_COMMENT)) {
  393. const cardIds = CardComments.textSearch(
  394. userId,
  395. queryParams.getPredicates(OPERATOR_COMMENT),
  396. com => {
  397. return com.cardId;
  398. },
  399. );
  400. if (cardIds.length) {
  401. selector._id = { $in: cardIds };
  402. } else {
  403. queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
  404. errors.addNotFound(OPERATOR_COMMENT, comment);
  405. });
  406. }
  407. }
  408. [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].forEach(field => {
  409. if (queryParams.hasOperator(field)) {
  410. selector[field] = {};
  411. const predicate = queryParams.getPredicate(field);
  412. selector[field][predicate.operator] = new Date(predicate.value);
  413. }
  414. });
  415. const queryUsers = {};
  416. queryUsers[OPERATOR_ASSIGNEE] = [];
  417. queryUsers[OPERATOR_MEMBER] = [];
  418. queryUsers[OPERATOR_CREATOR] = [];
  419. if (queryParams.hasOperator(OPERATOR_USER)) {
  420. const users = [];
  421. queryParams.getPredicates(OPERATOR_USER).forEach(username => {
  422. const user = ReactiveCache.getUser({ username });
  423. if (user) {
  424. users.push(user._id);
  425. } else {
  426. errors.addNotFound(OPERATOR_USER, username);
  427. }
  428. });
  429. if (users.length) {
  430. selector.$and.push({
  431. $or: [{ members: { $in: users } }, { assignees: { $in: users } }],
  432. });
  433. }
  434. }
  435. [OPERATOR_MEMBER, OPERATOR_ASSIGNEE, OPERATOR_CREATOR].forEach(key => {
  436. if (queryParams.hasOperator(key)) {
  437. const users = [];
  438. queryParams.getPredicates(key).forEach(username => {
  439. const user = ReactiveCache.getUser({ username });
  440. if (user) {
  441. users.push(user._id);
  442. } else {
  443. errors.addNotFound(key, username);
  444. }
  445. });
  446. if (users.length) {
  447. selector[key] = { $in: users };
  448. }
  449. }
  450. });
  451. if (queryParams.hasOperator(OPERATOR_LABEL)) {
  452. const queryLabels = [];
  453. queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
  454. let boards = Boards.userBoards(userId, null, {
  455. labels: { $elemMatch: { color: label.toLowerCase() } },
  456. });
  457. if (boards.length) {
  458. boards.forEach(board => {
  459. // eslint-disable-next-line no-console
  460. // console.log('board:', board);
  461. // eslint-disable-next-line no-console
  462. // console.log('board.labels:', board.labels);
  463. board.labels
  464. .filter(boardLabel => {
  465. return boardLabel.color === label.toLowerCase();
  466. })
  467. .forEach(boardLabel => {
  468. queryLabels.push(boardLabel._id);
  469. });
  470. });
  471. } else {
  472. // eslint-disable-next-line no-console
  473. // console.log('label:', label);
  474. const reLabel = new RegExp(escapeForRegex(label), 'i');
  475. // eslint-disable-next-line no-console
  476. // console.log('reLabel:', reLabel);
  477. boards = Boards.userBoards(userId, null, {
  478. labels: { $elemMatch: { name: reLabel } },
  479. });
  480. if (boards.length) {
  481. boards.forEach(board => {
  482. board.labels
  483. .filter(boardLabel => {
  484. if (!boardLabel.name) {
  485. return false;
  486. }
  487. return boardLabel.name.match(reLabel);
  488. })
  489. .forEach(boardLabel => {
  490. queryLabels.push(boardLabel._id);
  491. });
  492. });
  493. } else {
  494. errors.addNotFound(OPERATOR_LABEL, label);
  495. }
  496. }
  497. });
  498. if (queryLabels.length) {
  499. // eslint-disable-next-line no-console
  500. // console.log('queryLabels:', queryLabels);
  501. selector.labelIds = { $in: _.uniq(queryLabels) };
  502. }
  503. }
  504. if (queryParams.hasOperator(OPERATOR_HAS)) {
  505. queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
  506. switch (has.field) {
  507. case PREDICATE_ATTACHMENT:
  508. selector.$and.push({
  509. _id: {
  510. $in: ReactiveCache.getAttachments({}, { fields: { cardId: 1 } }).map(
  511. a => a.cardId,
  512. ),
  513. },
  514. });
  515. break;
  516. case PREDICATE_CHECKLIST:
  517. selector.$and.push({
  518. _id: {
  519. $in: ReactiveCache.getChecklists({}, { fields: { cardId: 1 } }).map(
  520. a => a.cardId,
  521. ),
  522. },
  523. });
  524. break;
  525. case PREDICATE_DESCRIPTION:
  526. case PREDICATE_START_AT:
  527. case PREDICATE_DUE_AT:
  528. case PREDICATE_END_AT:
  529. if (has.exists) {
  530. selector[has.field] = { $exists: true, $nin: [null, ''] };
  531. } else {
  532. selector[has.field] = { $in: [null, ''] };
  533. }
  534. break;
  535. case PREDICATE_ASSIGNEES:
  536. case PREDICATE_MEMBERS:
  537. if (has.exists) {
  538. selector[has.field] = { $exists: true, $nin: [null, []] };
  539. } else {
  540. selector[has.field] = { $in: [null, []] };
  541. }
  542. break;
  543. }
  544. });
  545. }
  546. if (queryParams.text) {
  547. const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
  548. const items = ReactiveCache.getChecklistItems(
  549. { title: regex },
  550. { fields: { cardId: 1, checklistId: 1 } },
  551. );
  552. const checklists = ReactiveCache.getChecklists(
  553. {
  554. $or: [
  555. { title: regex },
  556. { _id: { $in: items.map(item => item.checklistId) } },
  557. ],
  558. },
  559. { fields: { cardId: 1 } },
  560. );
  561. const attachments = ReactiveCache.getAttachments({ 'original.name': regex });
  562. const comments = ReactiveCache.getCardComments(
  563. { text: regex },
  564. { fields: { cardId: 1 } },
  565. );
  566. let cardsSelector = [
  567. { title: regex },
  568. { description: regex },
  569. { customFields: { $elemMatch: { value: regex } } },
  570. { _id: { $in: checklists.map(list => list.cardId) } },
  571. { _id: { $in: attachments.map(attach => attach.cardId) } },
  572. { _id: { $in: comments.map(com => com.cardId) } },
  573. ];
  574. if (queryParams.text === "false" || queryParams.text === "true") {
  575. cardsSelector.push({ customFields: { $elemMatch: { value: queryParams.text === "true" } } } );
  576. }
  577. selector.$and.push({ $or: cardsSelector });
  578. }
  579. if (selector.$and.length === 0) {
  580. delete selector.$and;
  581. }
  582. }
  583. if (process.env.DEBUG === 'true') {
  584. console.log('buildSelector - final selector:', JSON.stringify(selector, null, 2));
  585. }
  586. const query = new Query();
  587. query.selector = selector;
  588. query.setQueryParams(queryParams);
  589. query._errors = errors;
  590. return query;
  591. }
  592. function buildProjection(query) {
  593. // eslint-disable-next-line no-console
  594. // console.log('query:', query);
  595. let skip = 0;
  596. if (query.getQueryParams().skip) {
  597. skip = query.getQueryParams().skip;
  598. }
  599. let limit = DEFAULT_LIMIT;
  600. const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10);
  601. if (!isNaN(configLimit) && configLimit > 0) {
  602. limit = configLimit;
  603. }
  604. if (query.getQueryParams().hasOperator(OPERATOR_LIMIT)) {
  605. limit = query.getQueryParams().getPredicate(OPERATOR_LIMIT);
  606. }
  607. const projection = {
  608. fields: {
  609. _id: 1,
  610. archived: 1,
  611. boardId: 1,
  612. swimlaneId: 1,
  613. listId: 1,
  614. title: 1,
  615. type: 1,
  616. sort: 1,
  617. members: 1,
  618. assignees: 1,
  619. colors: 1,
  620. dueAt: 1,
  621. createdAt: 1,
  622. modifiedAt: 1,
  623. labelIds: 1,
  624. customFields: 1,
  625. userId: 1,
  626. description: 1,
  627. },
  628. sort: {
  629. boardId: 1,
  630. swimlaneId: 1,
  631. listId: 1,
  632. sort: 1,
  633. },
  634. skip,
  635. };
  636. if (limit > 0) {
  637. projection.limit = limit;
  638. }
  639. if (query.getQueryParams().hasOperator(OPERATOR_SORT)) {
  640. const order =
  641. query.getQueryParams().getPredicate(OPERATOR_SORT).order ===
  642. ORDER_ASCENDING
  643. ? 1
  644. : -1;
  645. switch (query.getQueryParams().getPredicate(OPERATOR_SORT).name) {
  646. case PREDICATE_DUE_AT:
  647. projection.sort = {
  648. dueAt: order,
  649. boardId: 1,
  650. swimlaneId: 1,
  651. listId: 1,
  652. sort: 1,
  653. };
  654. break;
  655. case PREDICATE_MODIFIED_AT:
  656. projection.sort = {
  657. modifiedAt: order,
  658. boardId: 1,
  659. swimlaneId: 1,
  660. listId: 1,
  661. sort: 1,
  662. };
  663. break;
  664. case PREDICATE_CREATED_AT:
  665. projection.sort = {
  666. createdAt: order,
  667. boardId: 1,
  668. swimlaneId: 1,
  669. listId: 1,
  670. sort: 1,
  671. };
  672. break;
  673. case PREDICATE_SYSTEM:
  674. projection.sort = {
  675. boardId: order,
  676. swimlaneId: order,
  677. listId: order,
  678. modifiedAt: order,
  679. sort: order,
  680. };
  681. break;
  682. }
  683. }
  684. // eslint-disable-next-line no-console
  685. // console.log('projection:', projection);
  686. query.projection = projection;
  687. return query;
  688. }
  689. function buildQuery(queryParams) {
  690. const query = buildSelector(queryParams);
  691. return buildProjection(query);
  692. }
  693. Meteor.publish('brokenCards', function(sessionId) {
  694. check(sessionId, String);
  695. const params = new QueryParams();
  696. params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
  697. const query = buildQuery(params);
  698. query.selector.$or = [
  699. { boardId: { $in: [null, ''] } },
  700. { swimlaneId: { $in: [null, ''] } },
  701. { listId: { $in: [null, ''] } },
  702. { type: { $nin: CARD_TYPES } },
  703. ];
  704. // console.log('brokenCards selector:', query.selector);
  705. const ret = findCards(sessionId, query);
  706. return ret;
  707. });
  708. Meteor.publish('nextPage', function(sessionId) {
  709. check(sessionId, String);
  710. const session = ReactiveCache.getSessionData({ sessionId });
  711. const projection = session.getProjection();
  712. projection.skip = session.lastHit;
  713. const ret = findCards(sessionId, new Query(session.getSelector(), projection));
  714. return ret;
  715. });
  716. Meteor.publish('previousPage', function(sessionId) {
  717. check(sessionId, String);
  718. const session = ReactiveCache.getSessionData({ sessionId });
  719. const projection = session.getProjection();
  720. projection.skip = session.lastHit - session.resultsCount - projection.limit;
  721. const ret = findCards(sessionId, new Query(session.getSelector(), projection));
  722. return ret;
  723. });
  724. function findCards(sessionId, query) {
  725. const userId = Meteor.userId();
  726. // eslint-disable-next-line no-console
  727. if (process.env.DEBUG === 'true') {
  728. console.log('findCards - userId:', userId);
  729. console.log('findCards - selector:', JSON.stringify(query.selector, null, 2));
  730. console.log('findCards - selector.$and:', query.selector.$and);
  731. console.log('findCards - projection:', query.projection);
  732. }
  733. const cards = ReactiveCache.getCards(query.selector, query.projection, true);
  734. if (process.env.DEBUG === 'true') {
  735. console.log('findCards - cards count:', cards ? cards.count() : 0);
  736. }
  737. const update = {
  738. $set: {
  739. totalHits: 0,
  740. lastHit: 0,
  741. resultsCount: 0,
  742. cards: [],
  743. selector: SessionData.pickle(query.selector),
  744. projection: SessionData.pickle(query.projection),
  745. errors: query.errors(),
  746. debug: query.getQueryParams().getPredicate(OPERATOR_DEBUG),
  747. modifiedAt: new Date()
  748. },
  749. };
  750. if (cards) {
  751. update.$set.totalHits = cards.count();
  752. update.$set.lastHit =
  753. query.projection.skip + query.projection.limit < cards.count()
  754. ? query.projection.skip + query.projection.limit
  755. : cards.count();
  756. update.$set.cards = cards.map(card => {
  757. return card._id;
  758. });
  759. update.$set.resultsCount = update.$set.cards.length;
  760. }
  761. if (process.env.DEBUG === 'true') {
  762. console.log('findCards - sessionId:', sessionId);
  763. console.log('findCards - userId:', userId);
  764. console.log('findCards - update:', JSON.stringify(update, null, 2));
  765. }
  766. const upsertResult = SessionData.upsert({ userId, sessionId }, update);
  767. if (process.env.DEBUG === 'true') {
  768. console.log('findCards - upsertResult:', upsertResult);
  769. }
  770. // Check if the session data was actually stored
  771. const storedSessionData = SessionData.findOne({ userId, sessionId });
  772. if (process.env.DEBUG === 'true') {
  773. console.log('findCards - stored session data:', storedSessionData);
  774. console.log('findCards - stored session data count:', storedSessionData ? 1 : 0);
  775. }
  776. // remove old session data
  777. SessionData.remove({
  778. userId,
  779. modifiedAt: {
  780. $lt: new Date(
  781. subtract(now(), 1, 'day').toISOString(),
  782. ),
  783. },
  784. });
  785. if (cards) {
  786. const boards = [];
  787. const swimlanes = [];
  788. const lists = [];
  789. const customFieldIds = [];
  790. const users = [this.userId];
  791. cards.forEach(card => {
  792. if (card.boardId) boards.push(card.boardId);
  793. if (card.swimlaneId) swimlanes.push(card.swimlaneId);
  794. if (card.listId) lists.push(card.listId);
  795. if (card.userId) {
  796. users.push(card.userId);
  797. }
  798. if (card.members) {
  799. card.members.forEach(userId => {
  800. users.push(userId);
  801. });
  802. }
  803. if (card.assignees) {
  804. card.assignees.forEach(userId => {
  805. users.push(userId);
  806. });
  807. }
  808. if (card.customFields) {
  809. card.customFields.forEach(field => {
  810. customFieldIds.push(field._id);
  811. });
  812. }
  813. });
  814. const fields = {
  815. _id: 1,
  816. title: 1,
  817. archived: 1,
  818. sort: 1,
  819. type: 1,
  820. };
  821. // Add a small delay to ensure the session data is committed to the database
  822. Meteor.setTimeout(() => {
  823. const sessionDataCursor = SessionData.find({ userId, sessionId });
  824. if (process.env.DEBUG === 'true') {
  825. console.log('findCards - publishing session data cursor (after delay):', sessionDataCursor);
  826. console.log('findCards - session data count (after delay):', sessionDataCursor.count());
  827. }
  828. }, 100);
  829. const sessionDataCursor = SessionData.find({ userId, sessionId });
  830. if (process.env.DEBUG === 'true') {
  831. console.log('findCards - publishing session data cursor:', sessionDataCursor);
  832. console.log('findCards - session data count:', sessionDataCursor.count());
  833. }
  834. return [
  835. cards,
  836. ReactiveCache.getBoards(
  837. { _id: { $in: boards } },
  838. { fields: { ...fields, labels: 1, color: 1 } },
  839. true,
  840. ),
  841. ReactiveCache.getSwimlanes(
  842. { _id: { $in: swimlanes } },
  843. { fields: { ...fields, color: 1 } },
  844. true,
  845. ),
  846. ReactiveCache.getLists({ _id: { $in: lists } }, { fields }, true),
  847. ReactiveCache.getCustomFields({ _id: { $in: customFieldIds } }, {}, true),
  848. ReactiveCache.getUsers({ _id: { $in: users } }, { fields: Users.safeFields }, true),
  849. ReactiveCache.getChecklists({ cardId: { $in: cards.map(c => c._id) } }, {}, true),
  850. ReactiveCache.getChecklistItems({ cardId: { $in: cards.map(c => c._id) } }, {}, true),
  851. ReactiveCache.getAttachments({ 'meta.cardId': { $in: cards.map(c => c._id) } }, {}, true).cursor,
  852. ReactiveCache.getCardComments({ cardId: { $in: cards.map(c => c._id) } }, {}, true),
  853. sessionDataCursor,
  854. ];
  855. }
  856. const sessionDataCursor = SessionData.find({ userId, sessionId });
  857. if (process.env.DEBUG === 'true') {
  858. console.log('findCards - publishing session data cursor (no cards):', sessionDataCursor);
  859. console.log('findCards - session data count (no cards):', sessionDataCursor.count());
  860. }
  861. return [sessionDataCursor];
  862. }