diff --git a/apps/web/playwright/event-types.e2e.ts b/apps/web/playwright/event-types.e2e.ts index a6f278abcc..9303198502 100644 --- a/apps/web/playwright/event-types.e2e.ts +++ b/apps/web/playwright/event-types.e2e.ts @@ -420,6 +420,33 @@ test.describe("Event Types tests", () => { await page.click("[data-testid=vertical-tab-event_advanced_tab_title]"); await expect(offerSeatsToggle).toBeDisabled(); }); + test("should enable timezone lock in event advanced settings and verify disabled timezone selector on booking page", async ({ + page, + users, + }) => { + await gotoFirstEventType(page); + await expect(page.locator("[data-testid=event-title]")).toBeVisible(); + await page.click("[data-testid=vertical-tab-event_advanced_tab_title]"); + await page.click("[data-testid=lock-timezone-toggle]"); + await page.click("[data-testid=timezone-select]"); + await page.locator('[aria-label="Timezone Select"]').fill("New York"); + await page.keyboard.press("Enter"); + + await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", { + action: () => page.locator("[data-testid=update-eventtype]").click(), + }); + await page.goto("/event-types"); + const previewLink = await page + .locator("[data-testid=preview-link-button]") + .first() + .getAttribute("href"); + + await page.goto(previewLink ?? ""); + const currentTimezone = page.locator('[data-testid="event-meta-current-timezone"]'); + await expect(currentTimezone).toBeVisible(); + await expect(currentTimezone).toHaveClass(/cursor-not-allowed/); + await expect(page.getByText("New York")).toBeVisible(); + }); }); test.describe("Interface Language Tests", () => { diff --git a/apps/web/server/lib/[user]/getServerSideProps.ts b/apps/web/server/lib/[user]/getServerSideProps.ts index 26e46ef97f..a41a603ded 100644 --- a/apps/web/server/lib/[user]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/getServerSideProps.ts @@ -60,6 +60,7 @@ type UserPageProps = { | "length" | "hidden" | "lockTimeZoneToggleOnBookingPage" + | "lockedTimeZone" | "requiresConfirmation" | "canSendCalVideoTranscriptionEmails" | "requiresBookerEmailVerification" diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index 0cf671d3c7..6f24f398cb 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -438,7 +438,6 @@ describe("handleChildrenEventTypes", () => { schedulingType: SchedulingType.MANAGED, requiresBookerEmailVerification: false, lockTimeZoneToggleOnBookingPage: false, - lockedTimeZone: "Europe/London", useEventTypeDestinationCalendarEmail: false, workflows: [], parentId: 1, diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 07affdc549..3231274aae 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -58,6 +58,7 @@ export const EventMeta = ({ event?: Pick< BookerEvent, | "lockTimeZoneToggleOnBookingPage" + | "lockedTimeZone" | "schedule" | "seatsPerTimeSlot" | "subsetOfUsers" @@ -112,9 +113,12 @@ export const EventMeta = ({ ); useEffect(() => { - //In case the event has lockTimeZone enabled ,set the timezone to event's attached availability timezone - if (event && event?.lockTimeZoneToggleOnBookingPage && event?.schedule?.timeZone) { - setTimezone(event.schedule?.timeZone); + //In case the event has lockTimeZone enabled ,set the timezone to event's locked timezone + if (event?.lockTimeZoneToggleOnBookingPage) { + const timezone = event.lockedTimeZone || event.schedule?.timeZone; + if (timezone) { + setTimezone(timezone); + } } }, [event, setTimezone]); @@ -217,7 +221,8 @@ export const EventMeta = ({ + }`} + data-testid="event-meta-current-timezone"> "ml-auto", container: () => "max-w-full", }} - value={event.lockTimeZoneToggleOnBookingPage ? CURRENT_TIMEZONE : timezone} + value={ + event.lockTimeZoneToggleOnBookingPage + ? event.lockedTimeZone || CURRENT_TIMEZONE + : timezone + } onChange={({ value }) => { setTimezone(value); setBookerStoreTimezone(value); diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts index 960f0f249c..fb46424865 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts @@ -65,6 +65,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => { periodDays: true, periodCountCalendarDays: true, lockTimeZoneToggleOnBookingPage: true, + lockedTimeZone: true, requiresConfirmation: true, requiresConfirmationForFreeEmail: true, requiresBookerEmailVerification: true, diff --git a/packages/features/bookings/types.ts b/packages/features/bookings/types.ts index 178c769c1c..91647f672f 100644 --- a/packages/features/bookings/types.ts +++ b/packages/features/bookings/types.ts @@ -46,6 +46,7 @@ export type BookerEvent = Pick< | "price" | "currency" | "lockTimeZoneToggleOnBookingPage" + | "lockedTimeZone" | "schedule" | "seatsPerTimeSlot" | "title" diff --git a/packages/features/components/timezone-select/TimezoneSelect.tsx b/packages/features/components/timezone-select/TimezoneSelect.tsx index 337f0e008a..6c0a29c632 100644 --- a/packages/features/components/timezone-select/TimezoneSelect.tsx +++ b/packages/features/components/timezone-select/TimezoneSelect.tsx @@ -108,6 +108,7 @@ export function TimezoneSelectComponent({ className={`${className} ${timezoneSelectCustomClassname}`} aria-label="Timezone Select" isLoading={isPending} + data-testid="timezone-select" isDisabled={isPending} {...reactSelectProps} timezones={{ diff --git a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx index bfb21d5644..dced1ca619 100644 --- a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx +++ b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx @@ -12,6 +12,7 @@ import { } from "@calcom/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsWebWrapper"; import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; +import { TimezoneSelect } from "@calcom/features/components/timezone-select"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; import { allowDisablingAttendeeConfirmationEmails, @@ -1033,23 +1034,63 @@ export const EventAdvancedTab = ({ /> ( - onChange(e)} - data-testid="lock-timezone-toggle" - /> - )} + render={({ field: { value, onChange } }) => { + // Calculate if we should show the selector based on current form state & handle backward compatibility + const currentLockedTimeZone = formMethods.getValues("lockedTimeZone"); + const showSelector = + value && + (!(eventType.lockTimeZoneToggleOnBookingPage && !eventType.lockedTimeZone) || + !!currentLockedTimeZone); + + return ( + { + onChange(e); + const lockedTimeZone = e ? eventType.lockedTimeZone ?? "Europe/London" : null; + formMethods.setValue("lockedTimeZone", lockedTimeZone, { shouldDirty: true }); + }} + data-testid="lock-timezone-toggle" + childrenClassName="lg:ml-0"> + {showSelector && ( +
+
+ ( + <> + + { + if (event) + formMethods.setValue("lockedTimeZone", event.value, { shouldDirty: true }); + }} + /> + + )} + /> +
+
+ )} +
+ ); + }} /> { disableGuests: true, metadata: true, lockTimeZoneToggleOnBookingPage: true, + lockedTimeZone: true, requiresConfirmation: true, autoTranslateDescriptionEnabled: true, fieldTranslations: { diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts index be6600c70d..2092c1a256 100644 --- a/packages/features/eventtypes/lib/types.ts +++ b/packages/features/eventtypes/lib/types.ts @@ -88,6 +88,7 @@ export type FormValues = { description: string; disableGuests: boolean; lockTimeZoneToggleOnBookingPage: boolean; + lockedTimeZone: string | null; requiresConfirmation: boolean; requiresConfirmationWillBlockSlot: boolean; requiresConfirmationForFreeEmail: boolean; diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 761c7ed9cd..7f90985e92 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -100,6 +100,7 @@ const commons = { destinationCalendar: null, team: null, lockTimeZoneToggleOnBookingPage: false, + lockedTimeZone: null, requiresConfirmation: false, requiresConfirmationForFreeEmail: false, requiresBookerEmailVerification: false, diff --git a/packages/lib/event-types/getEventTypesPublic.ts b/packages/lib/event-types/getEventTypesPublic.ts index 34f462d2e2..a884d21abe 100644 --- a/packages/lib/event-types/getEventTypesPublic.ts +++ b/packages/lib/event-types/getEventTypesPublic.ts @@ -35,14 +35,14 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => { const eventTypes = await prisma.$queryRaw` SELECT data."id", data."title", data."description", data."length", data."schedulingType"::text, data."recurringEvent", data."slug", data."hidden", data."price", data."currency", - data."lockTimeZoneToggleOnBookingPage", data."requiresConfirmation", data."requiresBookerEmailVerification", + data."lockTimeZoneToggleOnBookingPage", data."lockedTimeZone", data."requiresConfirmation", data."requiresBookerEmailVerification", data."metadata", data."canSendCalVideoTranscriptionEmails" FROM ( SELECT "EventType"."id", "EventType"."title", "EventType"."description", "EventType"."position", "EventType"."length", "EventType"."schedulingType"::text, "EventType"."recurringEvent", "EventType"."slug", "EventType"."hidden", "EventType"."price", "EventType"."currency", - "EventType"."lockTimeZoneToggleOnBookingPage", "EventType"."requiresConfirmation", + "EventType"."lockTimeZoneToggleOnBookingPage", "EventType"."lockedTimeZone", "EventType"."requiresConfirmation", "EventType"."requiresBookerEmailVerification", "EventType"."metadata", "EventType"."canSendCalVideoTranscriptionEmails" FROM "EventType" @@ -52,7 +52,7 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => { "EventType"."position", "EventType"."length", "EventType"."schedulingType"::text, "EventType"."recurringEvent", "EventType"."slug", "EventType"."hidden", "EventType"."price", "EventType"."currency", - "EventType"."lockTimeZoneToggleOnBookingPage", "EventType"."requiresConfirmation", + "EventType"."lockTimeZoneToggleOnBookingPage", "EventType"."lockedTimeZone", "EventType"."requiresConfirmation", "EventType"."requiresBookerEmailVerification", "EventType"."metadata", "EventType"."canSendCalVideoTranscriptionEmails" FROM "EventType" diff --git a/packages/lib/server/eventTypeSelect.ts b/packages/lib/server/eventTypeSelect.ts index 06c20e1f57..bfad34da4f 100644 --- a/packages/lib/server/eventTypeSelect.ts +++ b/packages/lib/server/eventTypeSelect.ts @@ -34,6 +34,7 @@ export const eventTypeSelect = { periodDays: true, periodCountCalendarDays: true, lockTimeZoneToggleOnBookingPage: true, + lockedTimeZone: true, requiresBookerEmailVerification: true, disableGuests: true, disableCancelling: true, diff --git a/packages/lib/server/repository/eventTypeRepository.ts b/packages/lib/server/repository/eventTypeRepository.ts index 1ffa6dbd1d..18bd8f960b 100644 --- a/packages/lib/server/repository/eventTypeRepository.ts +++ b/packages/lib/server/repository/eventTypeRepository.ts @@ -548,6 +548,7 @@ export class EventTypeRepository { periodEndDate: true, periodCountCalendarDays: true, lockTimeZoneToggleOnBookingPage: true, + lockedTimeZone: true, requiresConfirmation: true, requiresConfirmationForFreeEmail: true, canSendCalVideoTranscriptionEmails: true, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 3860088336..f967e421c2 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -112,6 +112,7 @@ export const buildEventType = (eventType?: Partial): EventType => { periodCountCalendarDays: null, recurringEvent: null, lockTimeZoneToggleOnBookingPage: false, + lockedTimeZone: null, requiresConfirmation: false, requiresConfirmationForFreeEmail: false, requiresConfirmationWillBlockSlot: false, diff --git a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts index 94fcb98228..0bca6f53f7 100644 --- a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts +++ b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts @@ -59,6 +59,7 @@ export const useEventTypeForm = ({ seatsShowAttendees: eventType.seatsShowAttendees, seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount, lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage, + lockedTimeZone: eventType.lockedTimeZone || null, locations: eventType.locations || [], destinationCalendar: eventType.destinationCalendar, recurringEvent: eventType.recurringEvent || null, diff --git a/packages/prisma/migrations/20250715154531_event_locked_timezone/migration.sql b/packages/prisma/migrations/20250715154531_event_locked_timezone/migration.sql new file mode 100644 index 0000000000..653b2dd099 --- /dev/null +++ b/packages/prisma/migrations/20250715154531_event_locked_timezone/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "lockedTimeZone" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 8298a97a79..c9c8bd6245 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -130,6 +130,7 @@ model EventType { periodDays Int? periodCountCalendarDays Boolean? lockTimeZoneToggleOnBookingPage Boolean @default(false) + lockedTimeZone String? requiresConfirmation Boolean @default(false) requiresConfirmationWillBlockSlot Boolean @default(false) requiresConfirmationForFreeEmail Boolean @default(false) diff --git a/packages/prisma/selects/event-types.ts b/packages/prisma/selects/event-types.ts index 5d76de3973..f98fd34894 100644 --- a/packages/prisma/selects/event-types.ts +++ b/packages/prisma/selects/event-types.ts @@ -12,6 +12,7 @@ export const baseEventTypeSelect = { price: true, currency: true, lockTimeZoneToggleOnBookingPage: true, + lockedTimeZone: true, requiresConfirmation: true, requiresBookerEmailVerification: true, canSendCalVideoTranscriptionEmails: true, @@ -31,6 +32,7 @@ export const bookEventTypeSelect = { periodEndDate: true, recurringEvent: true, lockTimeZoneToggleOnBookingPage: true, + lockedTimeZone: true, requiresConfirmation: true, canSendCalVideoTranscriptionEmails: true, requiresBookerEmailVerification: true, diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index cdecff651f..32e9d2495e 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -671,6 +671,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit