cards.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  1. const escapeForRegex = require('escape-string-regexp');
  2. Meteor.publish('card', cardId => {
  3. check(cardId, String);
  4. return Cards.find({ _id: cardId });
  5. });
  6. Meteor.publish('myCards', function(sessionId) {
  7. const queryParams = {
  8. users: [Meteor.user().username],
  9. // limit: 25,
  10. skip: 0,
  11. // sort: { name: 'dueAt', order: 'des' },
  12. };
  13. return buildQuery(sessionId, queryParams);
  14. });
  15. // Meteor.publish('dueCards', function(sessionId, allUsers = false) {
  16. // check(sessionId, String);
  17. // check(allUsers, Boolean);
  18. //
  19. // // eslint-disable-next-line no-console
  20. // // console.log('all users:', allUsers);
  21. //
  22. // const queryParams = {
  23. // has: [{ field: 'dueAt', exists: true }],
  24. // limit: 25,
  25. // skip: 0,
  26. // sort: { name: 'dueAt', order: 'des' },
  27. // };
  28. //
  29. // if (!allUsers) {
  30. // queryParams.users = [Meteor.user().username];
  31. // }
  32. //
  33. // return buildQuery(sessionId, queryParams);
  34. // });
  35. Meteor.publish('globalSearch', function(sessionId, queryParams) {
  36. check(sessionId, String);
  37. check(queryParams, Object);
  38. // eslint-disable-next-line no-console
  39. // console.log('queryParams:', queryParams);
  40. return buildQuery(sessionId, queryParams);
  41. });
  42. function buildQuery(sessionId, queryParams) {
  43. const userId = Meteor.userId();
  44. const errors = new (class {
  45. constructor() {
  46. this.notFound = {
  47. boards: [],
  48. swimlanes: [],
  49. lists: [],
  50. labels: [],
  51. users: [],
  52. members: [],
  53. assignees: [],
  54. status: [],
  55. comments: [],
  56. };
  57. this.colorMap = Boards.colorMap();
  58. }
  59. hasErrors() {
  60. for (const value of Object.values(this.notFound)) {
  61. if (value.length) {
  62. return true;
  63. }
  64. }
  65. return false;
  66. }
  67. errorMessages() {
  68. const messages = [];
  69. this.notFound.boards.forEach(board => {
  70. messages.push({ tag: 'board-title-not-found', value: board });
  71. });
  72. this.notFound.swimlanes.forEach(swim => {
  73. messages.push({ tag: 'swimlane-title-not-found', value: swim });
  74. });
  75. this.notFound.lists.forEach(list => {
  76. messages.push({ tag: 'list-title-not-found', value: list });
  77. });
  78. this.notFound.comments.forEach(comments => {
  79. comments.forEach(text => {
  80. messages.push({ tag: 'comment-not-found', value: text });
  81. });
  82. });
  83. this.notFound.labels.forEach(label => {
  84. messages.push({
  85. tag: 'label-not-found',
  86. value: label,
  87. color: Boards.labelColors().includes(label),
  88. });
  89. });
  90. this.notFound.users.forEach(user => {
  91. messages.push({ tag: 'user-username-not-found', value: user });
  92. });
  93. this.notFound.members.forEach(user => {
  94. messages.push({ tag: 'user-username-not-found', value: user });
  95. });
  96. this.notFound.assignees.forEach(user => {
  97. messages.push({ tag: 'user-username-not-found', value: user });
  98. });
  99. return messages;
  100. }
  101. })();
  102. let selector = {};
  103. let skip = 0;
  104. if (queryParams.skip) {
  105. skip = queryParams.skip;
  106. }
  107. let limit = 25;
  108. if (queryParams.limit) {
  109. limit = queryParams.limit;
  110. }
  111. if (queryParams.selector) {
  112. selector = queryParams.selector;
  113. } else {
  114. const boardsSelector = {};
  115. let archived = false;
  116. let endAt = null;
  117. if (queryParams.status && queryParams.status.length) {
  118. queryParams.status.forEach(status => {
  119. if (status === 'archived') {
  120. archived = true;
  121. } else if (status === 'all') {
  122. archived = null;
  123. } else if (status === 'ended') {
  124. endAt = { $nin: [null, ''] };
  125. } else if (['private', 'public'].includes(status)) {
  126. boardsSelector.permission = status;
  127. }
  128. });
  129. }
  130. selector = {
  131. type: 'cardType-card',
  132. // boardId: { $in: Boards.userBoardIds(userId) },
  133. $and: [],
  134. };
  135. if (archived !== null) {
  136. if (archived) {
  137. selector.boardId = {
  138. $in: Boards.userBoardIds(userId, null, boardsSelector),
  139. };
  140. selector.$and.push({
  141. $or: [
  142. {
  143. boardId: {
  144. $in: Boards.userBoardIds(userId, archived, boardsSelector),
  145. },
  146. },
  147. { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
  148. { listId: { $in: Lists.archivedListIds() } },
  149. { archived: true },
  150. ],
  151. });
  152. } else {
  153. selector.boardId = {
  154. $in: Boards.userBoardIds(userId, false, boardsSelector),
  155. };
  156. selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
  157. selector.listId = { $nin: Lists.archivedListIds() };
  158. selector.archived = false;
  159. }
  160. } else {
  161. selector.boardId = {
  162. $in: Boards.userBoardIds(userId, null, boardsSelector),
  163. };
  164. }
  165. if (endAt !== null) {
  166. selector.endAt = endAt;
  167. }
  168. if (queryParams.boards && queryParams.boards.length) {
  169. const queryBoards = [];
  170. queryParams.boards.forEach(query => {
  171. const boards = Boards.userSearch(userId, {
  172. title: new RegExp(escapeForRegex(query), 'i'),
  173. });
  174. if (boards.count()) {
  175. boards.forEach(board => {
  176. queryBoards.push(board._id);
  177. });
  178. } else {
  179. errors.notFound.boards.push(query);
  180. }
  181. });
  182. selector.boardId.$in = queryBoards;
  183. }
  184. if (queryParams.swimlanes && queryParams.swimlanes.length) {
  185. const querySwimlanes = [];
  186. queryParams.swimlanes.forEach(query => {
  187. const swimlanes = Swimlanes.find({
  188. title: new RegExp(escapeForRegex(query), 'i'),
  189. });
  190. if (swimlanes.count()) {
  191. swimlanes.forEach(swim => {
  192. querySwimlanes.push(swim._id);
  193. });
  194. } else {
  195. errors.notFound.swimlanes.push(query);
  196. }
  197. });
  198. // eslint-disable-next-line no-prototype-builtins
  199. if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
  200. selector.swimlaneId = { $in: [] };
  201. }
  202. selector.swimlaneId.$in = querySwimlanes;
  203. }
  204. if (queryParams.lists && queryParams.lists.length) {
  205. const queryLists = [];
  206. queryParams.lists.forEach(query => {
  207. const lists = Lists.find({
  208. title: new RegExp(escapeForRegex(query), 'i'),
  209. });
  210. if (lists.count()) {
  211. lists.forEach(list => {
  212. queryLists.push(list._id);
  213. });
  214. } else {
  215. errors.notFound.lists.push(query);
  216. }
  217. });
  218. // eslint-disable-next-line no-prototype-builtins
  219. if (!selector.hasOwnProperty('listId')) {
  220. selector.listId = { $in: [] };
  221. }
  222. selector.listId.$in = queryLists;
  223. }
  224. if (queryParams.comments && queryParams.comments.length) {
  225. const cardIds = CardComments.textSearch(userId, queryParams.comments).map(
  226. com => {
  227. return com.cardId;
  228. },
  229. );
  230. if (cardIds.length) {
  231. selector._id = { $in: cardIds };
  232. } else {
  233. errors.notFound.comments.push(queryParams.comments);
  234. }
  235. }
  236. ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => {
  237. if (queryParams[field]) {
  238. selector[field] = {};
  239. selector[field][queryParams[field].operator] = new Date(
  240. queryParams[field].value,
  241. );
  242. }
  243. });
  244. const queryMembers = [];
  245. const queryAssignees = [];
  246. if (queryParams.users && queryParams.users.length) {
  247. queryParams.users.forEach(query => {
  248. const users = Users.find({
  249. username: query,
  250. });
  251. if (users.count()) {
  252. users.forEach(user => {
  253. queryMembers.push(user._id);
  254. queryAssignees.push(user._id);
  255. });
  256. } else {
  257. errors.notFound.users.push(query);
  258. }
  259. });
  260. }
  261. if (queryParams.members && queryParams.members.length) {
  262. queryParams.members.forEach(query => {
  263. const users = Users.find({
  264. username: query,
  265. });
  266. if (users.count()) {
  267. users.forEach(user => {
  268. queryMembers.push(user._id);
  269. });
  270. } else {
  271. errors.notFound.members.push(query);
  272. }
  273. });
  274. }
  275. if (queryParams.assignees && queryParams.assignees.length) {
  276. queryParams.assignees.forEach(query => {
  277. const users = Users.find({
  278. username: query,
  279. });
  280. if (users.count()) {
  281. users.forEach(user => {
  282. queryAssignees.push(user._id);
  283. });
  284. } else {
  285. errors.notFound.assignees.push(query);
  286. }
  287. });
  288. }
  289. if (queryMembers.length && queryAssignees.length) {
  290. selector.$and.push({
  291. $or: [
  292. { members: { $in: queryMembers } },
  293. { assignees: { $in: queryAssignees } },
  294. ],
  295. });
  296. } else if (queryMembers.length) {
  297. selector.members = { $in: queryMembers };
  298. } else if (queryAssignees.length) {
  299. selector.assignees = { $in: queryAssignees };
  300. }
  301. if (queryParams.labels && queryParams.labels.length) {
  302. queryParams.labels.forEach(label => {
  303. const queryLabels = [];
  304. let boards = Boards.userSearch(userId, {
  305. labels: { $elemMatch: { color: label.toLowerCase() } },
  306. });
  307. if (boards.count()) {
  308. boards.forEach(board => {
  309. // eslint-disable-next-line no-console
  310. // console.log('board:', board);
  311. // eslint-disable-next-line no-console
  312. // console.log('board.labels:', board.labels);
  313. board.labels
  314. .filter(boardLabel => {
  315. return boardLabel.color === label.toLowerCase();
  316. })
  317. .forEach(boardLabel => {
  318. queryLabels.push(boardLabel._id);
  319. });
  320. });
  321. } else {
  322. // eslint-disable-next-line no-console
  323. // console.log('label:', label);
  324. const reLabel = new RegExp(escapeForRegex(label), 'i');
  325. // eslint-disable-next-line no-console
  326. // console.log('reLabel:', reLabel);
  327. boards = Boards.userSearch(userId, {
  328. labels: { $elemMatch: { name: reLabel } },
  329. });
  330. if (boards.count()) {
  331. boards.forEach(board => {
  332. board.labels
  333. .filter(boardLabel => {
  334. return boardLabel.name.match(reLabel);
  335. })
  336. .forEach(boardLabel => {
  337. queryLabels.push(boardLabel._id);
  338. });
  339. });
  340. } else {
  341. errors.notFound.labels.push(label);
  342. }
  343. }
  344. selector.labelIds = { $in: queryLabels };
  345. });
  346. }
  347. if (queryParams.has && queryParams.has.length) {
  348. queryParams.has.forEach(has => {
  349. switch (has.field) {
  350. case 'attachment':
  351. selector.$and.push({
  352. _id: {
  353. $in: Attachments.find({}, { fields: { cardId: 1 } }).map(
  354. a => a.cardId,
  355. ),
  356. },
  357. });
  358. break;
  359. case 'checklist':
  360. selector.$and.push({
  361. _id: {
  362. $in: Checklists.find({}, { fields: { cardId: 1 } }).map(
  363. a => a.cardId,
  364. ),
  365. },
  366. });
  367. break;
  368. case 'description':
  369. case 'startAt':
  370. case 'dueAt':
  371. case 'endAt':
  372. if (has.exists) {
  373. selector[has.field] = { $exists: true, $nin: [null, ''] };
  374. } else {
  375. selector[has.field] = { $in: [null, ''] };
  376. }
  377. break;
  378. case 'assignees':
  379. case 'members':
  380. if (has.exists) {
  381. selector[has.field] = { $exists: true, $nin: [null, []] };
  382. } else {
  383. selector[has.field] = { $in: [null, []] };
  384. }
  385. break;
  386. }
  387. });
  388. }
  389. if (queryParams.text) {
  390. const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
  391. const items = ChecklistItems.find(
  392. { title: regex },
  393. { fields: { cardId: 1 } },
  394. );
  395. const checklists = Checklists.find(
  396. {
  397. $or: [
  398. { title: regex },
  399. { _id: { $in: items.map(item => item.checklistId) } },
  400. ],
  401. },
  402. { fields: { cardId: 1 } },
  403. );
  404. const attachments = Attachments.find({ 'original.name': regex });
  405. // const comments = CardComments.find(
  406. // { text: regex },
  407. // { fields: { cardId: 1 } },
  408. // );
  409. selector.$and.push({
  410. $or: [
  411. { title: regex },
  412. { description: regex },
  413. { customFields: { $elemMatch: { value: regex } } },
  414. {
  415. _id: {
  416. $in: CardComments.textSearch(userId, [queryParams.text]).map(
  417. com => com.cardId,
  418. ),
  419. },
  420. },
  421. { _id: { $in: checklists.map(list => list.cardId) } },
  422. { _id: { $in: attachments.map(attach => attach.cardId) } },
  423. // { _id: { $in: comments.map(com => com.cardId) } },
  424. ],
  425. });
  426. }
  427. if (selector.$and.length === 0) {
  428. delete selector.$and;
  429. }
  430. }
  431. // eslint-disable-next-line no-console
  432. // console.log('selector:', selector);
  433. // eslint-disable-next-line no-console
  434. // console.log('selector.$and:', selector.$and);
  435. const projection = {
  436. fields: {
  437. _id: 1,
  438. archived: 1,
  439. boardId: 1,
  440. swimlaneId: 1,
  441. listId: 1,
  442. title: 1,
  443. type: 1,
  444. sort: 1,
  445. members: 1,
  446. assignees: 1,
  447. colors: 1,
  448. dueAt: 1,
  449. createdAt: 1,
  450. modifiedAt: 1,
  451. labelIds: 1,
  452. customFields: 1,
  453. },
  454. sort: {
  455. boardId: 1,
  456. swimlaneId: 1,
  457. listId: 1,
  458. sort: 1,
  459. },
  460. skip,
  461. limit,
  462. };
  463. if (queryParams.sort) {
  464. const order = queryParams.sort.order === 'asc' ? 1 : -1;
  465. switch (queryParams.sort.name) {
  466. case 'dueAt':
  467. projection.sort = {
  468. dueAt: order,
  469. boardId: 1,
  470. swimlaneId: 1,
  471. listId: 1,
  472. sort: 1,
  473. };
  474. break;
  475. case 'modifiedAt':
  476. projection.sort = {
  477. modifiedAt: order,
  478. boardId: 1,
  479. swimlaneId: 1,
  480. listId: 1,
  481. sort: 1,
  482. };
  483. break;
  484. case 'createdAt':
  485. projection.sort = {
  486. createdAt: order,
  487. boardId: 1,
  488. swimlaneId: 1,
  489. listId: 1,
  490. sort: 1,
  491. };
  492. break;
  493. case 'system':
  494. projection.sort = {
  495. boardId: order,
  496. swimlaneId: order,
  497. listId: order,
  498. modifiedAt: order,
  499. sort: order,
  500. };
  501. break;
  502. }
  503. }
  504. // eslint-disable-next-line no-console
  505. // console.log('projection:', projection);
  506. return findCards(sessionId, selector, projection, errors);
  507. }
  508. Meteor.publish('brokenCards', function() {
  509. const user = Users.findOne({ _id: this.userId });
  510. const permiitedBoards = [null];
  511. let selector = {};
  512. selector.$or = [
  513. { permission: 'public' },
  514. { members: { $elemMatch: { userId: user._id, isActive: true } } },
  515. ];
  516. Boards.find(selector).forEach(board => {
  517. permiitedBoards.push(board._id);
  518. });
  519. selector = {
  520. boardId: { $in: permiitedBoards },
  521. $or: [
  522. { boardId: { $in: [null, ''] } },
  523. { swimlaneId: { $in: [null, ''] } },
  524. { listId: { $in: [null, ''] } },
  525. ],
  526. };
  527. const cards = Cards.find(selector, {
  528. fields: {
  529. _id: 1,
  530. archived: 1,
  531. boardId: 1,
  532. swimlaneId: 1,
  533. listId: 1,
  534. title: 1,
  535. type: 1,
  536. sort: 1,
  537. members: 1,
  538. assignees: 1,
  539. colors: 1,
  540. dueAt: 1,
  541. },
  542. });
  543. const boards = [];
  544. const swimlanes = [];
  545. const lists = [];
  546. const users = [];
  547. cards.forEach(card => {
  548. if (card.boardId) boards.push(card.boardId);
  549. if (card.swimlaneId) swimlanes.push(card.swimlaneId);
  550. if (card.listId) lists.push(card.listId);
  551. if (card.members) {
  552. card.members.forEach(userId => {
  553. users.push(userId);
  554. });
  555. }
  556. if (card.assignees) {
  557. card.assignees.forEach(userId => {
  558. users.push(userId);
  559. });
  560. }
  561. });
  562. return [
  563. cards,
  564. Boards.find({ _id: { $in: boards } }),
  565. Swimlanes.find({ _id: { $in: swimlanes } }),
  566. Lists.find({ _id: { $in: lists } }),
  567. Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
  568. ];
  569. });
  570. Meteor.publish('nextPage', function(sessionId) {
  571. check(sessionId, String);
  572. const session = SessionData.findOne({ sessionId });
  573. const projection = session.getProjection();
  574. projection.skip = session.lastHit;
  575. return findCards(sessionId, session.getSelector(), projection);
  576. });
  577. Meteor.publish('previousPage', function(sessionId) {
  578. check(sessionId, String);
  579. const session = SessionData.findOne({ sessionId });
  580. const projection = session.getProjection();
  581. projection.skip = session.lastHit - session.resultsCount - projection.limit;
  582. return findCards(sessionId, session.getSelector(), projection);
  583. });
  584. function findCards(sessionId, selector, projection, errors = null) {
  585. const userId = Meteor.userId();
  586. // eslint-disable-next-line no-console
  587. console.log('selector:', selector);
  588. console.log('selector.$and:', selector.$and);
  589. // eslint-disable-next-line no-console
  590. // console.log('projection:', projection);
  591. let cards;
  592. if (!errors || !errors.hasErrors()) {
  593. cards = Cards.find(selector, projection);
  594. }
  595. // eslint-disable-next-line no-console
  596. // console.log('count:', cards.count());
  597. const update = {
  598. $set: {
  599. totalHits: 0,
  600. lastHit: 0,
  601. resultsCount: 0,
  602. cards: [],
  603. selector: SessionData.pickle(selector),
  604. projection: SessionData.pickle(projection),
  605. },
  606. };
  607. if (errors) {
  608. update.$set.errors = errors.errorMessages();
  609. }
  610. if (cards) {
  611. update.$set.totalHits = cards.count();
  612. update.$set.lastHit =
  613. projection.skip + projection.limit < cards.count()
  614. ? projection.skip + projection.limit
  615. : cards.count();
  616. update.$set.cards = cards.map(card => {
  617. return card._id;
  618. });
  619. update.$set.resultsCount = update.$set.cards.length;
  620. }
  621. SessionData.upsert({ userId, sessionId }, update);
  622. // remove old session data
  623. SessionData.remove({
  624. userId,
  625. modifiedAt: {
  626. $lt: new Date(
  627. moment()
  628. .subtract(1, 'day')
  629. .format(),
  630. ),
  631. },
  632. });
  633. if (cards) {
  634. const boards = [];
  635. const swimlanes = [];
  636. const lists = [];
  637. const customFieldIds = [];
  638. const users = [this.userId];
  639. cards.forEach(card => {
  640. if (card.boardId) boards.push(card.boardId);
  641. if (card.swimlaneId) swimlanes.push(card.swimlaneId);
  642. if (card.listId) lists.push(card.listId);
  643. if (card.members) {
  644. card.members.forEach(userId => {
  645. users.push(userId);
  646. });
  647. }
  648. if (card.assignees) {
  649. card.assignees.forEach(userId => {
  650. users.push(userId);
  651. });
  652. }
  653. if (card.customFields) {
  654. card.customFields.forEach(field => {
  655. customFieldIds.push(field._id);
  656. });
  657. }
  658. });
  659. const fields = {
  660. _id: 1,
  661. title: 1,
  662. archived: 1,
  663. sort: 1,
  664. type: 1,
  665. };
  666. return [
  667. cards,
  668. Boards.find(
  669. { _id: { $in: boards } },
  670. { fields: { ...fields, labels: 1, color: 1 } },
  671. ),
  672. Swimlanes.find(
  673. { _id: { $in: swimlanes } },
  674. { fields: { ...fields, color: 1 } },
  675. ),
  676. Lists.find({ _id: { $in: lists } }, { fields }),
  677. CustomFields.find({ _id: { $in: customFieldIds } }),
  678. Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
  679. Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
  680. Attachments.find({ cardId: { $in: cards.map(c => c._id) } }),
  681. CardComments.find({ cardId: { $in: cards.map(c => c._id) } }),
  682. SessionData.find({ userId, sessionId }),
  683. ];
  684. }
  685. return [SessionData.find({ userId, sessionId })];
  686. }