cards.js 26 KB

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