fix: use destination calendar email (#12821)
* fix: use destination calendar email * fix: success page * feat: add primary email in destination calendar * fix: set destination bug * chore: fix tests * test: add test for different email * wip: save progress * feat: add switch and email in booking * fix: use userPrimaryEmail * chore: fix type errr * chore: update buileder * fix: type err in advanced tab * fix: unit tests * Update apps/web/public/static/locales/en/common.json Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * fix: cancel, request reschedule, paymeny --------- Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
30 files changed
+162
-76
No files matched your search
@@ -30,6 +30,7 @@ import {
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
@@ -44,10 +45,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
|
||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
|
||||
const [useEventTypeDestinationCalendarEmail, setUseEventTypeDestinationCalendarEmail] = useState(
|
||||
formMethods.getValues("useEventTypeDestinationCalendarEmail")
|
||||
);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
@@ -116,6 +119,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -125,7 +130,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
* This will fallback to each user selected destination calendar.
|
||||
*/}
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
{!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
@@ -171,6 +176,18 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="w-full">
|
||||
<Switch
|
||||
label={t("display_add_to_calendar_organizer")}
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
onCheckedChange={(val) => {
|
||||
setUseEventTypeDestinationCalendarEmail(val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
|
||||
|
||||
@@ -82,6 +82,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
cancellationReason: true,
|
||||
responses: true,
|
||||
rejectionReason: true,
|
||||
userPrimaryEmail: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function Success(props: PageProps) {
|
||||
duration: calculatedDuration,
|
||||
organizer: {
|
||||
name: users[0].name || "Nameless",
|
||||
email: users[0].email || "Email-less",
|
||||
email: bookingInfo?.userPrimaryEmail || users[0].email || "Email-less",
|
||||
timeZone: users[0].timeZone,
|
||||
},
|
||||
confirmed: !needsConfirmation,
|
||||
@@ -450,7 +450,9 @@ export default function Success(props: PageProps) {
|
||||
</span>
|
||||
<Badge variant="blue">{t("Host")}</Badge>
|
||||
</div>
|
||||
<p className="text-default">{bookingInfo.user.email}</p>
|
||||
<p className="text-default">
|
||||
{bookingInfo?.userPrimaryEmail ?? bookingInfo.user.email}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
|
||||
@@ -148,6 +148,7 @@ export type FormValues = {
|
||||
multipleDurationEnabled: boolean;
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
@@ -298,6 +299,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
hosts: eventType.hosts,
|
||||
successRedirectUrl: eventType.successRedirectUrl || "",
|
||||
users: eventType.users,
|
||||
useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
|
||||
children: eventType.children.map((ch) => ({
|
||||
...ch,
|
||||
created: true,
|
||||
|
||||
@@ -119,7 +119,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
email: booking?.userPrimaryEmail ?? user.email,
|
||||
name,
|
||||
timeZone: user.timeZone,
|
||||
language: { translate: tOrganizer, locale: user.locale ?? "en" },
|
||||
|
||||
@@ -230,7 +230,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking.user?.email || "Email-less",
|
||||
email: booking?.userPrimaryEmail || booking.user?.email || "Email-less",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "Europe/London",
|
||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||
|
||||
@@ -807,6 +807,7 @@
|
||||
"additional_input_description": "Require scheduler to input additional inputs prior the booking is confirmed",
|
||||
"label": "Label",
|
||||
"placeholder": "Placeholder",
|
||||
"display_add_to_calendar_organizer":"Display 'Add to calendar' email as the organizer",
|
||||
"type": "Type",
|
||||
"edit": "Edit",
|
||||
"add_input": "Add an Input",
|
||||
|
||||
@@ -108,6 +108,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
timeZone,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -156,6 +157,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
scheduleId,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -250,6 +252,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
timeZone,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -298,6 +301,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
userId,
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -350,6 +354,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
|
||||
@@ -307,16 +307,18 @@ export function expectWebhookToHaveBeenCalledWith(
|
||||
export function expectWorkflowToBeTriggered({
|
||||
emails,
|
||||
organizer,
|
||||
destinationEmail,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
destinationEmail?: string;
|
||||
}) {
|
||||
const subjectPattern = /^Reminder: /i;
|
||||
expect(emails.get()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
subject: expect.stringMatching(subjectPattern),
|
||||
to: organizer.email,
|
||||
to: destinationEmail ?? organizer.email,
|
||||
}),
|
||||
])
|
||||
);
|
||||
@@ -370,6 +372,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
recurrence,
|
||||
bookingTimeRange,
|
||||
booking,
|
||||
destinationEmail,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: { email: string; name: string; timeZone: string };
|
||||
@@ -381,6 +384,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
eventDomain?: string;
|
||||
bookingTimeRange?: { start: Date; end: Date };
|
||||
booking: { uid: string; urlOrigin?: string };
|
||||
destinationEmail?: string;
|
||||
}) {
|
||||
const bookingUrlOrigin = booking.urlOrigin || WEBSITE_URL;
|
||||
expect(emails).toHaveEmail(
|
||||
@@ -409,7 +413,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
},
|
||||
}
|
||||
: null),
|
||||
to: `${organizer.email}`,
|
||||
to: `${destinationEmail ?? organizer.email}`,
|
||||
ics: {
|
||||
filename: "event.ics",
|
||||
iCalUID: `${iCalUID}`,
|
||||
@@ -417,7 +421,7 @@ export function expectSuccessfulBookingCreationEmails({
|
||||
method: "REQUEST",
|
||||
},
|
||||
},
|
||||
`${organizer.email}`
|
||||
`${destinationEmail ?? organizer.email}`
|
||||
);
|
||||
|
||||
expect(emails).toHaveEmail(
|
||||
|
||||
@@ -245,7 +245,7 @@ async function handler(req: CustomRequest) {
|
||||
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
|
||||
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
|
||||
organizer: {
|
||||
email: organizer.email,
|
||||
email: bookingToDelete?.userPrimaryEmail ?? organizer.email,
|
||||
name: organizer.name ?? "Nameless",
|
||||
timeZone: organizer.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat),
|
||||
@@ -524,7 +524,7 @@ async function handler(req: CustomRequest) {
|
||||
startTime: bookingToDelete.startTime.toISOString(),
|
||||
endTime: bookingToDelete.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: bookingToDelete.user?.email ?? "dev@calendso.com",
|
||||
email: bookingToDelete?.userPrimaryEmail ?? bookingToDelete.user?.email ?? "dev@calendso.com",
|
||||
name: bookingToDelete.user?.name ?? "no user",
|
||||
timeZone: bookingToDelete.user?.timeZone ?? "",
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat),
|
||||
|
||||
@@ -342,6 +342,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
durationLimits: true,
|
||||
assignAllTeamMembers: true,
|
||||
parentId: true,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
owner: {
|
||||
select: {
|
||||
hideBranding: true,
|
||||
@@ -741,6 +742,7 @@ async function createBooking({
|
||||
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
userPrimaryEmail: evt.organizer.email,
|
||||
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses,
|
||||
title: evt.title,
|
||||
startTime: dayjs.utc(evt.startTime).toDate(),
|
||||
@@ -1496,6 +1498,7 @@ async function handler(
|
||||
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) {
|
||||
teamDestinationCalendars.push(user.destinationCalendar);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email ?? "",
|
||||
@@ -1548,6 +1551,12 @@ async function handler(
|
||||
? await getBookerBaseUrl(eventType.team.parentId)
|
||||
: await getBookerBaseUrl(organizerOrganizationId ?? null);
|
||||
|
||||
const destinationCalendar = eventType.destinationCalendar
|
||||
? [eventType.destinationCalendar]
|
||||
: organizerUser.destinationCalendar
|
||||
? [organizerUser.destinationCalendar]
|
||||
: null;
|
||||
|
||||
let evt: CalendarEvent = {
|
||||
bookerUrl,
|
||||
type: eventType.slug,
|
||||
@@ -1560,7 +1569,10 @@ async function handler(
|
||||
organizer: {
|
||||
id: organizerUser.id,
|
||||
name: organizerUser.name || "Nameless",
|
||||
email: organizerUser.email || "Email-less",
|
||||
email:
|
||||
eventType.useEventTypeDestinationCalendarEmail && destinationCalendar?.[0]?.primaryEmail
|
||||
? destinationCalendar[0].primaryEmail
|
||||
: organizerUser.email || "Email-less",
|
||||
username: organizerUser.username || undefined,
|
||||
timeZone: organizerUser.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
|
||||
@@ -1571,11 +1583,7 @@ async function handler(
|
||||
attendees: attendeesList,
|
||||
location: bookingLocation, // Will be processed by the EventManager later.
|
||||
conferenceCredentialId,
|
||||
destinationCalendar: eventType.destinationCalendar
|
||||
? [eventType.destinationCalendar]
|
||||
: organizerUser.destinationCalendar
|
||||
? [organizerUser.destinationCalendar]
|
||||
: null,
|
||||
destinationCalendar,
|
||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||
requiresConfirmation: !isConfirmedByDefault,
|
||||
eventTypeId: eventType.id,
|
||||
|
||||
@@ -82,6 +82,9 @@ describe("handleNewBooking", () => {
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizerOtherEmail = "organizer2@example.com";
|
||||
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
@@ -92,6 +95,7 @@ describe("handleNewBooking", () => {
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "organizer@google-calendar.com",
|
||||
primaryEmail: organizerOtherEmail,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,6 +126,7 @@ describe("handleNewBooking", () => {
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
@@ -130,6 +135,7 @@ describe("handleNewBooking", () => {
|
||||
destinationCalendar: {
|
||||
integration: "google_calendar",
|
||||
externalId: "event-type-1@google-calendar.com",
|
||||
primaryEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -208,7 +214,11 @@ describe("handleNewBooking", () => {
|
||||
iCalUID: createdBooking.iCalUID,
|
||||
});
|
||||
|
||||
expectWorkflowToBeTriggered({ organizer, emails });
|
||||
expectWorkflowToBeTriggered({
|
||||
organizer,
|
||||
emails,
|
||||
destinationEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
});
|
||||
expectSuccessfulCalendarEventCreationInCalendar(calendarMock, {
|
||||
calendarId: "event-type-1@google-calendar.com",
|
||||
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
|
||||
@@ -225,6 +235,7 @@ describe("handleNewBooking", () => {
|
||||
organizer,
|
||||
emails,
|
||||
iCalUID,
|
||||
destinationEmail: organizerDestinationCalendarEmailOnEventType,
|
||||
});
|
||||
|
||||
expectBookingCreatedWebhookToHaveBeenFired({
|
||||
|
||||
@@ -101,6 +101,7 @@ const commons = {
|
||||
metadata: EventTypeMetaDataSchema.parse({}),
|
||||
bookingFields: [],
|
||||
assignAllTeamMembers: false,
|
||||
useEventTypeDestinationCalendarEmail: false,
|
||||
};
|
||||
|
||||
const dynamicEvent = {
|
||||
|
||||
@@ -112,6 +112,7 @@ export default async function getEventTypeById({
|
||||
successRedirectUrl: true,
|
||||
currency: true,
|
||||
bookingFields: true,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -96,7 +96,7 @@ export async function getBooking(bookingId: number) {
|
||||
bookingFields: booking.eventType?.bookingFields || null,
|
||||
}),
|
||||
organizer: {
|
||||
email: user.email,
|
||||
email: booking?.userPrimaryEmail ?? user.email,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
name: user.name!,
|
||||
timeZone: user.timeZone,
|
||||
|
||||
@@ -38,6 +38,7 @@ export const buildBooking = (booking?: Partial<Booking>): Booking => {
|
||||
uid,
|
||||
userId: null,
|
||||
eventTypeId: null,
|
||||
userPrimaryEmail: null,
|
||||
title: faker.lorem.sentence(),
|
||||
description: faker.lorem.paragraph(),
|
||||
customInputs: null,
|
||||
@@ -82,6 +83,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
userId: null,
|
||||
teamId: null,
|
||||
requiresBookerEmailVerification: false,
|
||||
useEventTypeDestinationCalendarEmail: false,
|
||||
eventName: faker.lorem.words(),
|
||||
timeZone: null,
|
||||
periodType: "UNLIMITED",
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DestinationCalendar" ADD COLUMN "primaryEmail" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Booking" ADD COLUMN "userPrimaryEmail" TEXT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "useEventTypeDestinationCalendarEmail" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -70,62 +70,63 @@ model EventType {
|
||||
profileId Int?
|
||||
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)
|
||||
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
hashedLink HashedLink?
|
||||
bookings Booking[]
|
||||
availability Availability[]
|
||||
webhooks Webhook[]
|
||||
destinationCalendar DestinationCalendar?
|
||||
eventName String?
|
||||
customInputs EventTypeCustomInput[]
|
||||
parentId Int?
|
||||
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children EventType[] @relation("managed_eventtype")
|
||||
/// @zod.custom(imports.eventTypeBookingFields)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodStartDate DateTime?
|
||||
periodStartDate DateTime?
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
/// @zod.custom(imports.recurringEventType)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
recurringEvent Json?
|
||||
disableGuests Boolean @default(false)
|
||||
hideCalendarNotes Boolean @default(false)
|
||||
/// @zod.min(0)
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
minimumBookingNotice Int @default(120)
|
||||
beforeEventBuffer Int @default(0)
|
||||
afterEventBuffer Int @default(0)
|
||||
seatsPerTimeSlot Int?
|
||||
onlyShowFirstAvailableSlot Boolean @default(false)
|
||||
seatsShowAttendees Boolean? @default(false)
|
||||
seatsShowAvailabilityCount Boolean? @default(true)
|
||||
schedulingType SchedulingType?
|
||||
schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||
scheduleId Int?
|
||||
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
|
||||
price Int @default(0)
|
||||
price Int @default(0)
|
||||
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
/// @zod.custom(imports.EventTypeMetaDataSchema)
|
||||
metadata Json?
|
||||
metadata Json?
|
||||
/// @zod.custom(imports.successRedirectUrl)
|
||||
successRedirectUrl String?
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
successRedirectUrl String?
|
||||
workflows WorkflowsOnEventTypes[]
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
bookingLimits Json?
|
||||
bookingLimits Json?
|
||||
/// @zod.custom(imports.intervalLimitsType)
|
||||
durationLimits Json?
|
||||
isInstantEvent Boolean @default(false)
|
||||
assignAllTeamMembers Boolean @default(false)
|
||||
durationLimits Json?
|
||||
isInstantEvent Boolean @default(false)
|
||||
assignAllTeamMembers Boolean @default(false)
|
||||
useEventTypeDestinationCalendarEmail Boolean @default(false)
|
||||
|
||||
@@unique([userId, slug])
|
||||
@@unique([teamId, slug])
|
||||
@@ -173,6 +174,8 @@ model DestinationCalendar {
|
||||
id Int @id @default(autoincrement())
|
||||
integration String
|
||||
externalId String
|
||||
/// @zod.email()
|
||||
primaryEmail String?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int? @unique
|
||||
booking Booking[]
|
||||
@@ -459,6 +462,9 @@ model Booking {
|
||||
uid String @unique
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
// User's email at the time of booking
|
||||
/// @zod.email()
|
||||
userPrimaryEmail String?
|
||||
references BookingReference[]
|
||||
eventType EventType? @relation(fields: [eventTypeId], references: [id])
|
||||
eventTypeId Int?
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Prisma } from "@prisma/client";
|
||||
export const bookingMinimalSelect = Prisma.validator<Prisma.BookingSelect>()({
|
||||
id: true,
|
||||
title: true,
|
||||
userPrimaryEmail: true,
|
||||
description: true,
|
||||
customInputs: true,
|
||||
startTime: true,
|
||||
|
||||
@@ -39,6 +39,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda
|
||||
user.selectedCalendars,
|
||||
user.destinationCalendar?.externalId
|
||||
);
|
||||
|
||||
let toggledCalendarDetails:
|
||||
| {
|
||||
externalId: string;
|
||||
@@ -59,7 +60,12 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda
|
||||
There are connected calendars, but no destination calendar
|
||||
So create a default destination calendar with the first primary connected calendar
|
||||
*/
|
||||
const { integration = "", externalId = "", credentialId } = connectedCalendars[0].primary ?? {};
|
||||
const {
|
||||
integration = "",
|
||||
externalId = "",
|
||||
credentialId,
|
||||
email: primaryEmail,
|
||||
} = connectedCalendars[0].primary ?? {};
|
||||
// Select the first calendar matching the primary by default since that will also be the destination calendar
|
||||
if (onboarding && externalId) {
|
||||
const calendarIndex = (connectedCalendars[0].calendars || []).findIndex(
|
||||
@@ -79,6 +85,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
primaryEmail,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -94,7 +101,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda
|
||||
|
||||
if (!destinationCal) {
|
||||
// If destinationCalendar is out of date, update it with the first primary connected calendar
|
||||
const { integration = "", externalId = "" } = connectedCalendars[0].primary ?? {};
|
||||
const { integration = "", externalId = "", email: primaryEmail } = connectedCalendars[0].primary ?? {};
|
||||
// Select the first calendar matching the primary by default since that will also be the destination calendar
|
||||
if (onboarding && externalId) {
|
||||
const calendarIndex = (connectedCalendars[0].calendars || []).findIndex(
|
||||
@@ -113,6 +120,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda
|
||||
data: {
|
||||
integration,
|
||||
externalId,
|
||||
primaryEmail,
|
||||
},
|
||||
});
|
||||
} else if (onboarding && !destinationCal.isSelected) {
|
||||
|
||||
@@ -272,7 +272,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: booking?.user?.email as string,
|
||||
email: booking?.userPrimaryEmail ?? (booking?.user?.email as string),
|
||||
name: booking?.user?.name ?? "Nameless",
|
||||
timeZone: booking?.user?.timeZone as string,
|
||||
language: { translate: tOrganizer, locale: booking?.user?.locale ?? "en" },
|
||||
|
||||
@@ -30,6 +30,8 @@ export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinati
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` });
|
||||
}
|
||||
|
||||
const primaryEmail = allCals.find((cal) => cal.primary && cal.credentialId === credentialId)?.email ?? null;
|
||||
|
||||
let where;
|
||||
|
||||
if (eventTypeId) {
|
||||
@@ -56,12 +58,14 @@ export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinati
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
primaryEmail,
|
||||
},
|
||||
create: {
|
||||
...where,
|
||||
integration,
|
||||
externalId,
|
||||
credentialId,
|
||||
primaryEmail,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -49,6 +49,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
||||
eventTypeId: true,
|
||||
responses: true,
|
||||
metadata: true,
|
||||
userPrimaryEmail: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -168,7 +169,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
email: user.email,
|
||||
email: booking.userPrimaryEmail ?? user.email,
|
||||
name: user.name || "Unnamed",
|
||||
username: user.username || undefined,
|
||||
timeZone: user.timeZone,
|
||||
|
||||
@@ -74,7 +74,7 @@ export const editLocationHandler = async ({ ctx, input }: EditLocationOptions) =
|
||||
startTime: booking.startTime ? dayjs(booking.startTime).format() : "",
|
||||
endTime: booking.endTime ? dayjs(booking.endTime).format() : "",
|
||||
organizer: {
|
||||
email: organizer.email,
|
||||
email: booking?.userPrimaryEmail ?? organizer.email,
|
||||
name: organizer.name ?? "Nameless",
|
||||
timeZone: organizer.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
|
||||
|
||||
@@ -53,6 +53,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventTypeId: true,
|
||||
userPrimaryEmail: true,
|
||||
eventType: {
|
||||
include: {
|
||||
team: {
|
||||
@@ -180,6 +181,10 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
|
||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||
const organizer = {
|
||||
...userAsPeopleType,
|
||||
email: bookingToReschedule?.userPrimaryEmail ?? userAsPeopleType.email,
|
||||
};
|
||||
|
||||
const builder = new CalendarEventBuilder();
|
||||
const eventType = bookingToReschedule.eventType;
|
||||
@@ -196,7 +201,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
organizer: userAsPeopleType,
|
||||
organizer,
|
||||
iCalUID: bookingToReschedule.iCalUID,
|
||||
});
|
||||
|
||||
@@ -257,7 +262,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
}),
|
||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
organizer: userAsPeopleType,
|
||||
organizer,
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
|
||||
@@ -67,7 +67,7 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions
|
||||
startTime: dayjs(booking.startTime).format(),
|
||||
endTime: dayjs(booking.endTime).format(),
|
||||
organizer: {
|
||||
email: booking.user?.email || "",
|
||||
email: booking?.userPrimaryEmail ?? booking.user?.email ?? "",
|
||||
name: booking.user?.name || "Nameless",
|
||||
timeZone: booking.user?.timeZone || "",
|
||||
language: { translate: tOrganizer, locale: booking.user?.locale ?? "en" },
|
||||
|
||||
@@ -169,7 +169,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
|
||||
organizer: booking.user
|
||||
? {
|
||||
name: booking.user.name || "",
|
||||
email: booking.user.email,
|
||||
email: booking?.userPrimaryEmail ?? booking.user.email,
|
||||
timeZone: booking.user.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat),
|
||||
language: { locale: booking.user.locale || defaultLocale },
|
||||
|
||||
@@ -288,7 +288,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
? {
|
||||
language: { locale: booking.user.locale || defaultLocale },
|
||||
name: booking.user.name || "",
|
||||
email: booking.user.email,
|
||||
email: booking?.userPrimaryEmail ?? booking.user.email,
|
||||
timeZone: booking.user.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat),
|
||||
}
|
||||
@@ -525,7 +525,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
? {
|
||||
language: { locale: booking.user.locale || defaultLocale },
|
||||
name: booking.user.name || "",
|
||||
email: booking.user.email,
|
||||
email: booking?.userPrimaryEmail ?? booking.user.email,
|
||||
timeZone: booking.user.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat),
|
||||
}
|
||||
@@ -669,7 +669,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
organizer: booking.user
|
||||
? {
|
||||
name: booking.user.name || "",
|
||||
email: booking.user.email,
|
||||
email: booking?.userPrimaryEmail ?? booking.user.email,
|
||||
timeZone: booking.user.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat),
|
||||
language: { locale: booking.user.locale || defaultLocale },
|
||||
|
||||
Reference in new issue
Block a user