cardComments.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import escapeForRegex from 'escape-string-regexp';
  2. import DOMPurify from 'dompurify';
  3. CardComments = new Mongo.Collection('card_comments');
  4. /**
  5. * A comment on a card
  6. */
  7. CardComments.attachSchema(
  8. new SimpleSchema({
  9. boardId: {
  10. /**
  11. * the board ID
  12. */
  13. type: String,
  14. },
  15. cardId: {
  16. /**
  17. * the card ID
  18. */
  19. type: String,
  20. },
  21. // XXX Rename in `content`? `text` is a bit vague...
  22. text: {
  23. /**
  24. * the text of the comment
  25. */
  26. type: String,
  27. },
  28. createdAt: {
  29. /**
  30. * when was the comment created
  31. */
  32. type: Date,
  33. denyUpdate: false,
  34. // eslint-disable-next-line consistent-return
  35. autoValue() {
  36. if (this.isInsert) {
  37. return new Date();
  38. } else if (this.isUpsert) {
  39. return { $setOnInsert: new Date() };
  40. } else {
  41. this.unset();
  42. }
  43. },
  44. },
  45. modifiedAt: {
  46. type: Date,
  47. denyUpdate: false,
  48. // eslint-disable-next-line consistent-return
  49. autoValue() {
  50. if (this.isInsert || this.isUpsert || this.isUpdate) {
  51. return new Date();
  52. } else {
  53. this.unset();
  54. }
  55. },
  56. },
  57. // XXX Should probably be called `authorId`
  58. userId: {
  59. /**
  60. * the author ID of the comment
  61. */
  62. type: String,
  63. // eslint-disable-next-line consistent-return
  64. autoValue() {
  65. if (this.isInsert && !this.isSet) {
  66. return this.userId;
  67. }
  68. },
  69. },
  70. }),
  71. );
  72. CardComments.allow({
  73. insert(userId, doc) {
  74. return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
  75. },
  76. update(userId, doc) {
  77. return userId === doc.userId || allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
  78. },
  79. remove(userId, doc) {
  80. return userId === doc.userId || allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
  81. },
  82. fetch: ['userId', 'boardId'],
  83. });
  84. CardComments.helpers({
  85. copy(newCardId) {
  86. this.cardId = newCardId;
  87. delete this._id;
  88. CardComments.insert(this);
  89. },
  90. user() {
  91. return Users.findOne(this.userId);
  92. },
  93. reactions() {
  94. const cardCommentReactions = CardCommentReactions.findOne({cardCommentId: this._id});
  95. return !!cardCommentReactions ? cardCommentReactions.reactions : [];
  96. },
  97. toggleReaction(reactionCodepoint) {
  98. if (reactionCodepoint !== DOMPurify.sanitize(reactionCodepoint)) {
  99. return false;
  100. } else {
  101. const cardCommentReactions = CardCommentReactions.findOne({cardCommentId: this._id});
  102. const reactions = !!cardCommentReactions ? cardCommentReactions.reactions : [];
  103. const userId = Meteor.userId();
  104. const reaction = reactions.find(r => r.reactionCodepoint === reactionCodepoint);
  105. // If no reaction is set for the codepoint, add this
  106. if (!reaction) {
  107. reactions.push({ reactionCodepoint, userIds: [userId] });
  108. } else {
  109. // toggle user reaction upon previous reaction state
  110. const userHasReacted = reaction.userIds.includes(userId);
  111. if (userHasReacted) {
  112. reaction.userIds.splice(reaction.userIds.indexOf(userId), 1);
  113. if (reaction.userIds.length === 0) {
  114. reactions.splice(reactions.indexOf(reaction), 1);
  115. }
  116. } else {
  117. reaction.userIds.push(userId);
  118. }
  119. }
  120. // If no reaction doc exists yet create otherwise update reaction set
  121. if (!!cardCommentReactions) {
  122. return CardCommentReactions.update({ _id: cardCommentReactions._id }, { $set: { reactions } });
  123. } else {
  124. return CardCommentReactions.insert({
  125. boardId: this.boardId,
  126. cardCommentId: this._id,
  127. cardId: this.cardId,
  128. reactions
  129. });
  130. }
  131. }
  132. }
  133. });
  134. CardComments.hookOptions.after.update = { fetchPrevious: false };
  135. function commentCreation(userId, doc) {
  136. const card = Cards.findOne(doc.cardId);
  137. Activities.insert({
  138. userId,
  139. activityType: 'addComment',
  140. boardId: doc.boardId,
  141. cardId: doc.cardId,
  142. commentId: doc._id,
  143. listId: card.listId,
  144. swimlaneId: card.swimlaneId,
  145. });
  146. }
  147. CardComments.textSearch = (userId, textArray) => {
  148. const selector = {
  149. boardId: { $in: Boards.userBoardIds(userId) },
  150. $and: [],
  151. };
  152. for (const text of textArray) {
  153. selector.$and.push({ text: new RegExp(escapeForRegex(text), 'i') });
  154. }
  155. // eslint-disable-next-line no-console
  156. // console.log('cardComments selector:', selector);
  157. const comments = CardComments.find(selector);
  158. // eslint-disable-next-line no-console
  159. // console.log('count:', comments.count());
  160. // eslint-disable-next-line no-console
  161. // console.log('cards with comments:', comments.map(com => { return com.cardId }));
  162. return comments;
  163. };
  164. if (Meteor.isServer) {
  165. // Comments are often fetched within a card, so we create an index to make these
  166. // queries more efficient.
  167. Meteor.startup(() => {
  168. CardComments._collection.createIndex({ modifiedAt: -1 });
  169. CardComments._collection.createIndex({ cardId: 1, createdAt: -1 });
  170. });
  171. CardComments.after.insert((userId, doc) => {
  172. commentCreation(userId, doc);
  173. });
  174. CardComments.after.update((userId, doc) => {
  175. const card = Cards.findOne(doc.cardId);
  176. Activities.insert({
  177. userId,
  178. activityType: 'editComment',
  179. boardId: doc.boardId,
  180. cardId: doc.cardId,
  181. commentId: doc._id,
  182. listId: card.listId,
  183. swimlaneId: card.swimlaneId,
  184. });
  185. });
  186. CardComments.before.remove((userId, doc) => {
  187. const card = Cards.findOne(doc.cardId);
  188. Activities.insert({
  189. userId,
  190. activityType: 'deleteComment',
  191. boardId: doc.boardId,
  192. cardId: doc.cardId,
  193. commentId: doc._id,
  194. listId: card.listId,
  195. swimlaneId: card.swimlaneId,
  196. });
  197. const activity = Activities.findOne({ commentId: doc._id });
  198. if (activity) {
  199. Activities.remove(activity._id);
  200. }
  201. });
  202. }
  203. //CARD COMMENT REST API
  204. if (Meteor.isServer) {
  205. /**
  206. * @operation get_all_comments
  207. * @summary Get all comments attached to a card
  208. *
  209. * @param {string} boardId the board ID of the card
  210. * @param {string} cardId the ID of the card
  211. * @return_type [{_id: string,
  212. * comment: string,
  213. * authorId: string}]
  214. */
  215. JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (
  216. req,
  217. res,
  218. ) {
  219. try {
  220. const paramBoardId = req.params.boardId;
  221. const paramCardId = req.params.cardId;
  222. Authentication.checkBoardAccess(req.userId, paramBoardId);
  223. JsonRoutes.sendResult(res, {
  224. code: 200,
  225. data: CardComments.find({
  226. boardId: paramBoardId,
  227. cardId: paramCardId,
  228. }).map(function (doc) {
  229. return {
  230. _id: doc._id,
  231. comment: doc.text,
  232. authorId: doc.userId,
  233. };
  234. }),
  235. });
  236. } catch (error) {
  237. JsonRoutes.sendResult(res, {
  238. code: 200,
  239. data: error,
  240. });
  241. }
  242. });
  243. /**
  244. * @operation get_comment
  245. * @summary Get a comment on a card
  246. *
  247. * @param {string} boardId the board ID of the card
  248. * @param {string} cardId the ID of the card
  249. * @param {string} commentId the ID of the comment to retrieve
  250. * @return_type CardComments
  251. */
  252. JsonRoutes.add(
  253. 'GET',
  254. '/api/boards/:boardId/cards/:cardId/comments/:commentId',
  255. function (req, res) {
  256. try {
  257. const paramBoardId = req.params.boardId;
  258. const paramCommentId = req.params.commentId;
  259. const paramCardId = req.params.cardId;
  260. Authentication.checkBoardAccess(req.userId, paramBoardId);
  261. JsonRoutes.sendResult(res, {
  262. code: 200,
  263. data: CardComments.findOne({
  264. _id: paramCommentId,
  265. cardId: paramCardId,
  266. boardId: paramBoardId,
  267. }),
  268. });
  269. } catch (error) {
  270. JsonRoutes.sendResult(res, {
  271. code: 200,
  272. data: error,
  273. });
  274. }
  275. },
  276. );
  277. /**
  278. * @operation new_comment
  279. * @summary Add a comment on a card
  280. *
  281. * @param {string} boardId the board ID of the card
  282. * @param {string} cardId the ID of the card
  283. * @param {string} authorId the user who 'posted' the comment
  284. * @param {string} text the content of the comment
  285. * @return_type {_id: string}
  286. */
  287. JsonRoutes.add(
  288. 'POST',
  289. '/api/boards/:boardId/cards/:cardId/comments',
  290. function (req, res) {
  291. try {
  292. const paramBoardId = req.params.boardId;
  293. const paramCardId = req.params.cardId;
  294. Authentication.checkBoardAccess(req.userId, paramBoardId);
  295. const id = CardComments.direct.insert({
  296. userId: req.body.authorId,
  297. text: req.body.comment,
  298. cardId: paramCardId,
  299. boardId: paramBoardId,
  300. });
  301. JsonRoutes.sendResult(res, {
  302. code: 200,
  303. data: {
  304. _id: id,
  305. },
  306. });
  307. const cardComment = CardComments.findOne({
  308. _id: id,
  309. cardId: paramCardId,
  310. boardId: paramBoardId,
  311. });
  312. commentCreation(req.body.authorId, cardComment);
  313. } catch (error) {
  314. JsonRoutes.sendResult(res, {
  315. code: 200,
  316. data: error,
  317. });
  318. }
  319. },
  320. );
  321. /**
  322. * @operation delete_comment
  323. * @summary Delete a comment on a card
  324. *
  325. * @param {string} boardId the board ID of the card
  326. * @param {string} cardId the ID of the card
  327. * @param {string} commentId the ID of the comment to delete
  328. * @return_type {_id: string}
  329. */
  330. JsonRoutes.add(
  331. 'DELETE',
  332. '/api/boards/:boardId/cards/:cardId/comments/:commentId',
  333. function (req, res) {
  334. try {
  335. const paramBoardId = req.params.boardId;
  336. const paramCommentId = req.params.commentId;
  337. const paramCardId = req.params.cardId;
  338. Authentication.checkBoardAccess(req.userId, paramBoardId);
  339. CardComments.remove({
  340. _id: paramCommentId,
  341. cardId: paramCardId,
  342. boardId: paramBoardId,
  343. });
  344. JsonRoutes.sendResult(res, {
  345. code: 200,
  346. data: {
  347. _id: paramCardId,
  348. },
  349. });
  350. } catch (error) {
  351. JsonRoutes.sendResult(res, {
  352. code: 200,
  353. data: error,
  354. });
  355. }
  356. },
  357. );
  358. }
  359. export default CardComments;