Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix - add team members to emails #7207

Merged
merged 26 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
98cd23b
On booking add team members & translation
joeauyeung Feb 18, 2023
d128c3e
Add team members to round robin create
joeauyeung Feb 18, 2023
d05fe6f
Only update calendars on reschedule if there is a calendar reference
joeauyeung Feb 18, 2023
cbdda75
Send email on reschedules
joeauyeung Feb 18, 2023
3dcb5dd
Send team email on cancelled event
joeauyeung Feb 19, 2023
5e9fda7
Add team members to calendar event description
joeauyeung Feb 19, 2023
ff93467
Clean up
joeauyeung Feb 19, 2023
24becee
Convert other emails to organizer & teams
joeauyeung Feb 19, 2023
b4c7145
Type check fixes
joeauyeung Feb 19, 2023
94a6cae
More type fixes
joeauyeung Feb 19, 2023
8f3f17b
Change organizer scheduled input to an object
joeauyeung Feb 21, 2023
5a9f20b
early return updateCalendarEvent
joeauyeung Feb 21, 2023
5288fec
Introduce team member type
joeauyeung Feb 21, 2023
9035b05
Merge branch 'main' into teams-include-team-members-in-email
joeauyeung Feb 21, 2023
97d7842
Fix type errors
joeauyeung Feb 21, 2023
b4fb91f
Merge branch 'main' into teams-include-team-members-in-email
zomars Feb 22, 2023
25a3d97
Put team members before attendees
joeauyeung Feb 21, 2023
c4c629c
Remove lodash cloneDeep
joeauyeung Feb 21, 2023
7936b3a
Merge branch 'main' into teams-include-team-members-in-email
joeauyeung Feb 22, 2023
e6dfa35
Update packages/core/EventManager.ts
joeauyeung Feb 22, 2023
9f121ff
Remove booking select object
joeauyeung Feb 22, 2023
fe94312
Revert "Remove booking select object"
joeauyeung Feb 22, 2023
8ee8d7a
Refactor email manager (#7270)
joeauyeung Feb 23, 2023
e17f69e
Type change
joeauyeung Feb 24, 2023
3ecbebd
Remove conditional check for updateAllCalendarEvents
joeauyeung Feb 26, 2023
9f463dc
Merge branch 'main' into teams-include-team-members-in-email
zomars Feb 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/web/pages/api/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {},
recurringEvent: null,
team: {
name: "Team example",
members: [
{ name: "team member 1", email: "team@example.com" },
{ name: "team member 2", email: "team2@example.com" },
{ name: "team member 3", email: "team3@example.com" },
{ name: "team member 4", email: "team4@example.com" },
{ name: "team member 5", email: "team5@example.com" },
{ name: "team member 6", email: "team6@example.com" },
],
},
appsStatus: [
{
appName: "Outlook Calendar",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1599,5 +1599,6 @@
"booking_with_payment_cancelled_already_paid": "A refund for this booking payment it's on the way.",
"booking_with_payment_cancelled_refunded": "This booking payment has been refunded.",
"booking_confirmation_failed": "Booking confirmation failed",
"get_started_zapier_templates": "Get started with Zapier templates"
"get_started_zapier_templates": "Get started with Zapier templates",
"team_member": "Team member"
}
12 changes: 7 additions & 5 deletions packages/core/EventManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DestinationCalendar } from "@prisma/client";
import type { DestinationCalendar } from "@prisma/client";
import merge from "lodash/merge";
import { v5 as uuidv5 } from "uuid";
import { z } from "zod";
import type { z } from "zod";

import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
Expand All @@ -10,7 +10,7 @@ import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event";
import type {
CreateUpdateResult,
Expand Down Expand Up @@ -255,8 +255,10 @@ export default class EventManager {
results.push(result);
}

// Update all calendar events.
results.push(...(await this.updateAllCalendarEvents(evt, booking)));
// If there is a calendar reference, update all calendar events.
if (booking.references.some((reference) => reference.type.includes("_calendar"))) {
results.push(...(await this.updateAllCalendarEvents(evt, booking)));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping this will also fix some rescheduling bugs. If there were no calendar references this was still throwing an error

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If updateAllCalendarEvents is supposed to have calendar references then instead of erroring shouldn't it ignore and early return? It seems hackish to do it outside of the fn when it's the functions requirement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed with @hariombalhara. Let's find the root cause and fix it properly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joeauyeung This one is still pending for merge.


const bookingPayment = booking?.payment;

Expand Down
69 changes: 62 additions & 7 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TFunction } from "next-i18next";
import { cloneDeep } from "lodash";
import type { TFunction } from "next-i18next";

import type { CalendarEvent, Person } from "@calcom/types/Calendar";

Expand All @@ -12,8 +13,10 @@ import AttendeeScheduledEmail from "./templates/attendee-scheduled-email";
import AttendeeWasRequestedToRescheduleEmail from "./templates/attendee-was-requested-to-reschedule-email";
import BrokenIntegrationEmail from "./templates/broken-integration-email";
import DisabledAppEmail from "./templates/disabled-app-email";
import FeedbackEmail, { Feedback } from "./templates/feedback-email";
import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email";
import type { Feedback } from "./templates/feedback-email";
import FeedbackEmail from "./templates/feedback-email";
import type { PasswordReset } from "./templates/forgot-password-email";
import ForgotPasswordEmail from "./templates/forgot-password-email";
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email";
import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email";
Expand All @@ -22,10 +25,12 @@ import OrganizerRequestReminderEmail from "./templates/organizer-request-reminde
import OrganizerRequestedToRescheduleEmail from "./templates/organizer-requested-to-reschedule-email";
import OrganizerRescheduledEmail from "./templates/organizer-rescheduled-email";
import OrganizerScheduledEmail from "./templates/organizer-scheduled-email";
import TeamInviteEmail, { TeamInvite } from "./templates/team-invite-email";
import type { TeamInvite } from "./templates/team-invite-email";
import TeamInviteEmail from "./templates/team-invite-email";

export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];
const clonedEvent = cloneDeep(calEvent);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we need to alter the attendee emails to hide attendees if needed. Let's clone the event to pass to the organizer emails

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we cloning it? Asking because deepClone might be a bit slow plus require lodash.

Copy link
Contributor Author

@joeauyeung joeauyeung Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the attendee emails, we sometimes have to filter calEvent.attendees when it is a seats booking and the organizer wants to hide other attendees from each other.


emailsToSend.push(
...calEvent.attendees.map((attendee) => {
Expand All @@ -43,19 +48,37 @@ export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerScheduledEmail(calEvent);
const scheduledEmail = new OrganizerScheduledEmail(clonedEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
}
})
);

if (clonedEvent.team) {
for (const teamMember of clonedEvent.team.members) {
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerScheduledEmail(clonedEvent, undefined, teamMember);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By passing a team member, we can extract their locale and translation for the email

resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerScheduledEmail.sendEmail failed", e));
}
})
);
}
hariombalhara marked this conversation as resolved.
Show resolved Hide resolved
}

await Promise.all(emailsToSend);
};

export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];

const clonedEvent = cloneDeep(calEvent);

// @TODO: we should obtain who is rescheduling the event and send them a different email
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
Expand All @@ -73,14 +96,29 @@ export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerRescheduledEmail(calEvent);
const scheduledEmail = new OrganizerRescheduledEmail(clonedEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerRescheduledEmail.sendEmail failed", e));
}
})
);

if (clonedEvent.team) {
for (const teamMember of clonedEvent.team.members) {
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerRescheduledEmail(clonedEvent, undefined, teamMember);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerRescheduledEmail.sendEmail failed", e));
}
})
);
hariombalhara marked this conversation as resolved.
Show resolved Hide resolved
}
}

await Promise.all(emailsToSend);
};

Expand Down Expand Up @@ -161,6 +199,8 @@ export const sendDeclinedEmails = async (calEvent: CalendarEvent) => {
export const sendCancelledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];

const clonedEvent = cloneDeep(calEvent);

emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
Expand All @@ -177,14 +217,29 @@ export const sendCancelledEmails = async (calEvent: CalendarEvent) => {
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerCancelledEmail(calEvent);
const scheduledEmail = new OrganizerCancelledEmail(clonedEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerCancelledEmail.sendEmail failed", e));
}
})
);

if (clonedEvent.team?.members) {
for (const teamMember of clonedEvent.team.members) {
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerCancelledEmail(clonedEvent, undefined, teamMember);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerCancelledEmail.sendEmail failed", e));
}
})
);
}
}

await Promise.all(emailsToSend);
};

Expand Down
10 changes: 9 additions & 1 deletion packages/emails/src/components/WhoInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TFunction } from "next-i18next";
import type { TFunction } from "next-i18next";

import type { CalendarEvent } from "@calcom/types/Calendar";

Expand Down Expand Up @@ -35,6 +35,14 @@ export function WhoInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
email={attendee.email}
/>
))}
{props.calEvent.team?.members.map((member) => (
joeauyeung marked this conversation as resolved.
Show resolved Hide resolved
<PersonInfo
key={member.id || member.name}
name={member.name}
role={t("team_member")}
email={member.email}
/>
))}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are team members, loop through and display in the email

</>
}
withSpacer
Expand Down
5 changes: 3 additions & 2 deletions packages/emails/src/templates/OrganizerScheduledEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const OrganizerScheduledEmail = (
calEvent: CalendarEvent;
attendee: Person;
newSeat?: boolean;
teamMember?: Person;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
let subject;
Expand All @@ -26,10 +27,10 @@ export const OrganizerScheduledEmail = (
title = "new_event_scheduled";
}

const t = props.calEvent.organizer.language.translate;
const t = props.teamMember?.language.translate || props.calEvent.organizer.language.translate;
return (
<BaseScheduledEmail
timeZone={props.calEvent.organizer.timeZone}
timeZone={props.teamMember?.timeZone || props.calEvent.organizer.timeZone}
Comment on lines +30 to +33
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By passing the team member we can personalize the language and time zone in the email

t={t}
subject={t(subject)}
title={t(title)}
Expand Down
21 changes: 15 additions & 6 deletions packages/emails/templates/attendee-scheduled-email.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEvent, DateArray } from "ics";
import { TFunction } from "next-i18next";
import type { DateArray } from "ics";
import { createEvent } from "ics";
import type { TFunction } from "next-i18next";
import { RRule } from "rrule";

import dayjs from "@calcom/dayjs";
Expand Down Expand Up @@ -47,10 +48,18 @@ export default class AttendeeScheduledEmail extends BaseEmail {
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
...(this.calEvent.team?.members
? this.calEvent.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
}))
: []),
],
Comment on lines +51 to +62
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the .ics file we just lump together attendees and team members

...{ recurrenceRule },
status: "CONFIRMED",
});
Expand Down
10 changes: 1 addition & 9 deletions packages/emails/templates/organizer-cancelled-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";

export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];

return {
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
Expand Down
10 changes: 1 addition & 9 deletions packages/emails/templates/organizer-rescheduled-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";

export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];

return {
icalEvent: {
Expand Down
36 changes: 20 additions & 16 deletions packages/emails/templates/organizer-scheduled-email.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEvent, DateArray, Person } from "ics";
import { TFunction } from "next-i18next";
import type { DateArray, Person } from "ics";
import { createEvent } from "ics";
import type { TFunction } from "next-i18next";
import { RRule } from "rrule";

import dayjs from "@calcom/dayjs";
Expand All @@ -14,13 +15,15 @@ export default class OrganizerScheduledEmail extends BaseEmail {
calEvent: CalendarEvent;
t: TFunction;
newSeat?: boolean;
teamMember?: Person;

constructor(calEvent: CalendarEvent, newSeat?: boolean) {
constructor(calEvent: CalendarEvent, newSeat?: boolean, teamMember?: Person) {
joeauyeung marked this conversation as resolved.
Show resolved Hide resolved
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = calEvent;
this.t = this.calEvent.organizer.language.translate;
this.newSeat = newSeat;
this.teamMember = teamMember;
}

protected getiCalEventAsString(): string | undefined {
Expand All @@ -43,10 +46,18 @@ export default class OrganizerScheduledEmail extends BaseEmail {
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
...{ recurrenceRule },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
...(this.calEvent.team?.members
? this.calEvent.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
}))
: []),
],
status: "CONFIRMED",
});
if (icsEvent.error) {
Expand All @@ -56,15 +67,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
}

protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];

return {
icalEvent: {
Expand All @@ -79,6 +82,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
html: renderEmail("OrganizerScheduledEmail", {
calEvent: this.calEvent,
attendee: this.calEvent.organizer,
teamMember: this.teamMember,
newSeat: this.newSeat,
}),
text: this.getTextBody(),
Expand Down
Loading