1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Add notification when user is added to card

This commit is contained in:
Maksim Eltyshev 2025-05-17 01:50:40 +02:00
parent f43785c3d0
commit f6568ce41b
14 changed files with 169 additions and 39 deletions

View file

@ -84,6 +84,36 @@ const Item = React.memo(({ id }) => {
break;
}
case ActivityTypes.ADD_MEMBER_TO_CARD:
contentNode =
user.id === activity.data.user.id ? (
<Trans
i18nKey="common.userJoinedThisCard"
values={{
user: userName,
}}
>
<span className={styles.author}>{userName}</span>
<span className={styles.text}>{' joined this card'}</span>
</Trans>
) : (
<Trans
i18nKey="common.userAddedUserToThisCard"
values={{
actorUser: userName,
addedUser: activity.data.user.name,
}}
>
<span className={styles.author}>{userName}</span>
<span className={styles.text}>
{' added '}
{activity.data.user.name}
{' to this card'}
</span>
</Trans>
);
break;
default:
contentNode = null;
}

View file

@ -103,6 +103,24 @@ const Item = React.memo(({ id, onClose }) => {
break;
}
case NotificationTypes.ADD_MEMBER_TO_CARD:
contentNode = (
<Trans
i18nKey="common.userAddedYouToCard"
values={{
user: creatorUserName,
card: cardName,
}}
>
{creatorUserName}
{` added you to `}
<Link to={Paths.CARDS.replace(':id', notification.cardId)} onClick={onClose}>
{cardName}
</Link>
</Trans>
);
break;
default:
contentNode = null;
}

View file

@ -90,11 +90,13 @@ export const AttachmentTypes = {
export const ActivityTypes = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
export const NotificationTypes = {
MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
export const NotificationServiceFormats = {

View file

@ -287,6 +287,9 @@ export default {
uploadedImages: 'Uploaded images',
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}</0><1> added this card to {{list}}</1>',
userAddedUserToThisCard: '<0>{{actorUser}}</0><1> added {{addedUser}} to this card</1>',
userAddedYouToCard: '{{user}} added you to <2>{{card}}</2>',
userJoinedThisCard: `<0>{{user}}</0><1> joined this card</1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}</2>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:

View file

@ -282,6 +282,9 @@ export default {
uploadedImages: 'Uploaded images',
userActions_title: 'User Actions',
userAddedThisCardToList: '<0>{{user}}</0><1> added this card to {{list}}</1>',
userAddedUserToThisCard: '<0>{{actorUser}}</0><1> added {{addedUser}} to this card</1>',
userAddedYouToCard: '{{user}} added you to <2>{{card}}</2>',
userJoinedThisCard: `<0>{{user}}</0><1> joined this card</1>`,
userLeftNewCommentToCard: '{{user}} left a new comment «{{comment}}» to <2>{{card}}</2>',
userMovedCardFromListToList: '{{user}} moved <2>{{card}}</2> from {{fromList}} to {{toList}}',
userMovedThisCardFromListToList:

View file

@ -67,7 +67,13 @@ module.exports = {
throw Errors.NOT_ENOUGH_RIGHTS;
}
const isBoardMember = await sails.helpers.users.isBoardMember(inputs.userId, board.id);
const user = await User.qm.getOneById(inputs.userId);
if (!user) {
throw Errors.USER_NOT_FOUND;
}
const isBoardMember = await sails.helpers.users.isBoardMember(user.id, board.id);
if (!isBoardMember) {
throw Errors.USER_NOT_FOUND;
@ -80,7 +86,7 @@ module.exports = {
list,
values: {
card,
userId: inputs.userId,
user,
},
actorUser: currentUser,
request: this.req,

View file

@ -143,55 +143,76 @@ module.exports = {
});
if (action.type !== Action.Types.CREATE_CARD) {
const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
action.cardId,
action.userId,
);
const boardSubscriptionUserIds = await sails.helpers.boards.getSubscriptionUserIds(
inputs.board.id,
action.userId,
);
const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
await Promise.all(
notifiableUserIds.map((userId) =>
sails.helpers.notifications.createOne.with({
if (action.type === Action.Types.ADD_MEMBER_TO_CARD) {
if (values.user !== action.data.user.id) {
await sails.helpers.notifications.createOne.with({
values: {
userId,
action,
type: action.type,
data: {
...action.data,
card: _.pick(values.card, ['name']),
},
userId: action.data.user.id,
creatorUser: values.user,
card: values.card,
},
project: inputs.project,
board: inputs.board,
list: inputs.list,
}),
),
);
}
});
}
} else {
const cardSubscriptionUserIds = await sails.helpers.cards.getSubscriptionUserIds(
action.cardId,
action.userId,
);
const notificationServices = await NotificationService.qm.getByBoardId(inputs.board.id);
const boardSubscriptionUserIds = await sails.helpers.boards.getSubscriptionUserIds(
inputs.board.id,
action.userId,
);
if (notificationServices.length > 0) {
const services = notificationServices.map((notificationService) =>
_.pick(notificationService, ['url', 'format']),
);
const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
buildAndSendNotifications(
services,
inputs.board,
values.card,
action,
values.user,
sails.helpers.utils.makeTranslator(),
);
await Promise.all(
notifiableUserIds.map((userId) =>
sails.helpers.notifications.createOne.with({
values: {
userId,
action,
type: action.type,
data: {
...action.data,
card: _.pick(values.card, ['name']),
},
creatorUser: values.user,
card: values.card,
},
project: inputs.project,
board: inputs.board,
list: inputs.list,
}),
),
);
const notificationServices = await NotificationService.qm.getByBoardId(inputs.board.id);
if (notificationServices.length > 0) {
const services = notificationServices.map((notificationService) =>
_.pick(notificationService, ['url', 'format']),
);
buildAndSendNotifications(
services,
inputs.board,
values.card,
action,
values.user,
sails.helpers.utils.makeTranslator(),
);
}
}
}
return action;

View file

@ -37,15 +37,12 @@ module.exports = {
async fn(inputs) {
const { values } = inputs;
if (values.user) {
values.userId = values.user.id;
}
let cardMembership;
try {
cardMembership = await CardMembership.qm.createOne({
...values,
cardId: values.card.id,
userId: values.user.id,
});
} catch (error) {
if (error.code === 'E_UNIQUE') {
@ -69,6 +66,7 @@ module.exports = {
buildData: () => ({
item: cardMembership,
included: {
users: [values.user],
projects: [inputs.project],
boards: [inputs.board],
lists: [inputs.list],
@ -107,6 +105,20 @@ module.exports = {
// TODO: send webhooks
}
await sails.helpers.actions.createOne.with({
values: {
type: Action.Types.ADD_MEMBER_TO_CARD,
data: {
user: _.pick(values.user, ['id', 'name']),
},
user: inputs.actorUser,
card: values.card,
},
project: inputs.project,
board: inputs.board,
list: inputs.list,
});
return cardMembership;
},
};

View file

@ -12,6 +12,8 @@ const buildTitle = (notification, t) => {
return t('Card Moved');
case Notification.Types.COMMENT_CARD:
return t('New Comment');
case Notification.Types.ADD_MEMBER_TO_CARD:
return t('You Were Added to Card');
default:
return null;
}
@ -77,6 +79,22 @@ const buildBodyByFormat = (board, card, notification, actorUser, t) => {
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
};
}
case Notification.Types.ADD_MEMBER_TO_CARD:
return {
text: t('%s added you to %s on %s', actorUser.name, card.name, board.name),
markdown: t(
'%s added you to %s on %s',
escapeMarkdown(actorUser.name),
markdownCardLink,
escapeMarkdown(board.name),
),
html: t(
'%s added you to %s on %s',
escapeHtml(actorUser.name),
htmlCardLink,
escapeHtml(board.name),
),
};
default:
return null;
}
@ -120,6 +138,15 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
boardLink,
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
break;
case Notification.Types.ADD_MEMBER_TO_CARD:
html = `<p>${t(
'%s added you to %s on %s',
escapeHtml(actorUser.name),
cardLink,
boardLink,
)}</p>`;
break;
default:
return;

View file

@ -13,6 +13,7 @@
const Types = {
CREATE_CARD: 'createCard',
MOVE_CARD: 'moveCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
module.exports = {

View file

@ -13,6 +13,7 @@
const Types = {
MOVE_CARD: 'moveCard',
COMMENT_CARD: 'commentCard',
ADD_MEMBER_TO_CARD: 'addMemberToCard',
};
module.exports = {

View file

@ -6,6 +6,8 @@
"This is a test text message!": "This is a test text message!",
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "This is a <i>test</i> <b>html</b> <code>message</code>",
"You Were Added to Card": "Your Were Added to Card",
"%s added you to %s on %s": "%s added you to %s on %s",
"%s created %s in %s on %s": "%s created %s in %s on %s",
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"

View file

@ -6,6 +6,8 @@
"This is a test text message!": "This is a test text message!",
"This is a *test* **markdown** `message`!": "This is a *test* **markdown** `message`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "This is a <i>test</i> <b>html</b> <code>message</code>",
"You Were Added to Card": "Your Were Added to Card",
"%s added you to %s on %s": "%s added you to %s on %s",
"%s created %s in %s on %s": "%s created %s in %s on %s",
"%s left a new comment to %s on %s": "%s left a new comment to %s on %s",
"%s moved %s from %s to %s on %s": "%s moved %s from %s to %s on %s"

View file

@ -6,6 +6,8 @@
"This is a test text message!": "Это тестовое сообщение!",
"This is a *test* **markdown** `message`!": "Это *тестовое* **markdown** `сообщение`!",
"This is a <i>test</i> <b>html</b> <code>message</code>": "Это <i>тестовое</i> <b>html</b> <code>сообщение</code>",
"You Were Added to Card": "Вы были добавлены к карточке",
"%s added you to %s on %s": "%s добавил(а) вас к %s на %s",
"%s created %s in %s on %s": "%s создал(а) %s в %s на %s",
"%s left a new comment to %s on %s": "%s оставил(а) новый комментарий к %s на %s",
"%s moved %s from %s to %s on %s": "%s переместил(а) %s из %s в %s на %s"