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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user