fix: refresh slots on timezone change for booker timezone restrictions (#27491)
* fix: refresh slots on timezone change for booker timezone restrictions * refactor: use useMemo for timezone change detection * revert: remove unnecessary formatting changes * feat: add timezone refresh for platform components Add timezone change detection and slot refresh to BookerPlatformWrapper and EventTypeCalendarViewComponent to handle restriction schedules with useBookerTimezone enabled. * refactor: extract timezone slot refresh logic into reusable hook * Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> * Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> * fix * fix: prevent unnecessary getSchedule calls when useBookerTimezone is disabled * fix: add timezone fields to BookerEvent type * fix: add missing properties to BookerEvent and BookerEventProfile types * trying to fix type errors * Add restrictionScheduleId and useBookerTimezone fields * fix: correct import path for useBookerTime hook Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: explicitly include restrictionScheduleId and useBookerTimezone in getPublicEvent return Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * chore: trigger fresh CI build Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: cast event.data to BookerEvent for timezone fields access Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: address review feedback for timezone slot refresh Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: add explicit return type to event handler to ensure type propagation Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: extract useStableTimezone hook and remove dead timezone detection code Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> --------- Co-authored-by: hackice20 <yashkam431@gmail.com> Co-authored-by: Yash <116657771+hackice20@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
ali@cal.com
hackice20
Yash
Morgan
Devin AI
parent
ab4eff1fe1
commit
4c73695d3a
@@ -19,7 +19,7 @@ import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store
|
||||
import { useEvent, useScheduleForEvent } from "@calcom/web/modules/schedules/hooks/useEvent";
|
||||
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
|
||||
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -42,11 +42,11 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen
|
||||
});
|
||||
const event = props.eventData
|
||||
? {
|
||||
data: props.eventData,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
}
|
||||
data: props.eventData,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
}
|
||||
: clientFetchedEvent;
|
||||
|
||||
const bookerLayout = useBookerLayout(event.data?.profile?.bookerLayouts);
|
||||
@@ -149,6 +149,9 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen
|
||||
useApiV2: props.useApiV2,
|
||||
bookerLayout,
|
||||
...(props.entity.orgSlug ? { orgSlug: props.entity.orgSlug } : {}),
|
||||
restrictionSchedule: event.data?.restrictionScheduleId
|
||||
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
|
||||
: undefined,
|
||||
});
|
||||
const bookings = useBookings({
|
||||
event,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime";
|
||||
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
|
||||
|
||||
export type useEventReturnType = ReturnType<typeof useEvent>;
|
||||
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
|
||||
@@ -71,6 +72,7 @@ export const useScheduleForEvent = ({
|
||||
isTeamEvent,
|
||||
useApiV2 = true,
|
||||
bookerLayout,
|
||||
restrictionSchedule,
|
||||
}: {
|
||||
username?: string | null;
|
||||
eventSlug?: string | null;
|
||||
@@ -92,13 +94,16 @@ export const useScheduleForEvent = ({
|
||||
extraDays: number;
|
||||
columnViewExtraDays: { current: number };
|
||||
};
|
||||
restrictionSchedule?: { id: number | null; useBookerTimezone: boolean };
|
||||
}) => {
|
||||
const { timezone } = useBookerTime();
|
||||
const { timezone: rawTimezone } = useBookerTime();
|
||||
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
|
||||
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
|
||||
shallow
|
||||
);
|
||||
|
||||
const effectiveTimezone = useStableTimezone(rawTimezone, restrictionSchedule);
|
||||
|
||||
const searchParams = useCompatSearchParams();
|
||||
const rescheduleUid = searchParams?.get("rescheduleUid");
|
||||
|
||||
@@ -106,7 +111,7 @@ export const useScheduleForEvent = ({
|
||||
username: usernameFromStore ?? username,
|
||||
eventSlug: eventSlugFromStore ?? eventSlug,
|
||||
eventId,
|
||||
timezone,
|
||||
timezone: effectiveTimezone,
|
||||
selectedDate,
|
||||
dayCount,
|
||||
rescheduleUid,
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { useStableTimezone } from "./useStableTimezone";
|
||||
|
||||
describe("useStableTimezone", () => {
|
||||
it("returns the raw timezone when no restriction schedule is provided", () => {
|
||||
const { result } = renderHook(() => useStableTimezone("America/New_York"));
|
||||
expect(result.current).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("returns the raw timezone when restriction schedule is undefined", () => {
|
||||
const { result } = renderHook(() => useStableTimezone("America/New_York", undefined));
|
||||
expect(result.current).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("returns the raw timezone when restriction schedule id is null", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useStableTimezone("America/New_York", { id: null, useBookerTimezone: false })
|
||||
);
|
||||
expect(result.current).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("returns the raw timezone when restriction schedule id is 0", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useStableTimezone("America/New_York", { id: 0, useBookerTimezone: false })
|
||||
);
|
||||
expect(result.current).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("returns the raw timezone when useBookerTimezone is true (timezone should follow the booker)", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useStableTimezone("America/New_York", { id: 1, useBookerTimezone: true })
|
||||
);
|
||||
expect(result.current).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("pins to initial timezone when restriction schedule exists and useBookerTimezone is false", () => {
|
||||
let timezone = "America/New_York";
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useStableTimezone(timezone, { id: 1, useBookerTimezone: false })
|
||||
);
|
||||
expect(result.current).toBe("America/New_York");
|
||||
|
||||
timezone = "Europe/London";
|
||||
rerender();
|
||||
expect(result.current).toBe("America/New_York");
|
||||
});
|
||||
|
||||
it("follows timezone changes when useBookerTimezone is true", () => {
|
||||
let timezone = "America/New_York";
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useStableTimezone(timezone, { id: 1, useBookerTimezone: true })
|
||||
);
|
||||
expect(result.current).toBe("America/New_York");
|
||||
|
||||
timezone = "Europe/London";
|
||||
rerender();
|
||||
expect(result.current).toBe("Europe/London");
|
||||
});
|
||||
|
||||
it("follows timezone changes when there is no restriction schedule", () => {
|
||||
let timezone = "America/New_York";
|
||||
const { result, rerender } = renderHook(() => useStableTimezone(timezone));
|
||||
expect(result.current).toBe("America/New_York");
|
||||
|
||||
timezone = "Europe/London";
|
||||
rerender();
|
||||
expect(result.current).toBe("Europe/London");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
export function useStableTimezone(
|
||||
timezone: string,
|
||||
restrictionSchedule?: { id: number | null; useBookerTimezone: boolean }
|
||||
): string {
|
||||
const initialRef = useRef(timezone);
|
||||
const shouldPin =
|
||||
restrictionSchedule?.id != null &&
|
||||
restrictionSchedule.id > 0 &&
|
||||
restrictionSchedule.useBookerTimezone === false;
|
||||
return shouldPin ? initialRef.current : timezone;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { ErrorOption, FieldPath } from "react-hook-form";
|
||||
|
||||
import type { RegularBookingCreateResult } from "@calcom/features/bookings/lib/dto/types";
|
||||
import type { Slots } from "@calcom/features/calendars/lib/types";
|
||||
import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
@@ -26,7 +24,10 @@ type BookerEventUser = Pick<
|
||||
bookerUrl: string;
|
||||
};
|
||||
|
||||
type BookerEventProfile = Pick<PublicEvent["profile"], "name" | "image" | "bookerLayouts">;
|
||||
type BookerEventProfile = Pick<
|
||||
PublicEvent["profile"],
|
||||
"name" | "image" | "bookerLayouts" | "brandColor" | "darkBrandColor" | "theme" | "weekStart" | "username"
|
||||
>;
|
||||
|
||||
// Re-export Slots from the server-safe location
|
||||
export type { Slots };
|
||||
@@ -40,10 +41,10 @@ export type BookerEvent = Pick<
|
||||
| "recurringEvent"
|
||||
| "entity"
|
||||
| "locations"
|
||||
| "enablePerHostLocations"
|
||||
| "metadata"
|
||||
| "isDynamic"
|
||||
| "requiresConfirmation"
|
||||
| "requiresBookerEmailVerification"
|
||||
| "price"
|
||||
| "currency"
|
||||
| "lockTimeZoneToggleOnBookingPage"
|
||||
@@ -66,13 +67,14 @@ export type BookerEvent = Pick<
|
||||
| "interfaceLanguage"
|
||||
| "team"
|
||||
| "owner"
|
||||
| "restrictionScheduleId"
|
||||
| "useBookerTimezone"
|
||||
> & {
|
||||
subsetOfUsers: BookerEventUser[];
|
||||
showInstantEventConnectNowModal: boolean;
|
||||
enablePerHostLocations?: boolean;
|
||||
} & { profile: BookerEventProfile };
|
||||
|
||||
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];
|
||||
|
||||
export type EventPrice = { currency: string; price: number; displayAlternateSymbol?: boolean };
|
||||
|
||||
export enum EventDetailBlocks {
|
||||
|
||||
@@ -172,6 +172,8 @@ export const getPublicEventSelect = (fetchAllUsers: boolean) => {
|
||||
hidden: true,
|
||||
assignAllTeamMembers: true,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
restrictionScheduleId: true,
|
||||
useBookerTimezone: true,
|
||||
parent: {
|
||||
select: {
|
||||
team: {
|
||||
@@ -341,6 +343,8 @@ export const getPublicEvent = async (
|
||||
return {
|
||||
...defaultEvent,
|
||||
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
|
||||
restrictionScheduleId: null,
|
||||
useBookerTimezone: false,
|
||||
// Clears meta data since we don't want to send this in the public api.
|
||||
subsetOfUsers: users.map((user) => ({
|
||||
...user,
|
||||
@@ -592,6 +596,8 @@ export const getPublicEvent = async (
|
||||
disableRescheduling: event.disableRescheduling,
|
||||
allowReschedulingCancelledBookings: event.allowReschedulingCancelledBookings,
|
||||
interfaceLanguage: event.interfaceLanguage,
|
||||
restrictionScheduleId: event.restrictionScheduleId,
|
||||
useBookerTimezone: event.useBookerTimezone,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import {
|
||||
useBookerStoreContext,
|
||||
useInitializeBookerStoreContext,
|
||||
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "@calcom/features/bookings/Booker/hooks/useBookingForm";
|
||||
import { useLocalSet } from "@calcom/features/bookings/Booker/hooks/useLocalSet";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
|
||||
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
|
||||
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import type { ConnectedDestinationCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||
@@ -243,7 +244,13 @@ const BookerPlatformWrapperComponent = (
|
||||
return restFormValues;
|
||||
}, [restFormValues]);
|
||||
|
||||
const { timezone } = useTimePreferences();
|
||||
const { timezone: rawTimezone } = useTimePreferences();
|
||||
const timezone = useStableTimezone(
|
||||
rawTimezone,
|
||||
event?.data?.restrictionScheduleId != null
|
||||
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
|
||||
: undefined
|
||||
);
|
||||
|
||||
const [calculatedStartTime, calculatedEndTime] = useTimesForSchedule({
|
||||
month,
|
||||
@@ -315,7 +322,7 @@ const BookerPlatformWrapperComponent = (
|
||||
}
|
||||
}, [schedule.data, schedule.isPending, schedule.error, onTimeslotsLoaded]);
|
||||
|
||||
const bookerForm = useBookingForm({
|
||||
const bookerForm= useBookingForm({
|
||||
event: event?.data,
|
||||
sessionEmail:
|
||||
session?.data?.email && clientId
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Header } from "@calcom/features/bookings/components/Header";
|
||||
import { BookerSection } from "@calcom/features/bookings/components/Section";
|
||||
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/hooks/useAvailableTimeSlots";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/hooks/useBookerLayout";
|
||||
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { LargeCalendar } from "./components/LargeCalendar";
|
||||
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||
@@ -68,7 +69,13 @@ export const EventTypeCalendarViewComponent = (
|
||||
bookerLayout,
|
||||
});
|
||||
|
||||
const { timezone } = useTimePreferences();
|
||||
const { timezone: rawTimezone } = useTimePreferences();
|
||||
const timezone = useStableTimezone(
|
||||
rawTimezone,
|
||||
event?.data?.restrictionScheduleId != null
|
||||
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
|
||||
: undefined
|
||||
);
|
||||
const isDynamic = useMemo(() => {
|
||||
return getUsernameList(username ?? "").length > 1;
|
||||
}, [username]);
|
||||
@@ -112,7 +119,7 @@ export const EventTypeCalendarViewComponent = (
|
||||
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const eventDuration = selectedEventDuration || event?.data?.length || 30;
|
||||
|
||||
const availableTimeSlots = useAvailableTimeSlots({ schedule: schedule.data, eventDuration });
|
||||
const availableTimeSlots= useAvailableTimeSlots({ schedule: schedule.data, eventDuration });
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
|
||||
|
||||
import type { TEventInputSchema } from "./event.schema";
|
||||
@@ -7,7 +8,7 @@ interface EventHandlerOptions {
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
export const eventHandler = async ({ input, userId }: EventHandlerOptions) => {
|
||||
export const eventHandler = async ({ input, userId }: EventHandlerOptions): Promise<PublicEventType> => {
|
||||
return await EventRepository.getPublicEvent(input, userId);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user