feat: don't update query params for booker atom (#20000)

* feat: Add `omitUpdatingParams` prop to Booker store for query param control

* remove console

* Update booking.tsx

* fix: type check failing

* undo changes

* feat: Expose `onBookerStateChange` callback to track `booker-store` updates

* add `onBookerStateChange` docs

* Update store.ts

* added `allowUpdatingUrlParams` for backwards compatibility
This commit is contained in:
Somay Chauhan authored and GitHub committed 2025-03-13 23:47:24 +05:30
1 parent f8aa2701b6
commit b1ebefb854
5 files changed
+141 -15

No files matched your search

+2
View File
@@ -120,6 +120,8 @@ Below is a list of props that can be passed to the booker atom.
| view | No | Specifies the layout of the booker atom into column, week, or month view |
| metadata | No | Used to pass custom metadata values into the booker. Metadata should be an object eg: `{ bookingSource: "website", userRole: "admin" }` |
| bannerUrl | No | Adds custom banner to the booker atom |
| onBookerStateChange | No | Callback function that is triggered when the state of the booker atom changes. |
| allowUpdatingUrlParams | No | Boolean indicating if the URL parameters should be updated, defaults to false. |
Along with the props, booker atom accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop.
<p></p>
+49 -13
View File
@@ -36,6 +36,8 @@ type StoreInitializeType = {
teamMemberEmail?: string | null;
crmOwnerRecordType?: string | null;
crmAppSlug?: string | null;
isPlatform?: boolean;
allowUpdatingUrlParams?: boolean;
};
type SeatedEventData = {
@@ -161,6 +163,8 @@ export type BookerStore = {
teamMemberEmail?: string | null;
crmOwnerRecordType?: string | null;
crmAppSlug?: string | null;
isPlatform?: boolean;
allowUpdatingUrlParams?: boolean;
};
/**
@@ -180,7 +184,9 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
}
updateQueryParam("layout", layout);
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("layout", layout);
}
return set({ layout });
},
selectedDate: getQueryParam("date") || null,
@@ -194,14 +200,14 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
const currentSelection = dayjs(get().selectedDate);
const newSelection = dayjs(selectedDate);
set({ selectedDate });
if (!omitUpdatingParams) {
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("date", selectedDate ?? "");
}
// Setting month make sure small calendar in fullscreen layouts also updates.
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!omitUpdatingParams) {
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
@@ -223,11 +229,15 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
updateQueryParam("month", newSelection.format("YYYY-MM"));
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
}
}
set({ selectedDate: newSelectionFormatted });
updateQueryParam("date", newSelectionFormatted);
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("date", newSelectionFormatted);
}
},
username: null,
eventSlug: null,
@@ -249,7 +259,9 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
return;
}
set({ month, selectedTimeslot: null });
updateQueryParam("month", month ?? "");
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", month ?? "");
}
get().setSelectedDate(null);
},
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null,
@@ -265,10 +277,13 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
},
setSeatedEventData: (seatedEventData: SeatedEventData) => {
set({ seatedEventData });
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null");
}
},
// This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker,
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
// it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link.
timezone: getQueryParam("cal.tz") ?? null,
setTimezone: (timezone: string | null) => {
set({ timezone });
@@ -291,6 +306,8 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
@@ -330,6 +347,8 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isPlatform,
allowUpdatingUrlParams,
});
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
@@ -353,7 +372,9 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
"minutes"
);
set({ selectedDuration: originalBookingLength });
updateQueryParam("duration", originalBookingLength ?? "");
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("duration", originalBookingLength ?? "");
}
}
if (month) set({ month });
@@ -367,9 +388,12 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
selectedTimeslot,
isInstantMeeting,
});
updateQueryParam("month", month);
updateQueryParam("date", selectedDate ?? "");
updateQueryParam("slot", selectedTimeslot ?? "", false);
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("month", month);
updateQueryParam("date", selectedDate ?? "");
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
}
//removeQueryParam("layout");
},
@@ -377,7 +401,9 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
selectedDuration: null,
setSelectedDuration: (selectedDuration: number | null) => {
set({ selectedDuration });
updateQueryParam("duration", selectedDuration ?? "");
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("duration", selectedDuration ?? "");
}
},
setBookingData: (bookingData: GetBookingType | null | undefined) => {
set({ bookingData: bookingData ?? null });
@@ -396,7 +422,9 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
},
setSelectedTimeslot: (selectedTimeslot: string | null) => {
set({ selectedTimeslot });
updateQueryParam("slot", selectedTimeslot ?? "", false);
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("slot", selectedTimeslot ?? "", false);
}
},
formValues: {},
setFormValues: (formValues: Record<string, any>) => {
@@ -406,6 +434,8 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
setOrg: (org: string | null | undefined) => {
set({ org });
},
isPlatform: false,
allowUpdatingUrlParams: true,
}));
export const useInitializeBookerStore = ({
@@ -426,6 +456,8 @@ export const useInitializeBookerStore = ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const initializeStore = useBookerStore((state) => state.initialize);
useEffect(() => {
@@ -447,6 +479,8 @@ export const useInitializeBookerStore = ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isPlatform,
allowUpdatingUrlParams,
});
}, [
initializeStore,
@@ -467,5 +501,7 @@ export const useInitializeBookerStore = ({
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
isPlatform,
allowUpdatingUrlParams,
]);
};
@@ -1,5 +1,7 @@
import { useQueryClient } from "@tanstack/react-query";
import { useMemo, useEffect, useCallback, useState } from "react";
// eslint-disable-next-line no-restricted-imports
import debounce from "lodash/debounce";
import { useMemo, useEffect, useCallback, useState, useRef } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
@@ -40,6 +42,7 @@ import { AtomsWrapper } from "../src/components/atoms-wrapper";
import type {
BookerPlatformWrapperAtomPropsForIndividual,
BookerPlatformWrapperAtomPropsForTeam,
BookerStoreValues,
} from "./types";
export const BookerPlatformWrapper = (
@@ -53,6 +56,8 @@ export const BookerPlatformWrapper = (
crmAppSlug,
crmOwnerRecordType,
preventEventTypeRedirect,
onBookerStateChange,
allowUpdatingUrlParams = false,
} = props;
const layout = BookerLayouts[view];
@@ -70,6 +75,48 @@ export const BookerPlatformWrapper = (
const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
);
const prevStateRef = useRef<BookerStoreValues | null>(null);
const getStateValues = useCallback(
(state: ReturnType<typeof useBookerStore.getState>): BookerStoreValues => {
return Object.fromEntries(
Object.entries(state).filter(([_, value]) => typeof value !== "function")
) as BookerStoreValues;
},
[]
);
const debouncedStateChange = useMemo(() => {
return debounce(
(currentStateValues: BookerStoreValues, callback: (values: BookerStoreValues) => void) => {
const prevState = prevStateRef.current;
const stateChanged = !prevState || JSON.stringify(prevState) !== JSON.stringify(currentStateValues);
if (stateChanged) {
callback(currentStateValues);
prevStateRef.current = currentStateValues;
}
},
50
);
}, []);
useEffect(() => {
if (!onBookerStateChange) return;
const unsubscribe = useBookerStore.subscribe((state) => {
const currentStateValues = getStateValues(state);
debouncedStateChange(currentStateValues, onBookerStateChange);
});
// Initial call with current state
const initialState = getStateValues(useBookerStore.getState());
onBookerStateChange(initialState);
prevStateRef.current = initialState;
return () => {
unsubscribe();
debouncedStateChange.cancel();
};
}, [onBookerStateChange, getStateValues, debouncedStateChange]);
useGetBookingForReschedule({
uid: props.rescheduleUid ?? props.bookingUid ?? "",
@@ -166,6 +213,8 @@ export const BookerPlatformWrapper = (
org: props.entity?.orgSlug,
username,
bookingData,
isPlatform: true,
allowUpdatingUrlParams,
});
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -397,6 +446,13 @@ export const BookerPlatformWrapper = (
},
[setIsOverlayCalendarEnabled]
);
const selectedDateProp = useMemo(
() => dayjs(props.selectedDate).format("YYYY-MM-DD"),
[props.selectedDate]
);
useEffect(() => {
setSelectedDate(selectedDateProp, true);
}, [selectedDateProp]);
useEffect(() => {
// reset booker whenever it's unmounted
@@ -408,7 +464,6 @@ export const BookerPlatformWrapper = (
setSelectedDuration(null);
setOrg(null);
setSelectedMonth(null);
setSelectedDuration(null);
if (props.rescheduleUid) {
// clean booking data from cache
queryClient.removeQueries({
+27
View File
@@ -1,4 +1,5 @@
import type { BookerProps } from "@calcom/features/bookings/Booker";
import type { BookerStore } from "@calcom/features/bookings/Booker/store";
import type { BookingResponse } from "@calcom/platform-libraries";
import type {
ApiSuccessResponse,
@@ -10,6 +11,30 @@ import type { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { UseCreateBookingInput } from "../hooks/bookings/useCreateBooking";
// Type that includes only the data values from BookerStore (excluding functions)
export type BookerStoreValues = Omit<
BookerStore,
| "setState"
| "setLayout"
| "setSelectedDate"
| "setSelectedDatesAndTimes"
| "addToSelectedDate"
| "setVerifiedEmail"
| "setMonth"
| "setDayCount"
| "setSeatedEventData"
| "setTimezone"
| "initialize"
| "setSelectedDuration"
| "setBookingData"
| "setRecurringEventCount"
| "setOccurenceCount"
| "setTentativeSelectedTimeslots"
| "setSelectedTimeslot"
| "setFormValues"
| "setOrg"
>;
export type BookerPlatformWrapperAtomProps = Omit<
BookerProps,
"username" | "entity" | "isTeamEvent" | "teamId"
@@ -39,6 +64,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
onReserveSlotError?: (data: ApiErrorResponse) => void;
onDeleteSlotSuccess?: (data: ApiSuccessResponseWithoutData) => void;
onDeleteSlotError?: (data: ApiErrorResponse) => void;
onBookerStateChange?: (state: BookerStoreValues) => void;
locationUrl?: string;
view?: VIEW_TYPE;
metadata?: Record<string, string>;
@@ -46,6 +72,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
onDryRunSuccess?: () => void;
hostsLimit?: number;
preventEventTypeRedirect?: boolean;
allowUpdatingUrlParams?: boolean;
};
type VIEW_TYPE = keyof typeof BookerLayouts;
@@ -101,6 +101,10 @@ export default function Bookings(props: { calUsername: string; calEmail: string
router.push(`/${data.data.uid}`);
}
}}
onBookerStateChange={(bookerState) => {
console.log("Booker state updated:", bookerState);
// You can perform any actions based on the updated state here
}}
metadata={{ CustomKey: "CustomValue" }}
duration={eventTypeDuration}
customClassNames={{
@@ -120,6 +124,8 @@ export default function Bookings(props: { calUsername: string; calEmail: string
? { isTeamEvent: true, teamId: teams?.[0]?.id || 0 }
: { username: props.calUsername })}
hostsLimit={3}
selectedDate={new Date("2025-03-25")}
allowUpdatingUrlParams={true}
/>
</>
)}