-
Notifications
You must be signed in to change notification settings - Fork 0
Add guest management functionality to existing bookings #3
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
base: guest-management-base
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import type { Dispatch, SetStateAction } from "react"; | ||
| import { useState } from "react"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import { trpc } from "@calcom/trpc/react"; | ||
| import { | ||
| Button, | ||
| Dialog, | ||
| DialogContent, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| MultiEmail, | ||
| Icon, | ||
| showToast, | ||
| } from "@calcom/ui"; | ||
|
|
||
| interface IAddGuestsDialog { | ||
| isOpenDialog: boolean; | ||
| setIsOpenDialog: Dispatch<SetStateAction<boolean>>; | ||
| bookingId: number; | ||
| } | ||
|
|
||
| export const AddGuestsDialog = (props: IAddGuestsDialog) => { | ||
| const { t } = useLocale(); | ||
| const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => { | ||
| const uniqueEmails = new Set(emails); | ||
| return uniqueEmails.size === emails.length; | ||
| }); | ||
| const { isOpenDialog, setIsOpenDialog, bookingId } = props; | ||
| const utils = trpc.useUtils(); | ||
| const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]); | ||
| const [isInvalidEmail, setIsInvalidEmail] = useState(false); | ||
|
|
||
| const addGuestsMutation = trpc.viewer.bookings.addGuests.useMutation({ | ||
| onSuccess: async () => { | ||
| showToast(t("guests_added"), "success"); | ||
| setIsOpenDialog(false); | ||
| setMultiEmailValue([""]); | ||
| utils.viewer.bookings.invalidate(); | ||
| }, | ||
| onError: (err) => { | ||
| const message = `${err.data?.code}: ${t(err.message)}`; | ||
| showToast(message || t("unable_to_add_guests"), "error"); | ||
| }, | ||
| }); | ||
|
|
||
| const handleAdd = () => { | ||
| if (multiEmailValue.length === 0) { | ||
| return; | ||
| } | ||
| const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue); | ||
| if (validationResult.success) { | ||
| addGuestsMutation.mutate({ bookingId, guests: multiEmailValue }); | ||
| } else { | ||
| setIsInvalidEmail(true); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}> | ||
| <DialogContent enableOverflow> | ||
| <div className="flex flex-row space-x-3"> | ||
| <div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full "> | ||
| <Icon name="user-plus" className="m-auto h-6 w-6" /> | ||
| </div> | ||
| <div className="w-full pt-1"> | ||
| <DialogHeader title={t("additional_guests")} /> | ||
| <MultiEmail | ||
| label={t("add_emails")} | ||
| value={multiEmailValue} | ||
| readOnly={false} | ||
| setValue={setMultiEmailValue} | ||
| /> | ||
|
|
||
| {isInvalidEmail && ( | ||
| <div className="my-4 flex text-sm text-red-700"> | ||
| <div className="flex-shrink-0"> | ||
| <Icon name="triangle-alert" className="h-5 w-5" /> | ||
| </div> | ||
| <div className="ml-3"> | ||
| <p className="font-medium">{t("emails_must_be_unique_valid")}</p> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| <DialogFooter> | ||
| <Button | ||
| onClick={() => { | ||
| setMultiEmailValue([""]); | ||
| setIsInvalidEmail(false); | ||
| setIsOpenDialog(false); | ||
| }} | ||
| type="button" | ||
| color="secondary"> | ||
| {t("cancel")} | ||
| </Button> | ||
| <Button data-testid="add_members" loading={addGuestsMutation.isPending} onClick={handleAdd}> | ||
| {t("add")} | ||
| </Button> | ||
| </DialogFooter> | ||
| </div> | ||
| </div> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,7 @@ import type { EmailVerifyLink } from "./templates/account-verify-email"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AccountVerifyEmail from "./templates/account-verify-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { OrganizationNotification } from "./templates/admin-organization-notification"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AdminOrganizationNotification from "./templates/admin-organization-notification"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AttendeeAddGuestsEmail from "./templates/attendee-add-guests-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AttendeeCancelledEmail from "./templates/attendee-cancelled-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -48,6 +49,7 @@ import type { OrganizationCreation } from "./templates/organization-creation-ema | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrganizationCreationEmail from "./templates/organization-creation-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { OrganizationEmailVerify } from "./templates/organization-email-verification"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrganizationEmailVerification from "./templates/organization-email-verification"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrganizerAddGuestsEmail from "./templates/organizer-add-guests-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrganizerAttendeeCancelledSeatEmail from "./templates/organizer-attendee-cancelled-seat-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrganizerCancelledEmail from "./templates/organizer-cancelled-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrganizerDailyVideoDownloadRecordingEmail from "./templates/organizer-daily-video-download-recording-email"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -520,6 +522,32 @@ export const sendLocationChangeEmails = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all(emailsToSend); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const calendarEvent = formatCalEvent(calEvent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emailsToSend: Promise<unknown>[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent }))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (calendarEvent.team?.members) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const teamMember of calendarEvent.team.members) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emailsToSend.push( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emailsToSend.push( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...calendarEvent.attendees.map((attendee) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (newGuests.includes(attendee.email)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+525
to
+547
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. Suggestion: Guest-added email sending ignores the existing event-type settings for disabling standard host/attendee emails, so organizers and attendees will still receive these notifications even when standard emails are configured as disabled; the function should accept optional event metadata and apply the same Severity Level: Major
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => { | |
| const calendarEvent = formatCalEvent(calEvent); | |
| const emailsToSend: Promise<unknown>[] = []; | |
| emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent }))); | |
| if (calendarEvent.team?.members) { | |
| for (const teamMember of calendarEvent.team.members) { | |
| emailsToSend.push( | |
| sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember })) | |
| ); | |
| } | |
| } | |
| emailsToSend.push( | |
| ...calendarEvent.attendees.map((attendee) => { | |
| if (newGuests.includes(attendee.email)) { | |
| return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee)); | |
| } else { | |
| return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee)); | |
| } | |
| }) | |
| ); | |
| export const sendAddGuestsEmails = async ( | |
| calEvent: CalendarEvent, | |
| newGuests: string[], | |
| eventTypeMetadata?: EventTypeMetadata | |
| ) => { | |
| const calendarEvent = formatCalEvent(calEvent); | |
| const emailsToSend: Promise<unknown>[] = []; | |
| if (!eventTypeDisableHostEmail(eventTypeMetadata)) { | |
| emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent }))); | |
| if (calendarEvent.team?.members) { | |
| for (const teamMember of calendarEvent.team.members) { | |
| emailsToSend.push( | |
| sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember })) | |
| ); | |
| } | |
| } | |
| } | |
| if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) { | |
| emailsToSend.push( | |
| ...calendarEvent.attendees.map((attendee) => { | |
| if (newGuests.includes(attendee.email)) { | |
| return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee)); | |
| } else { | |
| return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee)); | |
| } | |
| }) | |
| ); | |
| } |
Steps of Reproduction ✅
1. In the web UI, configure an event type to disable all standard emails for hosts and
attendees via the toggles bound to `metadata.disableStandardEmails.all.attendee` and
`metadata.disableStandardEmails.all.host` (see
`apps/web/components/eventtype/EventAdvancedTab.tsx:687-710`, where these fields are
wired).
2. Create and confirm a booking for this event type. During booking creation
(`packages/features/bookings/lib/handleNewBooking.ts:1471-1506`), the code reads
`eventType.metadata.disableStandardEmails` and passes `eventType.metadata` plus derived
`isHostConfirmationEmailsDisabled`/`isAttendeeConfirmationEmailDisabled` into
`sendScheduledEmails`, which in turn checks
`eventTypeDisableHostEmail`/`eventTypeDisableAttendeeEmail`
(`packages/emails/email-manager.ts:81-87, 89-132`). As a result, the standard scheduled
emails for this event are correctly suppressed in line with the settings.
3. After the booking exists, use the "add guests" functionality on that booking, which
invokes the TRPC handler `addGuestsHandler` at
`packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts:16-22`. This handler
loads the booking including its `eventType` and metadata (`addGuests.handler.ts:26-41`),
builds a `CalendarEvent` object `evt` (`addGuests.handler.ts:124-147`), and updates the
calendar attendees.
4. Still inside `addGuestsHandler`, observe that it calls `await sendAddGuestsEmails(evt,
guests);` without passing any event-type metadata (`addGuests.handler.ts:167-168`), even
though the booking's `eventType.metadata.disableStandardEmails.all.*` flags are set.
5. In `packages/emails/email-manager.ts:525-550`, the implementation of
`sendAddGuestsEmails` formats the calendar event and unconditionally pushes organizer and
team-member emails using `OrganizerAddGuestsEmail`, and attendee emails using
`AttendeeScheduledEmail` or `AttendeeAddGuestsEmail`, with no checks against
`eventTypeDisableHostEmail` or `eventTypeDisableAttendeeEmail` and no metadata parameter
at all.
6. Because of step 5, when step 3's add-guests action runs on an event type with
`metadata.disableStandardEmails.all.host`/`attendee` set, organizers, team members, and
attendees still receive guest-added emails, contradicting the global "disable standard
emails" configuration that other email flows (e.g., scheduled, rescheduled, cancelled,
location change) respect.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** packages/emails/email-manager.ts
**Line:** 525:547
**Comment:**
*Logic Error: Guest-added email sending ignores the existing event-type settings for disabling standard host/attendee emails, so organizers and attendees will still receive these notifications even when standard emails are configured as disabled; the function should accept optional event metadata and apply the same `eventTypeDisableHostEmail`/`eventTypeDisableAttendeeEmail` guards used by other booking-notification senders.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail"; | ||
|
|
||
| export const AttendeeAddGuestsEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => ( | ||
| <AttendeeScheduledEmail | ||
| title="new_guests_added" | ||
| headerType="calendarCircle" | ||
| subject="guests_added_event_type_subject" | ||
| {...props} | ||
| /> | ||
| ); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,11 @@ | ||||||||||||||||||||||
| import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => ( | ||||||||||||||||||||||
| <OrganizerScheduledEmail | ||||||||||||||||||||||
| title="new_guests_added" | ||||||||||||||||||||||
| headerType="calendarCircle" | ||||||||||||||||||||||
| subject="guests_added_event_type_subject" | ||||||||||||||||||||||
| callToAction={null} | ||||||||||||||||||||||
| {...props} | ||||||||||||||||||||||
|
Comment on lines
+5
to
+9
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. Suggestion: In the organizer guest-added email wrapper, Severity Level: Major
|
||||||||||||||||||||||
| title="new_guests_added" | |
| headerType="calendarCircle" | |
| subject="guests_added_event_type_subject" | |
| callToAction={null} | |
| {...props} | |
| {...props} | |
| title="new_guests_added" | |
| headerType="calendarCircle" | |
| subject="guests_added_event_type_subject" | |
| callToAction={null} |
Steps of Reproduction ✅
1. In the web app, use the "add guests" flow for an existing booking, which hits the TRPC
handler at `packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts:168` where
`await sendAddGuestsEmails(evt, guests);` is called.
2. Inside `sendAddGuestsEmails` in `packages/emails/email-manager.ts:525-549`, the code
formats the event and enqueues organizer emails via `new OrganizerAddGuestsEmail({
calEvent: calendarEvent })` and `new OrganizerAddGuestsEmail({ calEvent: calendarEvent,
teamMember })`.
3. The class `OrganizerAddGuestsEmail` in
`packages/emails/templates/organizer-add-guests-email.ts:7-36` builds the nodemailer
payload and renders HTML with `html: await renderEmail("OrganizerAddGuestsEmail", {
attendee: this.calEvent.organizer, calEvent: this.calEvent })`, invoking the React
template `packages/emails/src/templates/OrganizerAddGuestsEmail.tsx:3-11`.
4. In that React template, `<OrganizerScheduledEmail title="new_guests_added"
headerType="calendarCircle" subject="guests_added_event_type_subject" callToAction={null}
{...props} />` passes `callToAction={null}` and then immediately spreads `props`, where
`props.callToAction` is `undefined` from the `renderEmail` call; this override makes the
final `callToAction` prop `undefined`, so `BaseScheduledEmail` at
`packages/emails/src/templates/BaseScheduledEmail.tsx:63-67` treats it as not-null and
falls back to the default `<ManageLink ...>` call-to-action, causing a manage-link CTA to
appear even though this template explicitly attempts to disable it.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** packages/emails/src/templates/OrganizerAddGuestsEmail.tsx
**Line:** 5:9
**Comment:**
*Logic Error: In the organizer guest-added email wrapper, `callToAction` is set to `null` but then immediately overridden by the spread `props`, so any provided `callToAction` will still appear in the email; spreading `props` first and then setting `callToAction={null}` (and the fixed title/header/subject) ensures these values cannot be accidentally overridden.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { renderEmail } from "../"; | ||
| import generateIcsString from "../lib/generateIcsString"; | ||
| import AttendeeScheduledEmail from "./attendee-scheduled-email"; | ||
|
|
||
| export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail { | ||
| protected async getNodeMailerPayload(): Promise<Record<string, unknown>> { | ||
| return { | ||
| icalEvent: { | ||
| filename: "event.ics", | ||
| content: generateIcsString({ | ||
| event: this.calEvent, | ||
| title: this.t("new_guests_added"), | ||
| subtitle: this.t("emailed_you_and_any_other_attendees"), | ||
| role: "attendee", | ||
| status: "CONFIRMED", | ||
| }), | ||
| method: "REQUEST", | ||
| }, | ||
| to: `${this.attendee.name} <${this.attendee.email}>`, | ||
| from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, | ||
| replyTo: this.calEvent.organizer.email, | ||
| subject: `${this.t("guests_added_event_type_subject", { | ||
| eventType: this.calEvent.type, | ||
| name: this.calEvent.team?.name || this.calEvent.organizer.name, | ||
| date: this.getFormattedDate(), | ||
| })}`, | ||
| html: await renderEmail("AttendeeAddGuestsEmail", { | ||
| calEvent: this.calEvent, | ||
| attendee: this.attendee, | ||
| }), | ||
| text: this.getTextBody("new_guests_added"), | ||
| }; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { APP_NAME } from "@calcom/lib/constants"; | ||
|
|
||
| import { renderEmail } from "../"; | ||
| import generateIcsString from "../lib/generateIcsString"; | ||
| import OrganizerScheduledEmail from "./organizer-scheduled-email"; | ||
|
|
||
| export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail { | ||
| protected async getNodeMailerPayload(): Promise<Record<string, unknown>> { | ||
| const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; | ||
|
|
||
| return { | ||
| icalEvent: { | ||
| filename: "event.ics", | ||
| content: generateIcsString({ | ||
| event: this.calEvent, | ||
| title: this.t("new_guests_added"), | ||
| subtitle: this.t("emailed_you_and_any_other_attendees"), | ||
| role: "organizer", | ||
| status: "CONFIRMED", | ||
| }), | ||
| method: "REQUEST", | ||
| }, | ||
| from: `${APP_NAME} <${this.getMailerOptions().from}>`, | ||
| to: toAddresses.join(","), | ||
| replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)], | ||
| subject: `${this.t("guests_added_event_type_subject", { | ||
| eventType: this.calEvent.type, | ||
| name: this.calEvent.attendees[0].name, | ||
| date: this.getFormattedDate(), | ||
| })}`, | ||
| html: await renderEmail("OrganizerAddGuestsEmail", { | ||
| attendee: this.calEvent.organizer, | ||
| calEvent: this.calEvent, | ||
| }), | ||
| text: this.getTextBody("new_guests_added"), | ||
| }; | ||
| } | ||
| } |
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.
Suggestion: The invalid-email error flag is only ever set to true and never cleared on subsequent submissions (except when pressing "cancel"), so once a user triggers a validation error the red error message will continue to show even after they enter valid emails and successfully add guests or reopen the dialog; resetting the flag at the start of each submission prevents this stale error state. [logic error]
Severity Level: Major⚠️
Steps of Reproduction ✅
Prompt for AI Agent 🤖