mirror of
https://github.com/wekan/wekan.git
synced 2026-01-21 08:46:09 +01: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,
|
"Activities": true,
|
||||||
"Attachments": true,
|
"Attachments": true,
|
||||||
"Boards": true,
|
"Boards": true,
|
||||||
|
"CardCommentReactions": true,
|
||||||
"CardComments": true,
|
"CardComments": true,
|
||||||
"DatePicker": true,
|
"DatePicker": true,
|
||||||
"Cards": true,
|
"Cards": true,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,22 @@ template(name="editOrDeleteComment")
|
||||||
= ' - '
|
= ' - '
|
||||||
a.js-delete-comment {{_ "delete"}}
|
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")
|
template(name="activity")
|
||||||
.activity
|
.activity
|
||||||
+userAvatar(userId=activity.user._id)
|
+userAvatar(userId=activity.user._id)
|
||||||
|
|
@ -124,6 +140,7 @@ template(name="activity")
|
||||||
.activity-comment
|
.activity-comment
|
||||||
+viewer
|
+viewer
|
||||||
= activity.comment.text
|
= activity.comment.text
|
||||||
|
+commentReactions(reactions=activity.comment.reactions commentId=activity.comment._id)
|
||||||
span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||||
if($eq currentUser._id activity.comment.userId)
|
if($eq currentUser._id activity.comment.userId)
|
||||||
+editOrDeleteComment
|
+editOrDeleteComment
|
||||||
|
|
@ -150,20 +167,20 @@ template(name="activity")
|
||||||
|
|
||||||
if($eq activity.activityType 'a-startAt')
|
if($eq activity.activityType 'a-startAt')
|
||||||
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
|
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
|
||||||
|
|
||||||
if($eq activity.activityType 'a-dueAt')
|
if($eq activity.activityType 'a-dueAt')
|
||||||
| {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
|
| {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
|
||||||
|
|
||||||
if($eq activity.activityType 'a-endAt')
|
if($eq activity.activityType 'a-endAt')
|
||||||
| {{{_ 'activity-endDate' (sanitize endDate) cardLink}}}.
|
| {{{_ 'activity-endDate' (sanitize endDate) cardLink}}}.
|
||||||
|
|
||||||
if($eq mode 'board')
|
if($eq mode 'board')
|
||||||
if($eq activity.activityType 'a-receivedAt')
|
if($eq activity.activityType 'a-receivedAt')
|
||||||
| {{{_ 'activity-receivedDate' (sanitize receivedDate) cardLink}}}.
|
| {{{_ 'activity-receivedDate' (sanitize receivedDate) cardLink}}}.
|
||||||
|
|
||||||
if($eq activity.activityType 'a-startAt')
|
if($eq activity.activityType 'a-startAt')
|
||||||
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
|
| {{{_ 'activity-startDate' (sanitize startDate) cardLink}}}.
|
||||||
|
|
||||||
if($eq activity.activityType 'a-dueAt')
|
if($eq activity.activityType 'a-dueAt')
|
||||||
| {{{_ 'activity-dueDate' (sanitize dueDate) cardLink}}}.
|
| {{{_ '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) {
|
function createCardLink(card) {
|
||||||
if (!card) return '';
|
if (!card) return '';
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,20 @@
|
||||||
display: flex
|
display: flex
|
||||||
justify-content:space-between
|
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
|
.activities
|
||||||
clear: both
|
clear: both
|
||||||
|
|
||||||
|
|
@ -18,7 +32,7 @@
|
||||||
height: @width
|
height: @width
|
||||||
|
|
||||||
.activity-member
|
.activity-member
|
||||||
font-weight: 700
|
font-weight: 700
|
||||||
|
|
||||||
.activity-desc
|
.activity-desc
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
|
|
@ -39,6 +53,45 @@
|
||||||
margin-top: 5px
|
margin-top: 5px
|
||||||
padding: 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
|
.activity-checklist
|
||||||
display: block
|
display: block
|
||||||
border-radius: 3px
|
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() {
|
user() {
|
||||||
return Users.findOne(this.userId);
|
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 };
|
CardComments.hookOptions.after.update = { fetchPrevious: false };
|
||||||
|
|
@ -187,7 +224,7 @@ if (Meteor.isServer) {
|
||||||
* comment: string,
|
* comment: string,
|
||||||
* authorId: string}]
|
* authorId: string}]
|
||||||
*/
|
*/
|
||||||
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function(
|
JsonRoutes.add('GET', '/api/boards/:boardId/cards/:cardId/comments', function (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
) {
|
) {
|
||||||
|
|
@ -200,7 +237,7 @@ if (Meteor.isServer) {
|
||||||
data: CardComments.find({
|
data: CardComments.find({
|
||||||
boardId: paramBoardId,
|
boardId: paramBoardId,
|
||||||
cardId: paramCardId,
|
cardId: paramCardId,
|
||||||
}).map(function(doc) {
|
}).map(function (doc) {
|
||||||
return {
|
return {
|
||||||
_id: doc._id,
|
_id: doc._id,
|
||||||
comment: doc.text,
|
comment: doc.text,
|
||||||
|
|
@ -228,7 +265,7 @@ if (Meteor.isServer) {
|
||||||
JsonRoutes.add(
|
JsonRoutes.add(
|
||||||
'GET',
|
'GET',
|
||||||
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
||||||
function(req, res) {
|
function (req, res) {
|
||||||
try {
|
try {
|
||||||
const paramBoardId = req.params.boardId;
|
const paramBoardId = req.params.boardId;
|
||||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||||
|
|
@ -264,7 +301,7 @@ if (Meteor.isServer) {
|
||||||
JsonRoutes.add(
|
JsonRoutes.add(
|
||||||
'POST',
|
'POST',
|
||||||
'/api/boards/:boardId/cards/:cardId/comments',
|
'/api/boards/:boardId/cards/:cardId/comments',
|
||||||
function(req, res) {
|
function (req, res) {
|
||||||
try {
|
try {
|
||||||
const paramBoardId = req.params.boardId;
|
const paramBoardId = req.params.boardId;
|
||||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||||
|
|
@ -310,7 +347,7 @@ if (Meteor.isServer) {
|
||||||
JsonRoutes.add(
|
JsonRoutes.add(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
||||||
function(req, res) {
|
function (req, res) {
|
||||||
try {
|
try {
|
||||||
const paramBoardId = req.params.boardId;
|
const paramBoardId = req.params.boardId;
|
||||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
||||||
this.cursor(Lists.find({ boardId, archived: isArchived }));
|
this.cursor(Lists.find({ boardId, archived: isArchived }));
|
||||||
this.cursor(Swimlanes.find({ boardId, archived: isArchived }));
|
this.cursor(Swimlanes.find({ boardId, archived: isArchived }));
|
||||||
this.cursor(Integrations.find({ boardId }));
|
this.cursor(Integrations.find({ boardId }));
|
||||||
|
this.cursor(CardCommentReactions.find({ boardId }));
|
||||||
this.cursor(
|
this.cursor(
|
||||||
CustomFields.find(
|
CustomFields.find(
|
||||||
{ boardIds: { $in: [boardId] } },
|
{ boardIds: { $in: [boardId] } },
|
||||||
|
|
@ -161,6 +162,8 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
||||||
// Gather queries and send in bulk
|
// Gather queries and send in bulk
|
||||||
const cardComments = this.join(CardComments);
|
const cardComments = this.join(CardComments);
|
||||||
cardComments.selector = _ids => ({ cardId: _ids });
|
cardComments.selector = _ids => ({ cardId: _ids });
|
||||||
|
const cardCommentReactions = this.join(CardCommentReactions);
|
||||||
|
cardCommentReactions.selector = _ids => ({ cardId: _ids });
|
||||||
const attachments = this.join(Attachments);
|
const attachments = this.join(Attachments);
|
||||||
attachments.selector = _ids => ({ cardId: _ids });
|
attachments.selector = _ids => ({ cardId: _ids });
|
||||||
const checklists = this.join(Checklists);
|
const checklists = this.join(Checklists);
|
||||||
|
|
@ -194,12 +197,14 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
|
||||||
checklists.push(cardId);
|
checklists.push(cardId);
|
||||||
checklistItems.push(cardId);
|
checklistItems.push(cardId);
|
||||||
parentCards.push(cardId);
|
parentCards.push(cardId);
|
||||||
|
cardCommentReactions.push(cardId)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send bulk queries for all found ids
|
// Send bulk queries for all found ids
|
||||||
subCards.send();
|
subCards.send();
|
||||||
cardComments.send();
|
cardComments.send();
|
||||||
|
cardCommentReactions.send();
|
||||||
attachments.send();
|
attachments.send();
|
||||||
checklists.send();
|
checklists.send();
|
||||||
checklistItems.send();
|
checklistItems.send();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Lists from '../../models/lists';
|
||||||
import Swimlanes from '../../models/swimlanes';
|
import Swimlanes from '../../models/swimlanes';
|
||||||
import Cards from '../../models/cards';
|
import Cards from '../../models/cards';
|
||||||
import CardComments from '../../models/cardComments';
|
import CardComments from '../../models/cardComments';
|
||||||
|
import CardCommentReactions from '../../models/cardCommentReactions';
|
||||||
import Attachments from '../../models/attachments';
|
import Attachments from '../../models/attachments';
|
||||||
import Checklists from '../../models/checklists';
|
import Checklists from '../../models/checklists';
|
||||||
import ChecklistItems from '../../models/checklistItems';
|
import ChecklistItems from '../../models/checklistItems';
|
||||||
|
|
@ -699,6 +700,8 @@ function findCards(sessionId, query) {
|
||||||
type: 1,
|
type: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const comments = CardComments.find({ cardId: { $in: cards.map(c => c._id) } });
|
||||||
|
|
||||||
return [
|
return [
|
||||||
cards,
|
cards,
|
||||||
Boards.find(
|
Boards.find(
|
||||||
|
|
@ -714,7 +717,8 @@ function findCards(sessionId, query) {
|
||||||
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||||
Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
|
Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
|
||||||
Attachments.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 }),
|
SessionData.find({ userId, sessionId }),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue