mirror of
https://github.com/wekan/wekan.git
synced 2025-04-23 13:37:09 -04:00
adds card comment reactions feature
This commit is contained in:
parent
d8e8512d66
commit
2977120129
8 changed files with 239 additions and 10 deletions
|
@ -122,6 +122,7 @@
|
|||
"Activities": true,
|
||||
"Attachments": true,
|
||||
"Boards": true,
|
||||
"CardCommentReactions": true,
|
||||
"CardComments": true,
|
||||
"DatePicker": true,
|
||||
"Cards": true,
|
||||
|
|
|
@ -21,6 +21,22 @@ template(name="editOrDeleteComment")
|
|||
= ' - '
|
||||
a.js-delete-comment {{_ "delete"}}
|
||||
|
||||
template(name="commentReactions")
|
||||
.reactions
|
||||
each reaction in reactions
|
||||
span.reaction(class="{{#if isSelected reaction.userIds}}selected{{/if}}" data-codepoint="#{reaction.reactionCodepoint}" title="{{userNames reaction.userIds}}")
|
||||
span.reaction-codepoint !{reaction.reactionCodepoint}
|
||||
span.reaction-count #{reaction.userIds.length}
|
||||
if (currentUser.isBoardMember)
|
||||
a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
|
||||
i.fa.fa-smile-o
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="addReactionPopup")
|
||||
.reactions-popup
|
||||
each codepoint in codepoints
|
||||
span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}
|
||||
|
||||
template(name="activity")
|
||||
.activity
|
||||
+userAvatar(userId=activity.user._id)
|
||||
|
@ -124,6 +140,7 @@ template(name="activity")
|
|||
.activity-comment
|
||||
+viewer
|
||||
= activity.comment.text
|
||||
+commentReactions(reactions=activity.comment.reactions commentId=activity.comment._id)
|
||||
span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
if($eq currentUser._id activity.comment.userId)
|
||||
+editOrDeleteComment
|
||||
|
@ -150,20 +167,20 @@ template(name="activity")
|
|||
|
||||
if($eq activity.activityType 'a-startAt')
|
||||
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
|
||||
|
||||
|
||||
if($eq activity.activityType 'a-dueAt')
|
||||
| {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'a-endAt')
|
||||
| {{{_ 'activity-endDate' (sanitize endDate) cardLink}}}.
|
||||
|
||||
|
||||
if($eq mode 'board')
|
||||
if($eq activity.activityType 'a-receivedAt')
|
||||
| {{{_ 'activity-receivedDate' (sanitize receivedDate) cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'a-startAt')
|
||||
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
|
||||
|
||||
|
||||
if($eq activity.activityType 'a-dueAt')
|
||||
| {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
|
||||
|
||||
|
|
|
@ -240,6 +240,59 @@ Template.activity.helpers({
|
|||
},
|
||||
});
|
||||
|
||||
Template.commentReactions.events({
|
||||
'click .reaction'(event) {
|
||||
if (Meteor.user().isBoardMember()) {
|
||||
const codepoint = event.currentTarget.dataset['codepoint'];
|
||||
const commentId = Template.instance().data.commentId;
|
||||
const cardComment = CardComments.findOne({_id: commentId});
|
||||
cardComment.toggleReaction(codepoint);
|
||||
}
|
||||
},
|
||||
'click .open-comment-reaction-popup': Popup.open('addReaction'),
|
||||
})
|
||||
|
||||
Template.addReactionPopup.events({
|
||||
'click .add-comment-reaction'(event) {
|
||||
if (Meteor.user().isBoardMember()) {
|
||||
const codepoint = event.currentTarget.dataset['codepoint'];
|
||||
const commentId = Template.instance().data.commentId;
|
||||
const cardComment = CardComments.findOne({_id: commentId});
|
||||
cardComment.toggleReaction(codepoint);
|
||||
}
|
||||
Popup.close();
|
||||
},
|
||||
})
|
||||
|
||||
Template.addReactionPopup.helpers({
|
||||
codepoints() {
|
||||
return [
|
||||
'👍',
|
||||
'👎',
|
||||
'👀',
|
||||
'✅',
|
||||
'❌',
|
||||
'🙏',
|
||||
'👏',
|
||||
'🎉',
|
||||
'🚀',
|
||||
'😊',
|
||||
'🤔',
|
||||
'😔'];
|
||||
}
|
||||
})
|
||||
|
||||
Template.commentReactions.helpers({
|
||||
isSelected(userIds) {
|
||||
return userIds.includes(Meteor.user()._id);
|
||||
},
|
||||
userNames(userIds) {
|
||||
return Users.find({_id: {$in: userIds}})
|
||||
.map(user => user.profile.fullname)
|
||||
.join(', ');
|
||||
}
|
||||
})
|
||||
|
||||
function createCardLink(card) {
|
||||
if (!card) return '';
|
||||
return (
|
||||
|
|
|
@ -5,6 +5,20 @@
|
|||
display: flex
|
||||
justify-content:space-between
|
||||
|
||||
.reactions-popup
|
||||
.add-comment-reaction
|
||||
display: inline-block
|
||||
cursor: pointer
|
||||
border-radius: 5px
|
||||
font-size: 22px
|
||||
text-align: center
|
||||
line-height: 30px
|
||||
width: 40px
|
||||
|
||||
&:hover {
|
||||
background-color: #b0c4de
|
||||
}
|
||||
|
||||
.activities
|
||||
clear: both
|
||||
|
||||
|
@ -18,7 +32,7 @@
|
|||
height: @width
|
||||
|
||||
.activity-member
|
||||
font-weight: 700
|
||||
font-weight: 700
|
||||
|
||||
.activity-desc
|
||||
word-wrap: break-word
|
||||
|
@ -39,6 +53,45 @@
|
|||
margin-top: 5px
|
||||
padding: 5px
|
||||
|
||||
.reactions
|
||||
display: flex
|
||||
margin-top: 5px
|
||||
gap: 5px
|
||||
|
||||
.open-comment-reaction-popup
|
||||
display: flex
|
||||
align-items: center
|
||||
text-decoration: none
|
||||
height: 24px;
|
||||
|
||||
i.fa.fa-smile-o
|
||||
font-size: 17px
|
||||
font-weight: 500
|
||||
margin-left: 2px
|
||||
|
||||
i.fa.fa-plus
|
||||
font-size: 8px;
|
||||
margin-top: -7px;
|
||||
margin-left: 1px;
|
||||
|
||||
.reaction
|
||||
cursor: pointer
|
||||
border: 1px solid grey
|
||||
border-radius: 15px
|
||||
display: flex
|
||||
padding: 2px 5px
|
||||
|
||||
&.selected {
|
||||
background-color: #b0c4de
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #b0c4de
|
||||
}
|
||||
|
||||
.reaction-count
|
||||
font-size: 12px
|
||||
|
||||
.activity-checklist
|
||||
display: block
|
||||
border-radius: 3px
|
||||
|
|
59
models/cardCommentReactions.js
Normal file
59
models/cardCommentReactions.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const commentReactionSchema = new SimpleSchema({
|
||||
reactionCodepoint: { type: String, optional: false },
|
||||
userIds: { type: [String], defaultValue: [] }
|
||||
});
|
||||
|
||||
CardCommentReactions = new Mongo.Collection('card_comment_reactions');
|
||||
|
||||
/**
|
||||
* All reactions of a card comment
|
||||
*/
|
||||
CardCommentReactions.attachSchema(
|
||||
new SimpleSchema({
|
||||
boardId: {
|
||||
/**
|
||||
* the board ID
|
||||
*/
|
||||
type: String,
|
||||
optional: false
|
||||
},
|
||||
cardId: {
|
||||
/**
|
||||
* the card ID
|
||||
*/
|
||||
type: String,
|
||||
optional: false
|
||||
},
|
||||
cardCommentId: {
|
||||
/**
|
||||
* the card comment ID
|
||||
*/
|
||||
type: String,
|
||||
optional: false
|
||||
},
|
||||
reactions: {
|
||||
type: [commentReactionSchema],
|
||||
defaultValue: []
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
CardCommentReactions.allow({
|
||||
insert(userId, doc) {
|
||||
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
||||
},
|
||||
update(userId, doc) {
|
||||
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
|
||||
},
|
||||
fetch: ['boardId'],
|
||||
});
|
||||
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Meteor.startup(() => {
|
||||
CardCommentReactions._collection._ensureIndex({ cardCommentId: 1 }, { unique: true });
|
||||
});
|
||||
}
|
|
@ -93,6 +93,43 @@ CardComments.helpers({
|
|||
user() {
|
||||
return Users.findOne(this.userId);
|
||||
},
|
||||
|
||||
reactions() {
|
||||
const cardCommentReactions = CardCommentReactions.findOne({cardCommentId: this._id});
|
||||
return !!cardCommentReactions ? cardCommentReactions.reactions : [];
|
||||
},
|
||||
|
||||
toggleReaction(reactionCodepoint) {
|
||||
|
||||
const cardCommentReactions = CardCommentReactions.findOne({cardCommentId: this._id});
|
||||
const reactions = !!cardCommentReactions ? cardCommentReactions.reactions : [];
|
||||
const userId = Meteor.userId();
|
||||
const reaction = reactions.find(r => r.reactionCodepoint === reactionCodepoint);
|
||||
|
||||
if (!reaction) {
|
||||
reactions.push({ reactionCodepoint, userIds: [userId] });
|
||||
} else {
|
||||
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 (!!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 };
|
||||
|
@ -187,7 +224,7 @@ if (Meteor.isServer) {
|
|||
* comment: string,
|
||||
* authorId: string}]
|
||||
*/
|
||||
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function(
|
||||
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (
|
||||
req,
|
||||
res,
|
||||
) {
|
||||
|
@ -200,7 +237,7 @@ if (Meteor.isServer) {
|
|||
data: CardComments.find({
|
||||
boardId: paramBoardId,
|
||||
cardId: paramCardId,
|
||||
}).map(function(doc) {
|
||||
}).map(function (doc) {
|
||||
return {
|
||||
_id: doc._id,
|
||||
comment: doc.text,
|
||||
|
@ -228,7 +265,7 @@ if (Meteor.isServer) {
|
|||
JsonRoutes.add(
|
||||
'GET',
|
||||
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
||||
function(req, res) {
|
||||
function (req, res) {
|
||||
try {
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
@ -264,7 +301,7 @@ if (Meteor.isServer) {
|
|||
JsonRoutes.add(
|
||||
'POST',
|
||||
'/api/boards/:boardId/cards/:cardId/comments',
|
||||
function(req, res) {
|
||||
function (req, res) {
|
||||
try {
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
@ -310,7 +347,7 @@ if (Meteor.isServer) {
|
|||
JsonRoutes.add(
|
||||
'DELETE',
|
||||
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
||||
function(req, res) {
|
||||
function (req, res) {
|
||||
try {
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
|
|
@ -129,6 +129,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
|||
this.cursor(Lists.find({ boardId, archived: isArchived }));
|
||||
this.cursor(Swimlanes.find({ boardId, archived: isArchived }));
|
||||
this.cursor(Integrations.find({ boardId }));
|
||||
this.cursor(CardCommentReactions.find({ boardId }));
|
||||
this.cursor(
|
||||
CustomFields.find(
|
||||
{ boardIds: { $in: [boardId] } },
|
||||
|
@ -161,6 +162,8 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
|||
// Gather queries and send in bulk
|
||||
const cardComments = this.join(CardComments);
|
||||
cardComments.selector = _ids => ({ cardId: _ids });
|
||||
const cardCommentReactions = this.join(CardCommentReactions);
|
||||
cardCommentReactions.selector = _ids => ({ cardId: _ids });
|
||||
const attachments = this.join(Attachments);
|
||||
attachments.selector = _ids => ({ cardId: _ids });
|
||||
const checklists = this.join(Checklists);
|
||||
|
@ -194,12 +197,14 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
|||
checklists.push(cardId);
|
||||
checklistItems.push(cardId);
|
||||
parentCards.push(cardId);
|
||||
cardCommentReactions.push(cardId)
|
||||
},
|
||||
);
|
||||
|
||||
// Send bulk queries for all found ids
|
||||
subCards.send();
|
||||
cardComments.send();
|
||||
cardCommentReactions.send();
|
||||
attachments.send();
|
||||
checklists.send();
|
||||
checklistItems.send();
|
||||
|
|
|
@ -5,6 +5,7 @@ import Lists from '../../models/lists';
|
|||
import Swimlanes from '../../models/swimlanes';
|
||||
import Cards from '../../models/cards';
|
||||
import CardComments from '../../models/cardComments';
|
||||
import CardCommentReactions from '../../models/cardCommentReactions';
|
||||
import Attachments from '../../models/attachments';
|
||||
import Checklists from '../../models/checklists';
|
||||
import ChecklistItems from '../../models/checklistItems';
|
||||
|
@ -699,6 +700,8 @@ function findCards(sessionId, query) {
|
|||
type: 1,
|
||||
};
|
||||
|
||||
const comments = CardComments.find({ cardId: { $in: cards.map(c => c._id) } });
|
||||
|
||||
return [
|
||||
cards,
|
||||
Boards.find(
|
||||
|
@ -714,7 +717,8 @@ function findCards(sessionId, query) {
|
|||
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||
Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
|
||||
Attachments.find({ cardId: { $in: cards.map(c => c._id) } }),
|
||||
CardComments.find({ cardId: { $in: cards.map(c => c._id) } }),
|
||||
comments,
|
||||
CardCommentReactions.find({cardCommentId: {$in: comments.map(c => c._id) }}),
|
||||
SessionData.find({ userId, sessionId }),
|
||||
];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue