Skip to content

Commit

Permalink
feat(Goal): email notification
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisVorop committed Dec 7, 2023
1 parent 4fd3468 commit db021bc
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 60 deletions.
16 changes: 16 additions & 0 deletions src/utils/prepareRecipients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface User {
email: string;
}

interface Participant {
user?: User | null;
}

export const prepareRecipients = (participants: (Participant | null)[]) => {
return participants.reduce<string[]>((acc, cur) => {
if (cur?.user?.email) {
acc.push(cur.user.email);
}
return acc;
}, []);
};
13 changes: 9 additions & 4 deletions src/utils/worker/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,27 @@ export function createJob<K extends keyof JobDataMap>(kind: K, { data, priority,
});
}

type Maper<T extends keyof Templates = keyof Templates> = <Params extends Parameters<Templates[T]>[number]>(
type Mapper<T extends keyof Templates = keyof Templates> = <Params extends Parameters<Templates[T]>[number]>(
params: Params,
) => Params;

const excludeCurrentUser: Maper = (params) => ({
const excludeCurrentUser: Mapper = (params) => ({
...params,
to: params.to.filter((email) => email !== params.authorEmail),
});

const mappers = [excludeCurrentUser];
const excludeDuplicateUsers: Mapper = (params) => ({
...params,
to: Array.from(new Set(params.to)),
});

const mappers = [excludeDuplicateUsers, excludeCurrentUser];

export function createEmailJob<T extends keyof Templates, Params extends Parameters<Templates[T]>[number]>(
template: T,
data: Params,
) {
const mappedParams = mappers.reduce<Params>((acum, mapper) => mapper(acum), data);
const mappedParams = mappers.reduce<Params>((acc, mapper) => mapper(acc), data);

if (!mappedParams.to.length) {
return null;
Expand Down
62 changes: 62 additions & 0 deletions src/utils/worker/mail/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,65 @@ ${footer}`);
text: subject,
};
};

interface PartnerProjectToGoalProps {
to: SendMailProps['to'];
key: string;
title: string;
authorEmail: string;
author: string;
partnerProject: {
key?: string;
title?: string;
};
}

export const addPartnerProjectToGoal = async ({
to,
key,
title,
author,
partnerProject,
}: PartnerProjectToGoalProps) => {
const subject = `ℹ️ Added partner project to #${key}: ${title}`;
const html = md.render(`
🧑‍💻 **${author}** added a partner project **[${partnerProject.key}: ${partnerProject.title}](${absUrl(
`/projects/${partnerProject.key}`,
)})** to **[${title}](${absUrl(`/goals/${key}`)})**.
${notice}
${footer}`);

return {
to,
subject,
html: withBaseTmplStyles(html),
text: subject,
};
};

export const removePartnerProjectToGoal = async ({
to,
key,
title,
author,
partnerProject,
}: PartnerProjectToGoalProps) => {
const subject = `ℹ️ Removed partner project from #${key}: ${title}`;
const html = md.render(`
🧑‍💻 **${author}** removed a partner project **[${partnerProject.key}: ${partnerProject.title}](${absUrl(
`/projects/${partnerProject.key}`,
)})** from **[${title}](${absUrl(`/goals/${key}`)})**.
${notice}
${footer}`);

return {
to,
subject,
html: withBaseTmplStyles(html),
text: subject,
};
};
156 changes: 121 additions & 35 deletions trpc/router/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
import { addCalculatedProjectFields, nonArchivedPartialQuery } from '../queries/project';
import { recalculateCriteriaScore, goalIncludeCriteriaParams } from '../../src/utils/recalculateCriteriaScore';
import { getProjectAccessFilter } from '../queries/access';
import { prepareRecipients } from '../../src/utils/prepareRecipients';

const updateProjectUpdatedAt = async (id?: string | null) => {
if (!id) return;
Expand Down Expand Up @@ -348,13 +349,11 @@ export const goal = router({
const newGoal = await createGoal(input, activityId, role);
await updateProjectUpdatedAt(actualProject.id);

const recipients = Array.from(
new Set(
[...actualProject.participants, ...actualProject.watchers, actualProject.activity]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
...actualProject.participants,
...actualProject.watchers,
actualProject.activity,
]);

await Promise.all([
createEmailJob('goalCreated', {
Expand Down Expand Up @@ -589,13 +588,12 @@ export const goal = router({
await recalculateCriteriaScore(goal.id).recalcLinkedGoalsScores().recalcAverageProjectScore().run();
}

const recipients = Array.from(
new Set(
[...actualGoal.participants, ...actualGoal.watchers, actualGoal.activity, actualGoal.owner]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
...actualGoal.participants,
...actualGoal.watchers,
actualGoal.activity,
actualGoal.owner,
]);

await createEmailJob('goalUpdated', {
to: recipients,
Expand Down Expand Up @@ -723,13 +721,12 @@ export const goal = router({
.run();
}

const recipients = Array.from(
new Set(
[...actualGoal.participants, ...actualGoal.watchers, actualGoal.activity, actualGoal.owner]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
...actualGoal.participants,
...actualGoal.watchers,
actualGoal.activity,
actualGoal.owner,
]);

await createEmailJob('goalArchived', {
to: recipients,
Expand Down Expand Up @@ -835,13 +832,12 @@ export const goal = router({
.recalcAverageProjectScore()
.run();

const recipients = Array.from(
new Set(
[...actualGoal.participants, ...actualGoal.watchers, actualGoal.activity, actualGoal.owner]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
...actualGoal.participants,
...actualGoal.watchers,
actualGoal.activity,
actualGoal.owner,
]);

await createEmailJob('goalStateUpdated', {
to: recipients,
Expand Down Expand Up @@ -959,13 +955,12 @@ export const goal = router({
.run();
}

const recipients = Array.from(
new Set(
[...actualGoal.participants, ...actualGoal.watchers, actualGoal.activity, actualGoal.owner]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
...actualGoal.participants,
...actualGoal.watchers,
actualGoal.activity,
actualGoal.owner,
]);

if (input.stateId) {
await createEmailJob('goalStateUpdatedWithComment', {
Expand Down Expand Up @@ -1521,10 +1516,53 @@ export const goal = router({
},
},
},
include: {
activity: { include: { user: true } },
owner: { include: { user: true } },
participants: { include: { user: true } },
watchers: { include: { user: true } },
},
});

await updateProjectUpdatedAt(updatedGoal.projectId);

const connectedProject = await prisma.project.findUnique({
where: { id: input.projectId },
include: {
activity: { include: { user: true } },
accessUsers: { include: { user: true } },
participants: { include: { user: true } },
watchers: { include: { user: true } },
},
});

let recipients: string[] = [];

if (updatedGoal && connectedProject) {
recipients = prepareRecipients([
updatedGoal.owner,
updatedGoal.activity,
connectedProject.activity,
...updatedGoal.participants,
...updatedGoal.watchers,
...connectedProject.accessUsers,
...connectedProject.participants,
...connectedProject.watchers,
]);
}

await createEmailJob('addPartnerProjectToGoal', {
to: recipients,
key: `${updatedGoal.projectId}-${updatedGoal.scopeId}`,
title: updatedGoal.title,
partnerProject: {
key: connectedProject?.id,
title: connectedProject?.title,
},
author: ctx.session.user.name || ctx.session.user.email,
authorEmail: ctx.session.user.email,
});

return updatedGoal;
} catch (error: any) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: String(error.message), cause: error });
Expand All @@ -1550,6 +1588,54 @@ export const goal = router({
},
},
},
include: {
activity: { include: { user: true } },
owner: { include: { user: true } },
participants: { include: { user: true } },
watchers: { include: { user: true } },
},
});

const disconnectedProject = await prisma.project.findUnique({
where: { id: input.projectId },
include: {
activity: { include: { user: true } },
accessUsers: { include: { user: true } },
participants: { include: { user: true } },
watchers: { include: { user: true } },
},
});

let recipients: string[] = [];

if (updatedGoal && disconnectedProject) {
recipients = [
updatedGoal.owner,
updatedGoal.activity,
disconnectedProject.activity,
...updatedGoal.participants,
...updatedGoal.watchers,
...disconnectedProject.accessUsers,
...disconnectedProject.participants,
...disconnectedProject.watchers,
].reduce<string[]>((acc, cur) => {
if (cur?.user?.email) {
acc.push(cur.user.email);
}
return acc;
}, []);
}

await createEmailJob('removePartnerProjectToGoal', {
to: recipients,
key: `${updatedGoal.projectId}-${updatedGoal.scopeId}`,
title: updatedGoal.title,
partnerProject: {
key: disconnectedProject?.id,
title: disconnectedProject?.title,
},
author: ctx.session.user.name || ctx.session.user.email,
authorEmail: ctx.session.user.email,
});

await updateProjectUpdatedAt(updatedGoal.projectId);
Expand Down
40 changes: 19 additions & 21 deletions trpc/router/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { updateLinkedGoalsByProject } from '../../src/utils/db';
import { prepareUserDataFromActivity } from '../../src/utils/getUserName';
import { projectAccessMiddleware, projectEditAccessMiddleware } from '../access/accessMiddlewares';
import { getProjectAccessFilter } from '../queries/access';
import { prepareRecipients } from '../../src/utils/prepareRecipients';

export const project = router({
suggestions: protectedProcedure
Expand Down Expand Up @@ -580,13 +581,12 @@ export const project = router({

await Promise.all(
newParents.map((parent) => {
const recipients = Array.from(
new Set(
[parent.activity, ...parent.accessUsers, ...parent.participants, ...parent.watchers]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
parent.activity,
...parent.accessUsers,
...parent.participants,
...parent.watchers,
]);

return createEmailJob('childProjectCreated', {
to: recipients,
Expand Down Expand Up @@ -619,13 +619,12 @@ export const project = router({

await Promise.all(
oldParents.map((parent) => {
const recipients = Array.from(
new Set(
[parent.activity, ...parent.accessUsers, ...parent.participants, ...parent.watchers]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
parent.activity,
...parent.accessUsers,
...parent.participants,
...parent.watchers,
]);

return createEmailJob('childProjectDeleted', {
to: recipients,
Expand Down Expand Up @@ -665,13 +664,12 @@ export const project = router({
];
}

const recipients = Array.from(
new Set(
[...project.participants, ...updatedProject.accessUsers, ...project.watchers, project.activity]
.filter(Boolean)
.map((r) => r.user?.email),
),
);
const recipients = prepareRecipients([
...project.participants,
...updatedProject.accessUsers,
...project.watchers,
project.activity,
]);

await createEmailJob('projectUpdated', {
to: recipients,
Expand Down

0 comments on commit db021bc

Please sign in to comment.