feat: Ability to set a locked timezone for event type (#22531)

* feat: ability to set locked timezone

* update

* Update update-event-type.input.ts

* Update get-event-type-public.output.ts

* Update documentation.json

* revert

* nit

* feat: locked timezone

* update

* fix

* Update handleChildrenEventTypes.test.ts

* update

* fix test
This commit is contained in:
Anik Dhabal Babu
2025-07-29 10:20:35 +00:00
committed by GitHub
parent b21400c613
commit a11ea3dd1e
20 changed files with 119 additions and 26 deletions
+27
View File
@@ -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", () => {
@@ -60,6 +60,7 @@ type UserPageProps = {
| "length"
| "hidden"
| "lockTimeZoneToggleOnBookingPage"
| "lockedTimeZone"
| "requiresConfirmation"
| "canSendCalVideoTranscriptionEmails"
| "requiresBookerEmailVerification"
@@ -438,7 +438,6 @@ describe("handleChildrenEventTypes", () => {
schedulingType: SchedulingType.MANAGED,
requiresBookerEmailVerification: false,
lockTimeZoneToggleOnBookingPage: false,
lockedTimeZone: "Europe/London",
useEventTypeDestinationCalendarEmail: false,
workflows: [],
parentId: 1,
@@ -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 = ({
<span
className={`current-timezone before:bg-subtle min-w-32 -mt-[2px] flex h-6 max-w-full items-center justify-start before:absolute before:inset-0 before:bottom-[-3px] before:left-[-30px] before:top-[-3px] before:w-[calc(100%_+_35px)] before:rounded-md before:py-3 before:opacity-0 before:transition-opacity ${
event.lockTimeZoneToggleOnBookingPage ? "cursor-not-allowed" : ""
}`}>
}`}
data-testid="event-meta-current-timezone">
<TimezoneSelect
timeZones={timeZones}
menuPosition="absolute"
@@ -230,7 +235,11 @@ export const EventMeta = ({
indicatorsContainer: () => "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);
@@ -65,6 +65,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
periodDays: true,
periodCountCalendarDays: true,
lockTimeZoneToggleOnBookingPage: true,
lockedTimeZone: true,
requiresConfirmation: true,
requiresConfirmationForFreeEmail: true,
requiresBookerEmailVerification: true,
+1
View File
@@ -46,6 +46,7 @@ export type BookerEvent = Pick<
| "price"
| "currency"
| "lockTimeZoneToggleOnBookingPage"
| "lockedTimeZone"
| "schedule"
| "seatsPerTimeSlot"
| "title"
@@ -108,6 +108,7 @@ export function TimezoneSelectComponent({
className={`${className} ${timezoneSelectCustomClassname}`}
aria-label="Timezone Select"
isLoading={isPending}
data-testid="timezone-select"
isDisabled={isPending}
{...reactSelectProps}
timezones={{
@@ -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 = ({
/>
<Controller
name="lockTimeZoneToggleOnBookingPage"
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName={classNames("text-sm", customClassNames?.timezoneLock?.label)}
descriptionClassName={customClassNames?.timezoneLock?.description}
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
customClassNames?.timezoneLock?.container
)}
title={t("lock_timezone_toggle_on_booking_page")}
{...lockTimeZoneToggleOnBookingPageLocked}
description={t("description_lock_timezone_toggle_on_booking_page")}
checked={value}
onCheckedChange={(e) => 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 (
<SettingsToggle
labelClassName={classNames("text-sm", customClassNames?.timezoneLock?.label)}
descriptionClassName={customClassNames?.timezoneLock?.description}
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
customClassNames?.timezoneLock?.container,
showSelector && "rounded-b-none"
)}
title={t("lock_timezone_toggle_on_booking_page")}
{...lockTimeZoneToggleOnBookingPageLocked}
description={t("description_lock_timezone_toggle_on_booking_page")}
checked={value}
onCheckedChange={(e) => {
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 && (
<div className="border-subtle flex flex-col gap-6 rounded-b-lg border border-t-0 p-6">
<div>
<Controller
name="lockedTimeZone"
control={formMethods.control}
render={({ field: { value } }) => (
<>
<Label className="text-default mb-2 block text-sm font-medium">
<>{t("timezone")}</>
</Label>
<TimezoneSelect
id="lockedTimeZone"
value={value ?? "Europe/London"}
onChange={(event) => {
if (event)
formMethods.setValue("lockedTimeZone", event.value, { shouldDirty: true });
}}
/>
</>
)}
/>
</div>
</div>
)}
</SettingsToggle>
);
}}
/>
<Controller
name="allowReschedulingPastBookings"
@@ -68,6 +68,7 @@ export const getPublicEventSelect = (fetchAllUsers: boolean) => {
disableGuests: true,
metadata: true,
lockTimeZoneToggleOnBookingPage: true,
lockedTimeZone: true,
requiresConfirmation: true,
autoTranslateDescriptionEnabled: true,
fieldTranslations: {
@@ -88,6 +88,7 @@ export type FormValues = {
description: string;
disableGuests: boolean;
lockTimeZoneToggleOnBookingPage: boolean;
lockedTimeZone: string | null;
requiresConfirmation: boolean;
requiresConfirmationWillBlockSlot: boolean;
requiresConfirmationForFreeEmail: boolean;
+1
View File
@@ -100,6 +100,7 @@ const commons = {
destinationCalendar: null,
team: null,
lockTimeZoneToggleOnBookingPage: false,
lockedTimeZone: null,
requiresConfirmation: false,
requiresConfirmationForFreeEmail: false,
requiresBookerEmailVerification: false,
@@ -35,14 +35,14 @@ const getEventTypesWithHiddenFromDB = async (userId: number) => {
const eventTypes = await prisma.$queryRaw<RawEventType[]>`
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"
+1
View File
@@ -34,6 +34,7 @@ export const eventTypeSelect = {
periodDays: true,
periodCountCalendarDays: true,
lockTimeZoneToggleOnBookingPage: true,
lockedTimeZone: true,
requiresBookerEmailVerification: true,
disableGuests: true,
disableCancelling: true,
@@ -548,6 +548,7 @@ export class EventTypeRepository {
periodEndDate: true,
periodCountCalendarDays: true,
lockTimeZoneToggleOnBookingPage: true,
lockedTimeZone: true,
requiresConfirmation: true,
requiresConfirmationForFreeEmail: true,
canSendCalVideoTranscriptionEmails: true,
+1
View File
@@ -112,6 +112,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
periodCountCalendarDays: null,
recurringEvent: null,
lockTimeZoneToggleOnBookingPage: false,
lockedTimeZone: null,
requiresConfirmation: false,
requiresConfirmationForFreeEmail: false,
requiresConfirmationWillBlockSlot: false,
@@ -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,
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "lockedTimeZone" TEXT;
+1
View File
@@ -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)
+2
View File
@@ -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,
+1
View File
@@ -671,6 +671,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
maxActiveBookingsPerBooker: true,
maxActiveBookingPerBookerOfferReschedule: true,
lockTimeZoneToggleOnBookingPage: true,
lockedTimeZone: true,
requiresBookerEmailVerification: true,
assignAllTeamMembers: true,
isRRWeightsEnabled: true,