cards.js 25 KB

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