Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
Tooltip,
} from "@calcom/ui";

import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
Expand Down Expand Up @@ -189,6 +190,14 @@ function BookingListItem(booking: BookingItemProps) {
},
icon: "map-pin" as const,
},
{
id: "add_members",
label: t("additional_guests"),
onClick: () => {
setIsOpenAddGuestsDialog(true);
},
icon: "user-plus" as const,
},
];

if (booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
Expand Down Expand Up @@ -256,6 +265,7 @@ function BookingListItem(booking: BookingItemProps) {
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
Expand Down Expand Up @@ -344,6 +354,11 @@ function BookingListItem(booking: BookingItemProps) {
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
<AddGuestsDialog
isOpenDialog={isOpenAddGuestsDialog}
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
Expand Down
107 changes: 107 additions & 0 deletions apps/web/components/dialog/AddGuestsDialog.tsx
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) {

Copy link
Copy Markdown

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 ⚠️
- ⚠️ Add-guests dialog shows stale error after fixing emails.
- ⚠️ Users see error banner on reopen with valid state.
- ⚠️ Confusing UX when adding guests to existing bookings.
Suggested change
if (multiEmailValue.length === 0) {
setIsInvalidEmail(false);
Steps of Reproduction ✅
1. Open the booking list UI rendered by `apps/web/components/booking/BookingListItem.tsx`
(AddGuestsDialog imported at line 46 and used around lines 357-359).

2. Click the "Additional guests" action in `BookingListItem` to open `<AddGuestsDialog>`
(component defined in `apps/web/components/dialog/AddGuestsDialog.tsx:24`).

3. In the dialog, enter an invalid or duplicate email in the `MultiEmail` field and click
"Add"; `handleAdd` at `AddGuestsDialog.tsx:48-58` runs, `ZAddGuestsInputSchema.safeParse`
fails, and `setIsInvalidEmail(true)` executes, showing the red validation message at lines
76-83.

4. Correct the emails so they are all valid and unique and click "Add" again; `handleAdd`
does not clear `isInvalidEmail` before re-validating, so the error flag remains true and
the error UI at lines 76-83 stays visible until the dialog is closed, and on later
re-openings of the same dialog instance the error message appears immediately despite no
current validation error.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** apps/web/components/dialog/AddGuestsDialog.tsx
**Line:** 49:49
**Comment:**
	*Logic Error: 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.

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.
👍 | 👎

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>
);
};
6 changes: 6 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,9 @@
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "Click here to stop",
"event_location_changed": "Updated - Your event changed the location",
"new_guests_added": "Added - New guests added to your event",
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
"guests_added_event_type_subject": "Guests Added: {{eventType}} with {{name}} at {{date}}",
"current_location": "Current Location",
"new_location": "New Location",
"session": "Session",
Expand All @@ -1131,7 +1133,10 @@
"set_location": "Set Location",
"update_location": "Update Location",
"location_updated": "Location updated",
"guests_added": "Guests added",
"unable_to_add_guests": "Unable to add guests",
"email_validation_error": "That doesn't look like an email address",
"emails_must_be_unique_valid": "Emails must be unique and valid",
"url_validation_error": "That doesn't look like a URL",
"place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.",
"create_update_react_component": "Create or update an existing React component as shown below.",
Expand Down Expand Up @@ -2426,6 +2431,7 @@
"primary": "Primary",
"make_primary": "Make primary",
"add_email": "Add Email",
"add_emails": "Add Emails",
"add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.",
"confirm_email": "Confirm your email",
"scheduler_first_name": "The first name of the person booking",
Expand Down
28 changes: 28 additions & 0 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 eventTypeDisableHostEmail/eventTypeDisableAttendeeEmail guards used by other booking-notification senders. [logic error]

Severity Level: Major ⚠️
- ❌ Hosts emailed about guests despite 'disable host emails' setting.
- ❌ Attendees emailed despite 'disable attendee emails' configured.
- ⚠️ Breaks admin expectation of global email suppression.
- ⚠️ Inconsistent behavior versus booking confirmation email settings.
Suggested change
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.
👍 | 👎


await Promise.all(emailsToSend);
};
export const sendFeedbackEmail = async (feedback: Feedback) => {
await sendEmail(() => new FeedbackEmail(feedback));
};
Expand Down
10 changes: 10 additions & 0 deletions packages/emails/src/templates/AttendeeAddGuestsEmail.tsx
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}
/>
);
11 changes: 11 additions & 0 deletions packages/emails/src/templates/OrganizerAddGuestsEmail.tsx
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: 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. [logic error]

Severity Level: Major ⚠️
- ⚠️ Organizer "guests added" emails still show manage-link CTA.
- ⚠️ Template cannot reliably disable call-to-action region.
Suggested change
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.
👍 | 👎

/>
);
2 changes: 2 additions & 0 deletions packages/emails/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificat
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
34 changes: 34 additions & 0 deletions packages/emails/templates/attendee-add-guests-email.ts
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"),
};
}
}
38 changes: 38 additions & 0 deletions packages/emails/templates/organizer-add-guests-email.ts
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"),
};
}
}
19 changes: 19 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/_router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import authedProcedure from "../../../procedures/authedProcedure";
import publicProcedure from "../../../procedures/publicProcedure";
import { router } from "../../../trpc";
import { ZAddGuestsInputSchema } from "./addGuests.schema";
import { ZConfirmInputSchema } from "./confirm.schema";
import { ZEditLocationInputSchema } from "./editLocation.schema";
import { ZFindInputSchema } from "./find.schema";
Expand All @@ -14,6 +15,7 @@ type BookingsRouterHandlerCache = {
get?: typeof import("./get.handler").getHandler;
requestReschedule?: typeof import("./requestReschedule.handler").requestRescheduleHandler;
editLocation?: typeof import("./editLocation.handler").editLocationHandler;
addGuests?: typeof import("./addGuests.handler").addGuestsHandler;
confirm?: typeof import("./confirm.handler").confirmHandler;
getBookingAttendees?: typeof import("./getBookingAttendees.handler").getBookingAttendeesHandler;
find?: typeof import("./find.handler").getHandler;
Expand Down Expand Up @@ -74,6 +76,23 @@ export const bookingsRouter = router({
input,
});
}),
addGuests: authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.addGuests) {
UNSTABLE_HANDLER_CACHE.addGuests = await import("./addGuests.handler").then(
(mod) => mod.addGuestsHandler
);
}

// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.addGuests) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.addGuests({
ctx,
input,
});
}),

confirm: authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.confirm) {
Expand Down
Loading