import { ReactiveCache } from '/imports/reactiveCache'; import escapeForRegex from 'escape-string-regexp'; import DOMPurify from 'dompurify'; CardComments = new Mongo.Collection('card_comments'); /** * A comment on a card */ CardComments.attachSchema( new SimpleSchema({ boardId: { /** * the board ID */ type: String, }, cardId: { /** * the card ID */ type: String, }, // XXX Rename in `content`? `text` is a bit vague... text: { /** * the text of the comment */ type: String, }, createdAt: { /** * when was the comment created */ type: Date, denyUpdate: false, // eslint-disable-next-line consistent-return autoValue() { if (this.isInsert) { return new Date(); } else if (this.isUpsert) { return { $setOnInsert: new Date() }; } else { this.unset(); } }, }, modifiedAt: { type: Date, denyUpdate: false, // eslint-disable-next-line consistent-return autoValue() { if (this.isInsert || this.isUpsert || this.isUpdate) { return new Date(); } else { this.unset(); } }, }, // XXX Should probably be called `authorId` userId: { /** * the author ID of the comment */ type: String, // eslint-disable-next-line consistent-return autoValue() { if (this.isInsert && !this.isSet) { return this.userId; } }, }, }), ); CardComments.allow({ insert(userId, doc) { return allowIsBoardMember(userId, ReactiveCache.getBoard(doc.boardId)); }, update(userId, doc) { return userId === doc.userId || allowIsBoardAdmin(userId, ReactiveCache.getBoard(doc.boardId)); }, remove(userId, doc) { return userId === doc.userId || allowIsBoardAdmin(userId, ReactiveCache.getBoard(doc.boardId)); }, fetch: ['userId', 'boardId'], }); CardComments.helpers({ copy(newCardId) { this.cardId = newCardId; delete this._id; CardComments.insert(this); }, user() { return ReactiveCache.getUser(this.userId); }, reactions() { const cardCommentReactions = ReactiveCache.getCardCommentReaction({cardCommentId: this._id}); return !!cardCommentReactions ? cardCommentReactions.reactions : []; }, toggleReaction(reactionCodepoint) { if (reactionCodepoint !== DOMPurify.sanitize(reactionCodepoint)) { return false; } else { const cardCommentReactions = ReactiveCache.getCardCommentReaction({cardCommentId: this._id}); const reactions = !!cardCommentReactions ? cardCommentReactions.reactions : []; const userId = Meteor.userId(); const reaction = reactions.find(r => r.reactionCodepoint === reactionCodepoint); // If no reaction is set for the codepoint, add this if (!reaction) { reactions.push({ reactionCodepoint, userIds: [userId] }); } else { // toggle user reaction upon previous reaction state const userHasReacted = reaction.userIds.includes(userId); if (userHasReacted) { reaction.userIds.splice(reaction.userIds.indexOf(userId), 1); if (reaction.userIds.length === 0) { reactions.splice(reactions.indexOf(reaction), 1); } } else { reaction.userIds.push(userId); } } // If no reaction doc exists yet create otherwise update reaction set if (!!cardCommentReactions) { return CardCommentReactions.update({ _id: cardCommentReactions._id }, { $set: { reactions } }); } else { return CardCommentReactions.insert({ boardId: this.boardId, cardCommentId: this._id, cardId: this.cardId, reactions }); } } } }); CardComments.hookOptions.after.update = { fetchPrevious: false }; function commentCreation(userId, doc) { const card = ReactiveCache.getCard(doc.cardId); Activities.insert({ userId, activityType: 'addComment', boardId: doc.boardId, cardId: doc.cardId, commentId: doc._id, listId: card.listId, swimlaneId: card.swimlaneId, }); } CardComments.textSearch = (userId, textArray) => { const selector = { boardId: { $in: Boards.userBoardIds(userId) }, $and: [], }; for (const text of textArray) { selector.$and.push({ text: new RegExp(escapeForRegex(text), 'i') }); } // eslint-disable-next-line no-console // console.log('cardComments selector:', selector); const comments = ReactiveCache.getCardComments(selector); // eslint-disable-next-line no-console // console.log('count:', comments.count()); // eslint-disable-next-line no-console // console.log('cards with comments:', comments.map(com => { return com.cardId })); return comments; }; if (Meteor.isServer) { // Comments are often fetched within a card, so we create an index to make these // queries more efficient. Meteor.startup(() => { CardComments._collection.createIndex({ modifiedAt: -1 }); CardComments._collection.createIndex({ cardId: 1, createdAt: -1 }); }); CardComments.after.insert((userId, doc) => { commentCreation(userId, doc); }); CardComments.after.update((userId, doc) => { const card = ReactiveCache.getCard(doc.cardId); Activities.insert({ userId, activityType: 'editComment', boardId: doc.boardId, cardId: doc.cardId, commentId: doc._id, listId: card.listId, swimlaneId: card.swimlaneId, }); }); CardComments.before.remove((userId, doc) => { const card = ReactiveCache.getCard(doc.cardId); Activities.insert({ userId, activityType: 'deleteComment', boardId: doc.boardId, cardId: doc.cardId, commentId: doc._id, listId: card.listId, swimlaneId: card.swimlaneId, }); const activity = ReactiveCache.getActivity({ commentId: doc._id }); if (activity) { Activities.remove(activity._id); } }); } //CARD COMMENT REST API if (Meteor.isServer) { /** * @operation get_all_comments * @summary Get all comments attached to a card * * @param {string} boardId the board ID of the card * @param {string} cardId the ID of the card * @return_type [{_id: string, * comment: string, * authorId: string}] */ JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function ( req, res, ) { try { const paramBoardId = req.params.boardId; const paramCardId = req.params.cardId; Authentication.checkBoardAccess(req.userId, paramBoardId); JsonRoutes.sendResult(res, { code: 200, data: ReactiveCache.getCardComments({ boardId: paramBoardId, cardId: paramCardId, }).map(function (doc) { return { _id: doc._id, comment: doc.text, authorId: doc.userId, }; }), }); } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, }); } }); /** * @operation get_comment * @summary Get a comment on a card * * @param {string} boardId the board ID of the card * @param {string} cardId the ID of the card * @param {string} commentId the ID of the comment to retrieve * @return_type CardComments */ JsonRoutes.add( 'GET', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { try { const paramBoardId = req.params.boardId; const paramCommentId = req.params.commentId; const paramCardId = req.params.cardId; Authentication.checkBoardAccess(req.userId, paramBoardId); JsonRoutes.sendResult(res, { code: 200, data: ReactiveCache.getCardComment({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId, }), }); } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, }); } }, ); /** * @operation new_comment * @summary Add a comment on a card * * @param {string} boardId the board ID of the card * @param {string} cardId the ID of the card * @param {string} authorId the user who 'posted' the comment * @param {string} text the content of the comment * @return_type {_id: string} */ JsonRoutes.add( 'POST', '/api/boards/:boardId/cards/:cardId/comments', function (req, res) { try { const paramBoardId = req.params.boardId; const paramCardId = req.params.cardId; Authentication.checkBoardAccess(req.userId, paramBoardId); const id = CardComments.direct.insert({ userId: req.body.authorId, text: req.body.comment, cardId: paramCardId, boardId: paramBoardId, }); JsonRoutes.sendResult(res, { code: 200, data: { _id: id, }, }); const cardComment = ReactiveCache.getCardComment({ _id: id, cardId: paramCardId, boardId: paramBoardId, }); commentCreation(req.body.authorId, cardComment); } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, }); } }, ); /** * @operation delete_comment * @summary Delete a comment on a card * * @param {string} boardId the board ID of the card * @param {string} cardId the ID of the card * @param {string} commentId the ID of the comment to delete * @return_type {_id: string} */ JsonRoutes.add( 'DELETE', '/api/boards/:boardId/cards/:cardId/comments/:commentId', function (req, res) { try { const paramBoardId = req.params.boardId; const paramCommentId = req.params.commentId; const paramCardId = req.params.cardId; Authentication.checkBoardAccess(req.userId, paramBoardId); CardComments.remove({ _id: paramCommentId, cardId: paramCardId, boardId: paramBoardId, }); JsonRoutes.sendResult(res, { code: 200, data: { _id: paramCardId, }, }); } catch (error) { JsonRoutes.sendResult(res, { code: 200, data: error, }); } }, ); } export default CardComments;