-
Notifications
You must be signed in to change notification settings - Fork 7.8k
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
Changes from 6 commits
98cd23b
d128c3e
d05fe6f
cbdda75
3dcb5dd
5e9fda7
ff93467
24becee
b4c7145
94a6cae
8f3f17b
5a9f20b
5288fec
9035b05
97d7842
b4fb91f
25a3d97
c4c629c
7936b3a
e6dfa35
9f121ff
fe94312
8ee8d7a
e17f69e
3ecbebd
9f463dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
|
||
|
@@ -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"; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the attendee emails, we sometimes have to filter |
||
|
||
emailsToSend.push( | ||
...calEvent.attendees.map((attendee) => { | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
|
@@ -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); | ||
}; | ||
|
||
|
@@ -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) => { | ||
|
@@ -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); | ||
}; | ||
|
||
|
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"; | ||
|
||
|
@@ -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} | ||
/> | ||
))} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ export const OrganizerScheduledEmail = ( | |
calEvent: CalendarEvent; | ||
attendee: Person; | ||
newSeat?: boolean; | ||
teamMember?: Person; | ||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>> | ||
) => { | ||
let subject; | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)} | ||
|
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"; | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
}); | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.