-
Notifications
You must be signed in to change notification settings - Fork 10.2k
fix: multiple widgets for Booker
atom
#22925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Hey there and thank you for opening this pull request! 👋🏼 We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted. Details:
|
WalkthroughThis PR refactors the Booker state management to a context-backed Zustand store. It adds BookerStoreProvider, useBookerStoreContext, and useInitializeBookerStoreContext, and rewires components, hooks, utilities, and tests to use the context instead of the previous global store hook. The store itself is refactored into a factory (createBookerStore), with the default instance still exported for backward compatibility. Selected timeslot is elevated to explicit state and threaded via props to components like BookEventForm and EventMeta; AvailableTimeSlots emits a selection callback. Platform/web wrappers and calendars are wrapped/updated to consume the provider. Tests gain helpers to mock the store per render. Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎ |
Booker
atom
…t tests - Create reusable test utility in test-utils.tsx with comprehensive mock store - Update Booker.test.tsx to use context-based testing approach - Fix DatePicker tests in both bookings and calendars packages - Simulate auto-advance behavior for month navigation tests - All 14 previously failing tests now pass Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>
Graphite Automations"Add consumer team as reviewer" took an action on this PR • (08/11/25)1 reviewer was added to this PR based on Keith Williams's automation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TLDR here's a short summary of the changes made in this PR
useBookerStore
is being replaced by a new context calleduseBookerStoreContext
, where each Booker widget has its own store instead of sharing the same global store instance fromuseBookerStore
- hence a lot of variables now fetch values from
useBookerStoreContext
hook - in a couple of components some values or event handlers are now being accepted as props instead of fetching their values, reason behind it is that we use the same values in the parent component so the child component just accepts the value instead of calling
useBookerStoreContext
again - however there should be no change in the behaviour of the
Booker
in the web app, this should only affect theBooker
atom which we have in platform
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🔭 Outside diff range comments (9)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
305-314
: Critical: Direct singleton store usage breaks widget isolation.The error handler still uses
useBookerStore.setState()
directly, which breaks the isolation between multiple Booker widget instances. When an error occurs in one widget, it would update the state of all widgets on the page.This needs to use the context-based store's setters instead:
- useBookerStore.setState({ - rescheduleUid: error.data?.rescheduleUid, - }); - useBookerStore.setState({ - bookingData: { - uid: error.data?.rescheduleUid, - startTime: error.data?.startTime, - attendees: error.data?.attendees, - } as unknown as GetBookingType, - });You'll need to access the setters from the context:
const setRescheduleUid = useBookerStoreContext((state) => state.setRescheduleUid); const setBookingData = useBookerStoreContext((state) => state.setBookingData);Then use them in the error handler. However, since these setters aren't exposed in the current store interface, you may need to add them first.
packages/features/bookings/Booker/components/DatePicker.tsx (2)
96-103
: Avoid state updates during render; move to useEffectCalling moveToNextMonthOnNoAvailability() directly in render can trigger state updates during render when isLoading is false, causing React warnings and potential loops.
- const { moveToNextMonthOnNoAvailability } = useMoveToNextMonthOnNoAvailability({ + const { moveToNextMonthOnNoAvailability } = useMoveToNextMonthOnNoAvailability({ browsingDate, nonEmptyScheduleDays, onMonthChange, isLoading: isLoading ?? true, }); - moveToNextMonthOnNoAvailability(); + // Run after render to avoid setState during render + useEffect(() => { + if (!(isLoading ?? true)) { + moveToNextMonthOnNoAvailability(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [browsingDate.format("YYYY-MM"), JSON.stringify(nonEmptyScheduleDays), isLoading]);
145-145
: Handle null selectedDate to avoid invalid Dayjs instancePassing dayjs(selectedDate) when selectedDate is null yields an invalid instance. Pass null explicitly to keep downstream logic clean.
- selected={dayjs(selectedDate)} + selected={selectedDate ? dayjs(selectedDate) : null}packages/features/bookings/Booker/components/hooks/useSlots.ts (1)
143-155
: Fix stale closure in reservation interval when duration changesThe effect depends on event?.id and timeSlotToBeBooked, but handleReserveSlot also uses eventDuration. If the user changes duration after selecting a slot, the interval will keep using the old duration.
- }, [event?.id, timeSlotToBeBooked]); + }, [event?.id, timeSlotToBeBooked, eventDuration]);Optionally, for tighter correctness without frequent interval resets, store latest values in refs and read them inside the interval callback. I can provide that refactor if you prefer.
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (2)
160-170
: Localize the button label inside ServerTransDirect text “Please select a new time” violates the localization guideline. Use t() or let the i18n key provide the label.
- <button + <button key="please-select-a-new-time-button" type="button" className="underline" onClick={onCancel}> - Please select a new time + {t("please_select_a_new_time")} </button>,If timeslot_unavailable_book_a_new_time already includes the button text, pass an empty button and let the translation control the content:
- <button ...>{t("please_select_a_new_time")}</button>, + <button ... />
176-198
: Localize anchor labels for Terms and Privacy PolicyMaintain consistency with the later block that uses t("terms") and t("privacy_policy").
- Terms + {t("terms")} @@ - Privacy Policy. + {t("privacy_policy")}packages/platform/atoms/booker/BookerWebWrapper.tsx (1)
187-191
: Add error handling for localStorage operationslocalStorage operations can fail in certain environments (e.g., private browsing, storage quota exceeded). Consider wrapping these calls in try-catch blocks.
if (state) { current.set("overlayCalendar", "true"); - localStorage.setItem("overlayCalendarSwitchDefault", "true"); + try { + localStorage.setItem("overlayCalendarSwitchDefault", "true"); + } catch (e) { + console.warn("Failed to persist overlay calendar preference:", e); + } } else { current.delete("overlayCalendar"); - localStorage.removeItem("overlayCalendarSwitchDefault"); + try { + localStorage.removeItem("overlayCalendarSwitchDefault"); + } catch (e) { + console.warn("Failed to remove overlay calendar preference:", e); + } }packages/platform/atoms/booker/BookerPlatformWrapper.tsx (2)
111-128
: Critical: still subscribing to the legacy global storeThis effect uses useBookerStore.subscribe and useBookerStore.getState, tying onBookerStateChange to the singleton store. With multiple widgets, callbacks will receive state changes from other instances.
Refactor to derive values from the context store state instead. One approach: select the entire store state via context, filter out functions, and debounce callbacks. Example diff:
- 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(() => { + const prevStateRef = useRef<BookerStoreValues | null>(null); + const getStateValues = useCallback( + (state: any): 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]); + // Read full state snapshot from the context store + const contextStoreState = useBookerStoreContext((s) => s); + + useEffect(() => { + if (!onBookerStateChange) return; + const currentValues = getStateValues(contextStoreState); + debouncedStateChange(currentValues, onBookerStateChange); + return () => { + debouncedStateChange.cancel(); + }; + }, [contextStoreState, onBookerStateChange, getStateValues, debouncedStateChange]);And drop the legacy import:
- import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; + // remove useBookerStore importIf you prefer subscribe semantics for fine-grained control, consider exporting a useBookerStoreApi() from the provider to access the underlying StoreApi for subscribe within the provider context.
17-17
: Remove legacy store imports to avoid accidental couplingThe file still imports useBookerStore and useInitializeBookerStore from the legacy store. Remove them once the above refactors are applied to prevent regressions back to the global singleton.
-import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; +// Legacy store imports removed; using context-based store exclusively
🧹 Nitpick comments (9)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (5)
13-13
: React import is optional with the new JSX runtimeIf your tsconfig uses "jsx": "react-jsx", you can drop this import. Harmless either way.
23-23
: Avoidany
in the framer-motion mockYou can preserve types by using
typeof import("framer-motion")
. Also, since the mock returns the actual module unchanged, consider removing the mock if it’s not needed.- const actual = (await importOriginal()) as any; + const actual = (await importOriginal()) as typeof import("framer-motion"); return { ...actual, };If this mock is truly a no-op in your environment, you can delete the entire vi.mock block to reduce noise.
196-203
: Timezone-stable selectedDateYou’re mixing a date-only value ("2024-01-01") with UTC timestamps ("...Z"). If selectedDate is parsed relative to local TZ anywhere, this can lead to flaky tests. Consider normalizing to UTC or using a date-only parsing path explicitly.
Example:
- selectedDate: "2024-01-01", + // Keep tests TZ-agnostic; use an explicit UTC start-of-day if the code parses selectedDate as a Date + selectedDate: "2024-01-01T00:00:00Z",Or ensure the code treats selectedDate as a plain date string (no implicit Date parsing).
38-38
: Silence noisy console in test mockThe console.log in BookEventForm mock adds noise to CI logs.
- console.log("BookEventForm Called", { isTimeslotUnavailable, onCancel }); + // Intentionally silent to keep test logs clean
223-223
: Remove debugging helper from test runscreen.logTestingPlaygroundURL() is great locally but noisy in CI.
- screen.logTestingPlaygroundURL(); + // screen.logTestingPlaygroundURL();.changeset/fresh-rivers-glow.md (1)
5-5
: Fix grammatical error in changeset description.There's a typo in the description.
-Fixes an issues for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others. +Fixes an issue for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others.packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
305-314
: Consider the approach for error state management.Since
rescheduleUid
is typically set through theinitialize
function and there's no dedicated setter, you might need to either:
- Add explicit setters for
rescheduleUid
and update the store interface- Use a different pattern for error recovery that doesn't involve direct state manipulation
- Store error-specific state separately from the main booking state
packages/features/calendars/__tests__/DatePicker.test.tsx (1)
20-35
: Reduce wrapper repetition via a local render helperMinor DX: factor the nested providers into a small helper to avoid duplication and ease future changes.
+const renderWithProviders = (ui: React.ReactNode) => + render( + <BookerStoreProvider> + <TooltipProvider>{ui}</TooltipProvider> + </BookerStoreProvider> + ); @@ - const { getByTestId } = render( - <BookerStoreProvider> - <TooltipProvider> - <DatePicker - onChange={noop} - browsingDate={testDate} - locale="en" - periodData={{ - periodType: PeriodType.UNLIMITED, - periodDays: null, - periodCountCalendarDays: false, - periodStartDate: null, - periodEndDate: null, - }} - /> - </TooltipProvider> - </BookerStoreProvider> - ); + const { getByTestId } = renderWithProviders( + <DatePicker + onChange={noop} + browsingDate={testDate} + locale="en" + periodData={{ + periodType: PeriodType.UNLIMITED, + periodDays: null, + periodCountCalendarDays: false, + periodStartDate: null, + periodEndDate: null, + }} + /> + );Also applies to: 46-51, 61-66, 95-113, 139-157, 179-197
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (1)
507-518
: Non-localized error strings (informational)These thrown "Function not implemented." strings are developer-facing, not user-visible. No action required, but if they can bubble to UI, wrap with t() per guidelines for TSX files.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
.changeset/fresh-rivers-glow.md
(1 hunks)packages/features/bookings/Booker/Booker.tsx
(7 hunks)packages/features/bookings/Booker/BookerStoreProvider.tsx
(1 hunks)packages/features/bookings/Booker/__tests__/Booker.test.tsx
(5 hunks)packages/features/bookings/Booker/__tests__/test-utils.tsx
(1 hunks)packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
(5 hunks)packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
(4 hunks)packages/features/bookings/Booker/components/DatePicker.test.tsx
(5 hunks)packages/features/bookings/Booker/components/DatePicker.tsx
(2 hunks)packages/features/bookings/Booker/components/EventMeta.tsx
(3 hunks)packages/features/bookings/Booker/components/Header.tsx
(2 hunks)packages/features/bookings/Booker/components/hooks/useBookings.ts
(2 hunks)packages/features/bookings/Booker/components/hooks/useSlots.ts
(2 hunks)packages/features/bookings/Booker/store.ts
(2 hunks)packages/features/bookings/Booker/utils/event.ts
(3 hunks)packages/features/calendars/DatePicker.tsx
(3 hunks)packages/features/calendars/__tests__/DatePicker.test.tsx
(7 hunks)packages/platform/atoms/booker/BookerPlatformWrapper.tsx
(5 hunks)packages/platform/atoms/booker/BookerWebWrapper.tsx
(3 hunks)packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
(2 hunks)packages/platform/atoms/hooks/useSlots.ts
(4 hunks)
🔥 Files not summarized due to errors (1)
- packages/features/bookings/Booker/tests/Booker.test.tsx: Error: Server error: no LLM provider could handle the message
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/features/bookings/Booker/components/hooks/useBookings.ts
packages/features/bookings/Booker/components/hooks/useSlots.ts
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
packages/platform/atoms/hooks/useSlots.ts
packages/features/bookings/Booker/utils/event.ts
packages/features/bookings/Booker/store.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/features/bookings/Booker/components/hooks/useBookings.ts
packages/features/bookings/Booker/components/hooks/useSlots.ts
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
packages/features/bookings/Booker/__tests__/test-utils.tsx
packages/features/calendars/__tests__/DatePicker.test.tsx
packages/features/bookings/Booker/components/DatePicker.tsx
packages/features/bookings/Booker/components/Header.tsx
packages/features/bookings/Booker/BookerStoreProvider.tsx
packages/platform/atoms/hooks/useSlots.ts
packages/features/bookings/Booker/utils/event.ts
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
packages/features/bookings/Booker/components/DatePicker.test.tsx
packages/features/calendars/DatePicker.tsx
packages/features/bookings/Booker/components/EventMeta.tsx
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
packages/features/bookings/Booker/__tests__/Booker.test.tsx
packages/features/bookings/Booker/Booker.tsx
packages/platform/atoms/booker/BookerWebWrapper.tsx
packages/features/bookings/Booker/store.ts
packages/platform/atoms/booker/BookerPlatformWrapper.tsx
**/*.tsx
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Always use
t()
for text localization in frontend code; direct text embedding should trigger a warning
Files:
packages/features/bookings/Booker/__tests__/test-utils.tsx
packages/features/calendars/__tests__/DatePicker.test.tsx
packages/features/bookings/Booker/components/DatePicker.tsx
packages/features/bookings/Booker/components/Header.tsx
packages/features/bookings/Booker/BookerStoreProvider.tsx
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
packages/features/bookings/Booker/components/DatePicker.test.tsx
packages/features/calendars/DatePicker.tsx
packages/features/bookings/Booker/components/EventMeta.tsx
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
packages/features/bookings/Booker/__tests__/Booker.test.tsx
packages/features/bookings/Booker/Booker.tsx
packages/platform/atoms/booker/BookerWebWrapper.tsx
packages/platform/atoms/booker/BookerPlatformWrapper.tsx
🧬 Code Graph Analysis (19)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/__tests__/test-utils.tsx (3)
packages/features/bookings/Booker/store.ts (1)
BookerStore
(51-174)packages/embeds/embed-core/src/types.ts (1)
BookerLayouts
(6-6)packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
BookerStoreContext
(9-9)
packages/features/calendars/__tests__/DatePicker.test.tsx (4)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
BookerStoreProvider
(15-22)packages/platform/atoms/src/components/ui/tooltip.tsx (1)
TooltipProvider
(14-14)packages/features/bookings/Booker/components/DatePicker.tsx (1)
DatePicker
(54-153)packages/platform/libraries/index.ts (1)
PeriodType
(30-30)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/Header.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
packages/features/bookings/Booker/store.ts (3)
BookerStore
(51-174)createBookerStore
(179-446)StoreInitializeType
(18-42)
packages/platform/atoms/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/utils/event.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/calendars/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/EventMeta.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (2)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)packages/embeds/embed-core/src/types.ts (1)
BookerLayouts
(6-6)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (1)
packages/features/bookings/Booker/Booker.tsx (1)
Booker
(593-599)
packages/features/bookings/Booker/Booker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/platform/atoms/booker/BookerWebWrapper.tsx (4)
packages/platform/atoms/booker/index.ts (2)
BookerWebWrapperAtomProps
(2-2)BookerWebWrapper
(1-1)packages/features/bookings/Booker/types.ts (1)
BookerProps
(19-108)packages/features/eventtypes/lib/getPublicEvent.ts (1)
getPublicEvent
(254-564)packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
BookerStoreProvider
(15-22)
packages/features/bookings/Booker/store.ts (3)
packages/features/bookings/Booker/types.ts (2)
BookerState
(154-154)BookerLayout
(155-155)packages/embeds/embed-core/src/types.ts (1)
BookerLayouts
(6-6)packages/lib/constants.ts (1)
BOOKER_NUMBER_OF_DAYS_TO_LOAD
(170-173)
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (3)
packages/features/bookings/Booker/BookerStoreProvider.tsx (3)
useBookerStoreContext
(24-35)useInitializeBookerStoreContext
(37-113)BookerStoreProvider
(15-22)packages/trpc/server/routers/publicViewer/procedures/event.ts (1)
event
(8-12)packages/platform/atoms/booker/types.ts (2)
BookerPlatformWrapperAtomPropsForIndividual
(92-96)BookerPlatformWrapperAtomPropsForTeam
(98-103)
🔇 Additional comments (35)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (4)
169-169
: Flags initialization looks fineUsing string values for constants aligns with typical flag infra. No changes needed.
177-179
: LGTM: loading state testSolid assertion that container is empty during loading.
220-222
: LGTM: cancel flow invalidationGood coverage to assert schedule.invalidate() and clearing the selected timeslot on cancel when the flag is enabled.
242-244
: LGTM: quick availability check -> unavailableCovers the unavailability mapping correctly via prop-driven quick checks.
packages/features/bookings/Booker/utils/event.ts (1)
3-3
: LGTM! Proper migration to context-based store.The migration from
useBookerStore
touseBookerStoreContext
is correctly implemented in both hooks, maintaining the shallow comparison for performance optimization.Also applies to: 22-25, 92-95
packages/features/bookings/Booker/components/Header.tsx (1)
7-7
: LGTM! Clean migration to context-based store.The Header component has been properly migrated to use
useBookerStoreContext
with appropriate shallow comparison for performance optimization.Also applies to: 39-42
packages/features/calendars/__tests__/DatePicker.test.tsx (1)
7-7
: Correctly wrapped tests with BookerStoreProviderWrapping DatePicker with BookerStoreProvider (outside TooltipProvider) is necessary after migrating calendars/DatePicker to read Booker context. Wrapper order is correct and prevents runtime “must be used within BookerStoreProvider” errors.
Also applies to: 20-35, 46-51, 61-66, 95-113, 139-157, 179-197
packages/features/calendars/DatePicker.tsx (1)
7-7
: Context migration looks correctReplacing useBookerStore with useBookerStoreContext for selectedDatesAndTimes and bookingData is consistent with the provider-based architecture. Selectors and shallow equality are preserved.
Also applies to: 227-227, 395-396
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)
4-5
: Migration to useBookerStoreContext is consistentSelectors map 1:1 to the previous store keys. The bookingInput and flows (instant, recurring, normal) remain unchanged. No issues spotted.
Also applies to: 49-66
packages/features/bookings/Booker/components/DatePicker.test.tsx (1)
1-1
: Solid test updates with mock store seeding
- Switching to the shared test-utils render simplifies provider setup.
- Fixing dayjs().add(1, "month") is correct.
- Seeding mockStore.month aligns expectations for browsingDate.
Looks good.
Also applies to: 7-8, 20-23, 62-82, 93-109, 114-115
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (1)
46-47
: timeslot prop introduction is correct and improves clarityAccepting timeslot via props and gating the form until it’s set eliminates hidden coupling with the store. Early return with EmptyScreen is appropriate.
Also applies to: 66-67, 92-104
packages/features/bookings/Booker/components/EventMeta.tsx (1)
58-59
: EventMeta context migration and selectedTimeslot display look good
- selectedTimeslot prop is correctly used for FromToTime.
- setTimezone updates both user prefs and Booker store, preserving prior behavior.
Also applies to: 95-101, 206-216, 247-251
packages/features/bookings/Booker/__tests__/test-utils.tsx (2)
103-105
: Well-structured test utilities!The test utilities follow best practices by:
- Providing a custom render function that includes the necessary context
- Re-exporting testing-library utilities for convenience
- Aliasing
renderWithBookerStore
asrender
for familiar API usage
80-87
: Potential state mutation issue in mock setStateThe
setState
implementation directly modifies thestate
object before spreading, which could cause issues with reference equality checks in tests.Apply this fix to ensure proper immutability:
setState: vi.fn((updater) => { if (typeof updater === "function") { const newState = updater(state); - state = { ...state, ...newState }; + state = { ...newState }; } else { - state = { ...state, ...updater }; + state = { ...state, ...updater }; } }),Likely an incorrect or invalid review comment.
packages/platform/atoms/hooks/useSlots.ts (1)
39-43
: Successful migration to context-based store!All store accesses have been correctly migrated from the singleton pattern to the context-based approach. The shallow equality check for the multi-value selector is properly preserved.
Also applies to: 55-56, 88-88
packages/features/bookings/Booker/BookerStoreProvider.tsx (2)
15-22
: Correct provider implementation pattern!The provider correctly uses
useRef
to ensure the store is created only once and persists across re-renders, following Zustand's recommended pattern for context-based stores.
24-35
: Well-implemented context hook with proper error handling!The hook correctly validates context availability and provides a clear error message when used outside the provider. The selector pattern with optional equality function follows Zustand best practices.
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (2)
79-89
: Clean migration to context-based store access!All store accesses have been properly migrated to use
useBookerStoreContext
with appropriate selectors.
52-52
: Excellent architectural improvement with callback pattern!The introduction of
onAvailableTimeSlotSelect
callback properly lifts state management to the parent component, improving component reusability and testability. This aligns well with the migration to context-based state management.Also applies to: 151-151, 167-167
packages/platform/atoms/booker/BookerWebWrapper.tsx (1)
270-276
: Clean separation of concerns with provider pattern!The restructuring properly separates the store provisioning (in
BookerWebWrapper
) from the component logic (inBookerPlatformWrapperComponent
), maintaining the same public API while enabling context-based state management.packages/features/bookings/Booker/Booker.tsx (9)
13-13
: Good: switched to context-based store accessImporting useBookerStoreContext aligns with the per-instance provider and the PR goal to avoid a shared global store.
87-90
: Correct: tuple selector with shallow equalityUsing a tuple selector with shallow is the right pattern for Zustand to avoid unnecessary re-renders while reading [state, setState].
92-93
: LGTM: selectedDate setters via contextClean and idiomatic; no concerns.
107-110
: LGTM: seatedEventData selector pairConsistent with the rest of the migration.
112-115
: LGTM: dayCount selector pairConsistent with other selectors; using shallow is appropriate.
194-197
: Wiring timeslot selection through the storeonAvailableTimeSlotSelect correctly forwards the selection to the context store.
234-234
: Verify downstream prop type for timeslotConfirm BookEventForm’s new prop is typed as timeslot: string | null and is used safely when null.
Would you like me to scan usages of BookEventForm to ensure all call sites pass the new prop?
407-407
: Propagate selectedTimeslot to EventMetaLooks good; verify EventMeta props were updated accordingly to accept selectedTimeslot: string | null.
Do you want a quick repository check to confirm EventMeta’s prop signature?
492-492
: AvailableTimeSlots: new selection callbackPassing onAvailableTimeSlotSelect ensures the parent owns the state; this is consistent with the context-backed design.
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (6)
9-13
: Good: provider and context hooks importedThis sets up the path to per-instance store isolation.
48-50
: Component extraction is fineInternal component organization is OK and keeps the public API intact.
71-83
: Good: reading/writing state via context selectorsUsing useBookerStoreContext with shallow tuples for [state, setter] is correct and performant.
197-202
: LGTM: reading dayCount/selectedDate/month/eventSlug from contextThese reads are correctly aligned with the provider-based store.
445-452
: Minor: ensure URL-param updates obey allowUpdatingUrlParams in context initializersetSelectedDate({ date: selectedDateProp, omitUpdatingParams: true }) is explicit here, which is good. Verify the context store initializer respects allowUpdatingUrlParams to avoid cross-instance URL mutations when multiple widgets exist.
Would you like a quick check across BookerStoreProvider.tsx/store.ts to assert allowUpdatingUrlParams gates any window.history updates?
582-590
: All production<Booker>
renderings are already wrapped byBookerStoreProvider
A repo-wide search confirms:
- The two platform atoms (
BookerPlatformWrapper
andBookerWebWrapper
) both wrap their children in<BookerStoreProvider>
.- No other production code imports or renders
<Booker>
outside of those wrappers.- Test files in
packages/features/bookings/Booker/__tests__
render<Booker>
directly but use the test harness’smockStore
option to inject a fake store.No further changes are needed.
import { render, screen } from "./test-utils"; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add a test that verifies per-instance store isolation (core PR objective)
Good to see the custom render wrapper. However, there’s no test asserting that two Booker instances on the same page don’t share state. Please add one.
Here’s a starting point you can adapt to your test-utils and provider API:
@@
describe("Booker", () => {
@@
it("should render DryRunMessage when in dry run mode", () => {
@@
});
+ it("isolates state across multiple Booker instances on the same page", () => {
+ const leftProps = {
+ ...defaultProps,
+ eventSlug: "left-event",
+ slots: {
+ ...defaultProps.slots,
+ setSelectedTimeslot: vi.fn(),
+ },
+ };
+ const rightProps = {
+ ...defaultProps,
+ eventSlug: "right-event",
+ slots: {
+ ...defaultProps.slots,
+ setSelectedTimeslot: vi.fn(),
+ },
+ };
+
+ // Render two instances; ensure each is wrapped with its own store/provider
+ // If your test-utils render() only supports a single mockStore,
+ // render them in two separate calls to simulate two widgets on one page.
+ const left = render(<Booker {...leftProps} />, {
+ mockStore: {
+ state: "booking",
+ selectedDate: "2024-01-01",
+ selectedTimeslot: "2024-01-01T10:00:00Z",
+ tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
+ },
+ });
+ const right = render(<Booker {...rightProps} />, {
+ mockStore: {
+ state: "booking",
+ selectedDate: "2024-01-02",
+ selectedTimeslot: "2024-01-02T10:00:00Z",
+ tentativeSelectedTimeslots: ["2024-01-02T10:00:00Z"],
+ },
+ });
+
+ // Act on the left instance (e.g., cancel)
+ left.getByRole("button", { name: /cancel/i }).click();
+
+ // Assert: left instance props updated, right instance untouched
+ expect((leftProps.slots.setSelectedTimeslot as any)).toHaveBeenCalledWith(null);
+ expect((rightProps.slots.setSelectedTimeslot as any)).not.toHaveBeenCalled();
+ });
If you prefer a single render with both siblings, ensure your test-utils can supply separate store providers per instance; I can help wire that up.
🤖 Prompt for AI Agents
packages/features/bookings/Booker/__tests__/Booker.test.tsx around lines 20-21:
the test file imports the custom render but lacks a test to assert per-instance
store isolation; add a new test that renders two Booker instances with distinct
stores (either by calling your custom render twice with independent
provider/store options or by rendering both siblings inside a wrapper that
supplies separate providers per child), perform an action that mutates the first
instance's state (e.g., select or add a booking), then assert the second
instance's UI/state remains unchanged; ensure the test uses your test-utils APIs
to create isolated stores and only references public UI queries (screen.getBy*,
etc.) to verify independence.
state.setMonth = vi.fn((month: string | null) => { | ||
state.month = month; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Inconsistent mock behavior - only setMonth
updates state
The mock implementation only provides a functional setter for setMonth
, while all other setters remain as no-op mocks. This inconsistency could lead to confusing test behavior where some state updates work while others silently fail.
Consider either:
- Making all setters functional (preferred for integration tests), or
- Keeping all setters as mocks (for unit tests that verify function calls)
For a more complete mock that supports state updates:
- state.setMonth = vi.fn((month: string | null) => {
- state.month = month;
- });
+ // Make all setters functional
+ state.setMonth = vi.fn((month: string | null) => {
+ state.month = month;
+ });
+ state.setVerifiedEmail = vi.fn((email: string | null) => {
+ state.verifiedEmail = email;
+ });
+ state.setState = vi.fn((newState: BookerState) => {
+ state.state = newState;
+ });
+ // ... continue for other setters
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
state.setMonth = vi.fn((month: string | null) => { | |
state.month = month; | |
}); | |
// Make all setters functional | |
state.setMonth = vi.fn((month: string | null) => { | |
state.month = month; | |
}); | |
state.setVerifiedEmail = vi.fn((email: string | null) => { | |
state.verifiedEmail = email; | |
}); | |
state.setState = vi.fn((newState: BookerState) => { | |
state.state = newState; | |
}); | |
// ... continue for other setters |
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/__tests__/test-utils.tsx around lines 74 to
76, the mock only implements a functional setter for setMonth while other
setters are no-ops, causing inconsistent test behavior; either make all setters
consistent by turning setMonth into a no-op mock to match others, or (preferred)
make every setter mock functional so each setter updates its corresponding state
property (e.g., implement each setter as a vi.fn that assigns the incoming value
to the appropriate state field) so tests that rely on state changes behave
deterministically.
useEffect(() => { | ||
initializeStore({ | ||
username, | ||
eventSlug, | ||
month, | ||
eventId, | ||
rescheduleUid, | ||
rescheduledBy, | ||
bookingData, | ||
layout, | ||
isTeamEvent, | ||
org, | ||
verifiedEmail, | ||
durationConfig, | ||
isInstantMeeting, | ||
timezone, | ||
teamMemberEmail, | ||
crmOwnerRecordType, | ||
crmAppSlug, | ||
crmRecordId, | ||
isPlatform, | ||
allowUpdatingUrlParams, | ||
}); | ||
}, [ | ||
initializeStore, | ||
org, | ||
username, | ||
eventSlug, | ||
month, | ||
eventId, | ||
rescheduleUid, | ||
rescheduledBy, | ||
bookingData, | ||
layout, | ||
isTeamEvent, | ||
verifiedEmail, | ||
durationConfig, | ||
isInstantMeeting, | ||
timezone, | ||
teamMemberEmail, | ||
crmOwnerRecordType, | ||
crmAppSlug, | ||
crmRecordId, | ||
isPlatform, | ||
allowUpdatingUrlParams, | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Consider the impact of frequent re-initializations
The useEffect
dependency array includes all initialization parameters, which means the store will be re-initialized whenever any prop changes. This could lead to unexpected state resets or performance issues.
Consider whether all these dependencies are necessary. Typically, initialization should only occur when key identifiers change (e.g., username
, eventSlug
, eventId
). Props like timezone
, verifiedEmail
, or allowUpdatingUrlParams
might be better handled through separate update methods rather than full re-initialization.
🏁 Script executed:
#!/bin/bash
# Check how the initialize method handles re-initialization in the store
ast-grep --pattern 'initialize: (data: StoreInitializeType) => {
$$$
}'
Length of output: 75
I didn’t find an initializeStore
implementation with the previous AST query. Let’s locate its definition and see how it handles repeated calls:
🏁 Script executed:
#!/bin/bash
# Search for where initializeStore is defined or imported
rg -n "initializeStore" --glob "*.ts*"
Length of output: 875
Let’s inspect the initialize
implementation to see if it fully resets state or merges updates:
🏁 Script executed:
#!/bin/bash
# Show the initialize method in Booker store
sed -n '460,550p' packages/features/bookings/Booker/store.ts
Length of output: 1233
Let’s inspect the actual initialize
action in the Booker store to see whether it fully resets state or just merges updates:
🏁 Script executed:
#!/bin/bash
# Locate and display the initialize action implementation
rg -n -C 5 "initialize:" packages/features/bookings/Booker/store.ts
Length of output: 883
Inspecting the full initialize
implementation to see whether it resets or merges state:
🏁 Script executed:
#!/bin/bash
# Extract the body of the initialize action
sed -n '295,380p' packages/features/bookings/Booker/store.ts
Length of output: 2812
Align useEffect
dependencies with the store’s guard logic
The initialize
action in your Booker store only checks a specific subset of fields before early-returning (username, eventSlug, month, eventId, rescheduleUid, bookingUid, bookingData.responses.email, layout, timezone, rescheduledBy, teamMemberEmail, crmOwnerRecordType, crmAppSlug, crmRecordId). However, your useEffect
dependency array also includes props—like org
, isTeamEvent
, durationConfig
, isInstantMeeting
, verifiedEmail
, isPlatform
, and allowUpdatingUrlParams
—that the guard never compares. This means:
- Changes to those extra props will trigger
initialize
calls that immediately return (no state update). - Conversely, if you do intend for some of those props to update the store, they’ll be ignored by the guard.
To resolve this mismatch, choose one of:
- Narrow the dependency array in
BookerStoreProvider.tsx
to just the keys the guard actually compares. - Expand the guard in
packages/features/bookings/Booker/store.ts
(around lines 295–340) to include any additional props you do want to trigger a re-initialization. - Separate out specific update methods (e.g.
setTimezone
,setAllowUpdatingUrlParams
) instead of feeding everything throughinitialize
.
Places to update:
packages/features/bookings/Booker/BookerStoreProvider.tsx
(lines 67–112) – adjustuseEffect
depspackages/features/bookings/Booker/store.ts
(initialize action at ~lines 295–340) – update guard or parameter handling
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/BookerStoreProvider.tsx around lines 67 to
112, the useEffect dependency array includes props that the store.initialize
guard does not check (org, isTeamEvent, durationConfig, isInstantMeeting,
verifiedEmail, isPlatform, allowUpdatingUrlParams), causing unnecessary
initialize calls or ignored updates; either narrow the dependency array to only
the keys the guard actually compares (username, eventSlug, month, eventId,
rescheduleUid, bookingUid/bookingData.responses.email, layout, timezone,
rescheduledBy, teamMemberEmail, crmOwnerRecordType, crmAppSlug, crmRecordId) or
update the initialize guard in packages/features/bookings/Booker/store.ts
(around lines 295–340) to compare and accept the additional props you want to
trigger re-initialization, or add specific setter actions (e.g., setTimezone,
setAllowUpdatingUrlParams) and call those from the provider instead of
initialize.
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { | ||
// unset selected date | ||
if (!selectedDate) { | ||
removeQueryParam("date"); | ||
return; | ||
} | ||
|
||
// Setting month make sure small calendar in fullscreen layouts also updates. | ||
// preventMonthSwitching is true in monthly view | ||
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { | ||
set({ month: newSelection.format("YYYY-MM") }); | ||
const currentSelection = dayjs(get().selectedDate); | ||
const newSelection = dayjs(selectedDate); | ||
set({ selectedDate }); | ||
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { | ||
updateQueryParam("month", newSelection.format("YYYY-MM")); | ||
updateQueryParam("date", selectedDate ?? ""); | ||
} | ||
} | ||
}, | ||
selectedDatesAndTimes: null, | ||
setSelectedDatesAndTimes: (selectedDatesAndTimes) => { | ||
set({ selectedDatesAndTimes }); | ||
}, | ||
addToSelectedDate: (days: number) => { | ||
const currentSelection = dayjs(get().selectedDate); | ||
let newSelection = currentSelection.add(days, "day"); | ||
|
||
// If newSelection is before the current date, set it to today | ||
if (newSelection.isBefore(dayjs(), "day")) { | ||
newSelection = dayjs(); | ||
} | ||
|
||
const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); | ||
// Setting month make sure small calendar in fullscreen layouts also updates. | ||
// preventMonthSwitching is true in monthly view | ||
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { | ||
set({ month: newSelection.format("YYYY-MM") }); | ||
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { | ||
updateQueryParam("month", newSelection.format("YYYY-MM")); | ||
} | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential issue with null date handling in setSelectedDate.
When selectedDate
is null (either initially or when unsetting), the dayjs instances created on lines 203-204 may be invalid, leading to unexpected behavior in the month comparison on line 212.
Consider adding null checks:
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
+ set({ selectedDate: null });
return;
}
- const currentSelection = dayjs(get().selectedDate);
+ const currentSelectedDate = get().selectedDate;
+ const currentSelection = currentSelectedDate ? dayjs(currentSelectedDate) : dayjs();
const newSelection = dayjs(selectedDate);
set({ selectedDate });
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { | |
// unset selected date | |
if (!selectedDate) { | |
removeQueryParam("date"); | |
return; | |
} | |
// Setting month make sure small calendar in fullscreen layouts also updates. | |
// preventMonthSwitching is true in monthly view | |
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { | |
set({ month: newSelection.format("YYYY-MM") }); | |
const currentSelection = dayjs(get().selectedDate); | |
const newSelection = dayjs(selectedDate); | |
set({ selectedDate }); | |
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { | |
updateQueryParam("month", newSelection.format("YYYY-MM")); | |
updateQueryParam("date", selectedDate ?? ""); | |
} | |
} | |
}, | |
selectedDatesAndTimes: null, | |
setSelectedDatesAndTimes: (selectedDatesAndTimes) => { | |
set({ selectedDatesAndTimes }); | |
}, | |
addToSelectedDate: (days: number) => { | |
const currentSelection = dayjs(get().selectedDate); | |
let newSelection = currentSelection.add(days, "day"); | |
// If newSelection is before the current date, set it to today | |
if (newSelection.isBefore(dayjs(), "day")) { | |
newSelection = dayjs(); | |
} | |
const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); | |
// Setting month make sure small calendar in fullscreen layouts also updates. | |
// preventMonthSwitching is true in monthly view | |
if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { | |
set({ month: newSelection.format("YYYY-MM") }); | |
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { | |
updateQueryParam("month", newSelection.format("YYYY-MM")); | |
} | |
} | |
}, | |
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { | |
// unset selected date | |
if (!selectedDate) { | |
removeQueryParam("date"); | |
set({ selectedDate: null }); | |
return; | |
} | |
const currentSelectedDate = get().selectedDate; | |
const currentSelection = currentSelectedDate | |
? dayjs(currentSelectedDate) | |
: dayjs(); | |
const newSelection = dayjs(selectedDate); | |
set({ selectedDate }); | |
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { | |
updateQueryParam("date", selectedDate); | |
} | |
// Setting month make sure small calendar in fullscreen layouts also updates. | |
// preventMonthSwitching is true in monthly view | |
if ( | |
!preventMonthSwitching && | |
newSelection.month() !== currentSelection.month() | |
) { | |
set({ month: newSelection.format("YYYY-MM") }); | |
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { | |
updateQueryParam("month", newSelection.format("YYYY-MM")); | |
} | |
} | |
}, |
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 196 to 218, guard
against get().selectedDate being null/undefined before creating currentSelection
and before comparing months: only construct dayjs(current) if get().selectedDate
is truthy (or treat it as an invalid sentinel), and perform the month-switching
logic only when both currentSelection and newSelection are valid dayjs objects;
if currentSelection is missing treat it as different so the month is updated.
Adjust the conditional around the month comparison and the
set/updateQueryParam("month", ...) calls to run only when newSelection is valid
and the comparison is safe.
verifiedEmail: null, | ||
setVerifiedEmail: (email: string | null) => { | ||
set({ verifiedEmail: email }); | ||
}, | ||
month: | ||
getQueryParam("month") || | ||
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid() | ||
? dayjs(getQueryParam("date")).format("YYYY-MM") | ||
: null) || | ||
dayjs().format("YYYY-MM"), | ||
setMonth: (month: string | null) => { | ||
if (!month) { | ||
removeQueryParam("month"); | ||
return; | ||
} | ||
set({ month, selectedTimeslot: null }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("month", month ?? ""); | ||
} | ||
get().setSelectedDate({ date: null }); | ||
}, | ||
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null, | ||
setDayCount: (dayCount: number | null) => { | ||
set({ dayCount }); | ||
}, | ||
isTeamEvent: false, | ||
seatedEventData: { | ||
seatsPerTimeSlot: undefined, | ||
attendees: undefined, | ||
bookingUid: undefined, | ||
showAvailableSeatsCount: true, | ||
}, | ||
setSeatedEventData: (seatedEventData: SeatedEventData) => { | ||
set({ seatedEventData }); | ||
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 }); | ||
}, | ||
initialize: ({ | ||
username, | ||
eventSlug, | ||
month, | ||
eventId, | ||
rescheduleUid = null, | ||
rescheduledBy = null, | ||
bookingUid = null, | ||
bookingData = null, | ||
layout, | ||
isTeamEvent, | ||
durationConfig, | ||
org, | ||
isInstantMeeting, | ||
timezone = null, | ||
teamMemberEmail, | ||
crmOwnerRecordType, | ||
crmAppSlug, | ||
crmRecordId, | ||
isPlatform = false, | ||
allowUpdatingUrlParams = true, | ||
}: StoreInitializeType) => { | ||
const selectedDateInStore = get().selectedDate; | ||
const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); | ||
|
||
if (newSelection.month() !== currentSelection.month()) { | ||
set({ month: newSelection.format("YYYY-MM") }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("month", newSelection.format("YYYY-MM")); | ||
} | ||
} | ||
|
||
if ( | ||
get().username === username && | ||
get().eventSlug === eventSlug && | ||
get().month === month && | ||
get().eventId === eventId && | ||
get().rescheduleUid === rescheduleUid && | ||
get().bookingUid === bookingUid && | ||
get().bookingData?.responses.email === bookingData?.responses.email && | ||
get().layout === layout && | ||
get().timezone === timezone && | ||
get().rescheduledBy === rescheduledBy && | ||
get().teamMemberEmail === teamMemberEmail && | ||
get().crmOwnerRecordType === crmOwnerRecordType && | ||
get().crmAppSlug === crmAppSlug && | ||
get().crmRecordId === crmRecordId | ||
) | ||
return; | ||
set({ | ||
set({ selectedDate: newSelectionFormatted }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("date", newSelectionFormatted); | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle null selectedDate in addToSelectedDate.
The function doesn't handle the case where selectedDate
might be null, which could cause issues when creating the dayjs instance.
Consider defaulting to today when selectedDate is null:
addToSelectedDate: (days: number) => {
- const currentSelection = dayjs(get().selectedDate);
+ const currentSelectedDate = get().selectedDate;
+ const currentSelection = currentSelectedDate ? dayjs(currentSelectedDate) : dayjs();
let newSelection = currentSelection.add(days, "day");
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
addToSelectedDate: (days: number) => { | |
const currentSelection = dayjs(get().selectedDate); | |
let newSelection = currentSelection.add(days, "day"); | |
if (newSelection.month() !== currentSelection.month()) { | |
set({ month: newSelection.format("YYYY-MM") }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("month", newSelection.format("YYYY-MM")); | |
// If newSelection is before the current date, set it to today | |
if (newSelection.isBefore(dayjs(), "day")) { | |
newSelection = dayjs(); | |
} | |
} | |
set({ selectedDate: newSelectionFormatted }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("date", newSelectionFormatted); | |
} | |
}, | |
username: null, | |
eventSlug: null, | |
eventId: null, | |
rescheduledBy: null, | |
verifiedEmail: null, | |
setVerifiedEmail: (email: string | null) => { | |
set({ verifiedEmail: email }); | |
}, | |
month: | |
getQueryParam("month") || | |
(getQueryParam("date") && dayjs(getQueryParam("date")).isValid() | |
? dayjs(getQueryParam("date")).format("YYYY-MM") | |
: null) || | |
dayjs().format("YYYY-MM"), | |
setMonth: (month: string | null) => { | |
if (!month) { | |
removeQueryParam("month"); | |
return; | |
} | |
set({ month, selectedTimeslot: null }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("month", month ?? ""); | |
} | |
get().setSelectedDate({ date: null }); | |
}, | |
dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null, | |
setDayCount: (dayCount: number | null) => { | |
set({ dayCount }); | |
}, | |
isTeamEvent: false, | |
seatedEventData: { | |
seatsPerTimeSlot: undefined, | |
attendees: undefined, | |
bookingUid: undefined, | |
showAvailableSeatsCount: true, | |
}, | |
setSeatedEventData: (seatedEventData: SeatedEventData) => { | |
set({ seatedEventData }); | |
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 }); | |
}, | |
initialize: ({ | |
username, | |
eventSlug, | |
month, | |
eventId, | |
rescheduleUid = null, | |
rescheduledBy = null, | |
bookingUid = null, | |
bookingData = null, | |
layout, | |
isTeamEvent, | |
durationConfig, | |
org, | |
isInstantMeeting, | |
timezone = null, | |
teamMemberEmail, | |
crmOwnerRecordType, | |
crmAppSlug, | |
crmRecordId, | |
isPlatform = false, | |
allowUpdatingUrlParams = true, | |
}: StoreInitializeType) => { | |
const selectedDateInStore = get().selectedDate; | |
const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); | |
if (newSelection.month() !== currentSelection.month()) { | |
set({ month: newSelection.format("YYYY-MM") }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("month", newSelection.format("YYYY-MM")); | |
} | |
} | |
if ( | |
get().username === username && | |
get().eventSlug === eventSlug && | |
get().month === month && | |
get().eventId === eventId && | |
get().rescheduleUid === rescheduleUid && | |
get().bookingUid === bookingUid && | |
get().bookingData?.responses.email === bookingData?.responses.email && | |
get().layout === layout && | |
get().timezone === timezone && | |
get().rescheduledBy === rescheduledBy && | |
get().teamMemberEmail === teamMemberEmail && | |
get().crmOwnerRecordType === crmOwnerRecordType && | |
get().crmAppSlug === crmAppSlug && | |
get().crmRecordId === crmRecordId | |
) | |
return; | |
set({ | |
set({ selectedDate: newSelectionFormatted }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("date", newSelectionFormatted); | |
} | |
}, | |
addToSelectedDate: (days: number) => { | |
const currentSelectedDate = get().selectedDate; | |
const currentSelection = currentSelectedDate | |
? dayjs(currentSelectedDate) | |
: dayjs(); | |
let newSelection = currentSelection.add(days, "day"); | |
// If newSelection is before the current date, set it to today | |
if (newSelection.isBefore(dayjs(), "day")) { | |
newSelection = dayjs(); | |
} | |
const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); | |
if (newSelection.month() !== currentSelection.month()) { | |
set({ month: newSelection.format("YYYY-MM") }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("month", newSelection.format("YYYY-MM")); | |
} | |
} | |
set({ selectedDate: newSelectionFormatted }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("date", newSelectionFormatted); | |
} | |
}, |
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 223 to 245, add
handling for a null or undefined selectedDate before constructing the dayjs
instance: if get().selectedDate is null/undefined, treat currentSelection as
today (dayjs()), then proceed with computing newSelection, month and date
updates and URL param updates as before; ensure you use the resolved
currentSelection for month comparison and formatting so no dayjs is constructed
with a null value.
@@ -4,7 +4,7 @@ import { shallow } from "zustand/shallow"; | |||
import type { Dayjs } from "@calcom/dayjs"; | |||
import dayjs from "@calcom/dayjs"; | |||
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe"; | |||
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; | |||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify all DatePicker usages are within BookerStoreProvider
This file now requires BookerStoreProvider at runtime. Please verify all consumers import/use this component within a provider to avoid runtime errors.
Run this script to list all imports of calendars/DatePicker and quickly spot non-Booker contexts:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files importing @calcom/features/calendars/DatePicker:"
rg -n $'@calcom/features/calendars/DatePicker' | cut -d: -f1 | sort -u
echo
echo "Heuristic check for files that render <DatePicker ...> without nearby BookerStoreProvider:"
# List lines with DatePicker JSX; manual inspection recommended for provider presence.
rg -n -A 3 -B 3 $'<DatePicker[\\s>]' | sed -n '1,200p'
Length of output: 6298
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== packages/features/embed/Embed.tsx ==="
rg -n 'import.*DatePicker' -C3 packages/features/embed/Embed.tsx || true
rg -n '<DatePicker' -C3 packages/features/embed/Embed.tsx || true
rg -n 'BookerStoreProvider' -C3 packages/features/embed/Embed.tsx || true
echo
echo "=== packages/features/bookings/Booker/components/DatePicker.tsx ==="
rg -n 'import' -C5 packages/features/bookings/Booker/components/DatePicker.tsx || true
rg -n '<DatePicker' -C5 packages/features/bookings/Booker/components/DatePicker.tsx || true
rg -n 'BookerStoreProvider' -C5 packages/features/bookings/Booker/components/DatePicker.tsx || true
echo
echo "=== packages/features/bookings/Booker/components/DatePicker.test.tsx ==="
rg -n '<DatePicker' -C3 packages/features/bookings/Booker/components/DatePicker.test.tsx || true
rg -n 'BookerStoreProvider' -C3 packages/features/bookings/Booker/components/DatePicker.test.tsx || true
Length of output: 4935
Wrap <DatePicker>
in Embed.tsx with BookerStoreProvider
packages/features/embed/Embed.tsx currently imports and renders the core <DatePicker>
outside of any provider. Since @calcom/features/calendars/DatePicker
now calls useBookerStoreContext
, it must be rendered within <BookerStoreProvider>
to avoid “useContext” runtime errors.
Action items:
- At the top of
packages/features/embed/Embed.tsx
, add:import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
- Wrap the existing
<DatePicker>
JSX block:<div className="text-default text-sm">{t("select_date")}</div> - <DatePicker + <BookerStoreProvider> + <DatePicker isLoading={schedule.isPending} onChange={(date: Dayjs | null) => { setSelectedDate(date === null ? null : date.format("YYYY-MM-DD")); + </BookerStoreProvider>
Also audit any other direct <DatePicker>
usages (including tests) to ensure they either mock the context or are wrapped in BookerStoreProvider
.
🤖 Prompt for AI Agents
In packages/features/embed/Embed.tsx at the top of the file and in
packages/features/calendars/DatePicker.tsx around line 7, the DatePicker is
being rendered outside the BookerStoreProvider; import BookerStoreProvider from
"@calcom/features/bookings/Booker/BookerStoreProvider" at the top of Embed.tsx
and wrap the existing <DatePicker> JSX with
<BookerStoreProvider>...</BookerStoreProvider>; then audit other direct
DatePicker usages (components and tests) and either wrap them in
BookerStoreProvider or mock useBookerStoreContext in tests.
useInitializeBookerStoreContext({ | ||
...props, | ||
teamMemberEmail, | ||
crmAppSlug, | ||
crmOwnerRecordType, | ||
crmRecordId: props.crmRecordId, | ||
eventId: event?.data?.id, | ||
rescheduleUid: props.rescheduleUid ?? null, | ||
bookingUid: props.bookingUid ?? null, | ||
layout: layout, | ||
org: props.entity?.orgSlug, | ||
username, | ||
bookingData, | ||
isPlatform: true, | ||
allowUpdatingUrlParams, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initialize only the context store; remove legacy initializer
You’re initializing both the old global store (useInitializeBookerStore at Line 165) and the new context store here. Initializing the legacy singleton undermines per-instance isolation and can leak state across widgets.
Please remove the legacy initializer and rely solely on useInitializeBookerStoreContext. Suggested diff:
- useInitializeBookerStore({
- ...props,
- teamMemberEmail,
- crmAppSlug,
- crmOwnerRecordType,
- crmRecordId: props.crmRecordId,
- eventId: event?.data?.id,
- rescheduleUid: props.rescheduleUid ?? null,
- bookingUid: props.bookingUid ?? null,
- layout: layout,
- org: props.entity?.orgSlug,
- username,
- bookingData,
- isPlatform: true,
- allowUpdatingUrlParams,
- });
Also remove the unused import:
- import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+ import { /* remove legacy store imports */ } from "@calcom/features/bookings/Booker/store";
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 165 and
181-196, remove the legacy global initializer useInitializeBookerStore (the call
at ~line 165) so only useInitializeBookerStoreContext is invoked, and delete the
now-unused import for the legacy hook; ensure per-instance isolation by keeping
the existing useInitializeBookerStoreContext call and removing the old
initializer and its import from the top of the file.
💡 Verification agent
🧩 Analysis chain
Repo-wide verification: detect lingering usage of the legacy singleton store
To ensure multi-widget isolation, we should remove or quarantine useBookerStore usage outside the provider. This script highlights any remaining direct imports/usages of the legacy store in Booker paths.
Also applies to: 111-128, 17-17
🏁 Script executed:
#!/bin/bash
# 1) Find legacy store imports/usages
rg -n --no-heading $'@calcom/features/bookings/Booker/store|useBookerStore\\(|useInitializeBookerStore\\('
# 2) Specifically flag subscribe/getState calls on the legacy store
rg -n --no-heading $'useBookerStore\\.subscribe|useBookerStore\\.getState'
# 3) Ensure provider wrapping is present wherever Booker is rendered
rg -n --no-heading $'BookerStoreProvider'
Length of output: 12859
Action Required: Quarantine all legacy useBookerStore
imports and calls outside of BookerStoreProvider
Our scan shows numerous direct imports and usages of the singleton useBookerStore
(and even .subscribe
/.getState
) scattered across both feature and platform code. To guarantee true multi‐widget isolation, every component must instead consume the store via context—i.e. within a BookerStoreProvider
using useBookerStoreContext
(and only initialize via useInitializeBookerStoreContext
in your wrapper).
Key locations needing migration:
- packages/features/schedules/lib/use-schedule/useSchedule.ts
- packages/features/embed/Embed.tsx
- All files under packages/features/bookings/components/** and their hooks
- packages/features/bookings/Booker/** (components, hooks, utils)
- packages/platform/atoms/booker/BookerPlatformWrapper.tsx
- packages/platform/atoms/booker/BookerWebWrapper.tsx
Next steps:
- Remove all direct
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"
- Replace with
useBookerStoreContext
anduseInitializeBookerStoreContext
inside aBookerStoreProvider
wrapper - Eliminate any calls to
useBookerStore.subscribe
oruseBookerStore.getState
This ensures each widget instance maintains its own isolated store without leaking or sharing global state.
🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 181 to
196, quarantine and remove any direct uses or imports of the singleton
useBookerStore (including .subscribe and .getState) outside of a
BookerStoreProvider: replace imports of useBookerStore /
useInitializeBookerStore with useBookerStoreContext and
useInitializeBookerStoreContext, ensure this wrapper mounts a
BookerStoreProvider and calls useInitializeBookerStoreContext here (only), and
update all nested components to consume the store via useBookerStoreContext (not
the singleton) so no component imports or calls the global store APIs directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🔭 Outside diff range comments (3)
packages/features/bookings/Booker/components/Header.tsx (2)
113-120
: Localize aria-labels per guidelinesAvoid hardcoded strings in TSX. Wrap aria-labels with t().
- <Button + <Button className="group rtl:ml-1 rtl:rotate-180" variant="icon" color="minimal" StartIcon="chevron-left" - aria-label="Previous Day" + aria-label={t("previous_day")} onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)} /> <Button className="group rtl:mr-1 rtl:rotate-180" variant="icon" color="minimal" StartIcon="chevron-right" - aria-label="Next Day" + aria-label={t("next_day")} onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)} />Also applies to: 121-128
186-213
: Bug: "${t(...)}" renders literally; use {t(...)} in JSXScreen-reader labels currently include literal “${…}”. Replace with JSX expression to localize correctly.
- <span className="sr-only">${t("switch_monthly")}</span> + <span className="sr-only">{t("switch_monthly")}</span> @@ - <span className="sr-only">${t("switch_weekly")}</span> + <span className="sr-only">{t("switch_weekly")}</span> @@ - <span className="sr-only">${t("switch_columnview")}</span> + <span className="sr-only">{t("switch_columnview")}</span>packages/platform/atoms/hooks/useSlots.ts (1)
88-102
: Avoid stale reservation params; simplify dependency by usingselectedTimeslot
directlyThe effect depends on
timeslot
only, buthandleReserveSlot
also capturesselectedDuration
andseatedEventData.bookingUid
. If either changes, reservations may lag until the next interval or timeslot change.Recommend:
- Drop the extra
timeslot
variable and useselectedTimeslot
directly.- Include
selectedDuration
andseatedEventData?.bookingUid
in the dependency array to keep reservations in sync.- const timeslot = useBookerStoreContext((state) => state.selectedTimeslot); + // use the already-selected `selectedTimeslot` directly; no extra selector needed @@ - }, [event?.data?.id, timeslot]); + }, [event?.data?.id, selectedTimeslot, selectedDuration, seatedEventData?.bookingUid]);
🧹 Nitpick comments (8)
.changeset/fresh-rivers-glow.md (1)
5-5
: Grammar nitpick in changesetFix minor typo for clarity.
-Fixes an issues for the Booker atom wherein when multiple widgets were being placed on the same page, changes made in one widget would also get reflected in the others. +Fixes an issue in the Booker atom where placing multiple widgets on the same page caused changes in one widget to be reflected in the others.packages/features/bookings/Booker/components/DatePicker.tsx (1)
105-106
: Prefer enum over string literals for layout checksFor consistency with other files (e.g., Header.tsx), consider using BookerLayouts enum instead of string literals for layout comparisons.
-const isCompact = layout !== "month_view" && layout !== "mobile"; +import { BookerLayouts } from "@calcom/prisma/zod-utils"; +const isCompact = layout !== BookerLayouts.MONTH_VIEW && layout !== "mobile";packages/platform/atoms/booker/BookerWebWrapper.tsx (1)
7-7
: Remove redundant React importThe explicit React import is unnecessary in modern Next.js applications since React is automatically available in JSX files.
-import React from "react";
packages/features/bookings/Booker/components/DatePicker.test.tsx (1)
1-1
: Remove redundant React importThe explicit React import is unnecessary in modern test files since React is automatically available when using JSX.
-import React from "react";
packages/features/bookings/Booker/__tests__/Booker.test.tsx (1)
13-13
: Remove redundant React importThe explicit React import is unnecessary in test files since React is automatically available when using JSX.
-import React from "react";
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
37-113
: Two initializer hooks now exist; consider consolidatingThere’s duplication between
useInitializeBookerStoreContext
here anduseInitializeBookerStore
instore.ts
. Prefer a single, context-first initializer to reduce drift and confusion. You can re-export the context version and deprecate the legacy one.packages/features/bookings/Booker/store.ts (1)
451-452
: Legacy global store export retainedKeeping
useBookerStore
for backward compatibility is fine short-term. Consider adding a deprecation JSDoc to steer new code to the Provider pattern.packages/platform/atoms/booker/BookerPlatformWrapper.tsx (1)
197-199
: Optimize selector to avoid unnecessary rerendersYou select both dayCount and setDayCount but only use dayCount. Selecting functions inside the tuple prevents shallow equality from short-circuiting effectively.
- const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow); + const dayCount = useBookerStoreContext((state) => state.dayCount);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
.changeset/fresh-rivers-glow.md
(1 hunks)packages/features/bookings/Booker/Booker.tsx
(7 hunks)packages/features/bookings/Booker/BookerStoreProvider.tsx
(1 hunks)packages/features/bookings/Booker/__tests__/Booker.test.tsx
(5 hunks)packages/features/bookings/Booker/__tests__/test-utils.tsx
(1 hunks)packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
(5 hunks)packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
(4 hunks)packages/features/bookings/Booker/components/DatePicker.test.tsx
(5 hunks)packages/features/bookings/Booker/components/DatePicker.tsx
(2 hunks)packages/features/bookings/Booker/components/EventMeta.tsx
(3 hunks)packages/features/bookings/Booker/components/Header.tsx
(2 hunks)packages/features/bookings/Booker/components/hooks/useBookings.ts
(2 hunks)packages/features/bookings/Booker/components/hooks/useSlots.ts
(2 hunks)packages/features/bookings/Booker/store.ts
(2 hunks)packages/features/bookings/Booker/utils/event.ts
(3 hunks)packages/features/calendars/DatePicker.tsx
(3 hunks)packages/features/calendars/__tests__/DatePicker.test.tsx
(7 hunks)packages/platform/atoms/booker/BookerPlatformWrapper.tsx
(5 hunks)packages/platform/atoms/booker/BookerWebWrapper.tsx
(3 hunks)packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
(2 hunks)packages/platform/atoms/hooks/useSlots.ts
(4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
packages/features/bookings/Booker/components/hooks/useBookings.ts
packages/features/bookings/Booker/utils/event.ts
packages/platform/atoms/hooks/useSlots.ts
packages/features/bookings/Booker/components/hooks/useSlots.ts
packages/features/bookings/Booker/store.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts
packages/features/bookings/Booker/components/hooks/useBookings.ts
packages/features/calendars/__tests__/DatePicker.test.tsx
packages/features/bookings/Booker/BookerStoreProvider.tsx
packages/features/bookings/Booker/components/DatePicker.tsx
packages/features/bookings/Booker/utils/event.ts
packages/features/bookings/Booker/__tests__/test-utils.tsx
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
packages/platform/atoms/booker/BookerWebWrapper.tsx
packages/platform/atoms/hooks/useSlots.ts
packages/features/bookings/Booker/components/DatePicker.test.tsx
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
packages/features/calendars/DatePicker.tsx
packages/features/bookings/Booker/components/hooks/useSlots.ts
packages/features/bookings/Booker/Booker.tsx
packages/features/bookings/Booker/components/EventMeta.tsx
packages/features/bookings/Booker/__tests__/Booker.test.tsx
packages/features/bookings/Booker/components/Header.tsx
packages/platform/atoms/booker/BookerPlatformWrapper.tsx
packages/features/bookings/Booker/store.ts
**/*.tsx
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Always use
t()
for text localization in frontend code; direct text embedding should trigger a warning
Files:
packages/features/calendars/__tests__/DatePicker.test.tsx
packages/features/bookings/Booker/BookerStoreProvider.tsx
packages/features/bookings/Booker/components/DatePicker.tsx
packages/features/bookings/Booker/__tests__/test-utils.tsx
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx
packages/platform/atoms/booker/BookerWebWrapper.tsx
packages/features/bookings/Booker/components/DatePicker.test.tsx
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
packages/features/calendars/DatePicker.tsx
packages/features/bookings/Booker/Booker.tsx
packages/features/bookings/Booker/components/EventMeta.tsx
packages/features/bookings/Booker/__tests__/Booker.test.tsx
packages/features/bookings/Booker/components/Header.tsx
packages/platform/atoms/booker/BookerPlatformWrapper.tsx
🧬 Code Graph Analysis (18)
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/hooks/useBookings.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/calendars/__tests__/DatePicker.test.tsx (5)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
BookerStoreProvider
(15-22)packages/platform/atoms/src/components/ui/tooltip.tsx (1)
TooltipProvider
(14-14)packages/features/calendars/DatePicker.tsx (1)
DatePicker
(493-493)packages/features/bookings/Booker/components/DatePicker.tsx (1)
DatePicker
(54-153)packages/platform/libraries/index.ts (1)
PeriodType
(30-30)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/utils/event.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/__tests__/test-utils.tsx (3)
packages/features/bookings/Booker/store.ts (1)
BookerStore
(51-174)packages/embeds/embed-core/src/types.ts (1)
BookerLayouts
(6-6)packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
BookerStoreContext
(9-9)
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (2)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)packages/embeds/embed-core/src/types.ts (1)
BookerLayouts
(6-6)
packages/platform/atoms/booker/BookerWebWrapper.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
BookerStoreProvider
(15-22)
packages/platform/atoms/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/calendars/DatePicker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/hooks/useSlots.ts (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/Booker.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/components/EventMeta.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/features/bookings/Booker/__tests__/Booker.test.tsx (1)
packages/features/bookings/Booker/Booker.tsx (1)
Booker
(593-599)
packages/features/bookings/Booker/components/Header.tsx (1)
packages/features/bookings/Booker/BookerStoreProvider.tsx (1)
useBookerStoreContext
(24-35)
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (1)
packages/platform/atoms/booker/types.ts (2)
BookerPlatformWrapperAtomPropsForIndividual
(92-96)BookerPlatformWrapperAtomPropsForTeam
(98-103)
packages/features/bookings/Booker/store.ts (3)
packages/features/bookings/Booker/types.ts (2)
BookerState
(154-154)BookerLayout
(155-155)packages/embeds/embed-core/src/types.ts (1)
BookerLayouts
(6-6)packages/lib/constants.ts (1)
BOOKER_NUMBER_OF_DAYS_TO_LOAD
(170-173)
🔇 Additional comments (39)
packages/features/bookings/Booker/components/DatePicker.tsx (1)
5-5
: Migration Incomplete: DirectuseBookerStore
References DetectedThe migration to
useBookerStoreContext
is not yet complete—there are numerous directuseBookerStore
imports and calls still in the codebase. Please replace these withuseBookerStoreContext
(and ensure the corresponding provider wraps each entry point). Key locations needing attention:• packages/platform/atoms/booker/BookerWebWrapper.tsx
• packages/platform/atoms/booker/BookerPlatformWrapper.tsx
• packages/features/schedules/lib/use-schedule/useSchedule.ts
• packages/features/bookings/components/AvailableTimes.tsx
• packages/features/embed/Embed.tsx
• All remaining imports ofuseBookerStore
underpackages/features/bookings/
Example replacement in
BookerWebWrapper.tsx
:- import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; + import { useBookerStoreContext, useInitializeBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";Continue this pattern project-wide to remove all direct store hooks and ensure every component consuming the store is wrapped by
BookerStoreProvider
.Likely an incorrect or invalid review comment.
packages/features/bookings/Booker/utils/event.ts (1)
3-3
: LGTM: context-based store migration is correctSelectors and shallow equality usage are appropriate. This aligns with per-instance store isolation.
Also applies to: 22-25, 92-95
packages/features/bookings/Booker/components/Header.tsx (1)
7-7
: LGTM: store access migrated to contextHeader now correctly uses useBookerStoreContext for both reads and actions.
Also applies to: 39-43
packages/features/calendars/__tests__/DatePicker.test.tsx (1)
7-7
: Tests correctly wrap DatePicker with BookerStoreProviderGood adjustment to match useBookerStoreContext usage; keeps tests aligned with runtime usage.
Also applies to: 20-36, 46-51, 61-66, 95-113, 139-157, 179-197
packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts (1)
4-4
: LGTM: context-based migration preserves isolationAll booker state reads/actions are via useBookerStoreContext, removing global coupling.
Also applies to: 50-66
packages/platform/atoms/booker/BookerWebWrapper.tsx (3)
36-268
: LGTM: Clean extraction of internal componentThe extraction of the previous BookerWebWrapper logic into BookerPlatformWrapperComponent is well-implemented and maintains all existing functionality while enabling the context-based store pattern.
188-191
: Good: Persistent overlay calendar preferenceThe localStorage implementation correctly persists the overlay calendar default state across page reloads, improving user experience by remembering their preference.
270-276
: LGTM: Context provider wrapper maintains backward compatibilityThe new BookerWebWrapper correctly wraps the internal component with BookerStoreProvider, enabling per-instance store isolation while preserving the original public API.
packages/features/bookings/Booker/components/DatePicker.test.tsx (2)
64-64
: LGTM: Fixed Day.js parameter typesThe correction from string parameters (
"1"
) to number parameters (1
) indayjs().add()
calls is proper usage and aligns with Day.js API expectations.Also applies to: 106-106, 114-114
76-81
: Good: Context-based test setupThe new mockStore configuration approach enables per-test state initialization through the BookerStoreProvider context, providing better test isolation and more explicit test setup.
Also applies to: 104-108
packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx (3)
46-46
: LGTM: Explicit timeslot prop improves component clarityMaking the selected timeslot an explicit prop rather than internal state makes the component's dependencies clearer and aligns with the centralized timeslot management pattern.
Also applies to: 66-66
75-79
: Good: Context-based store access migrationThe migration from useBookerStore to useBookerStoreContext maintains the same selectors while enabling proper context-based state management and store isolation per Booker instance.
94-103
: Appropriate fallback for missing timeslotThe EmptyScreen component provides clear user feedback when no timeslot is selected, with actionable guidance to return and select a time. This improves the user experience for edge cases.
packages/features/bookings/Booker/__tests__/Booker.test.tsx (3)
23-23
: Good: Proper type casting for mockThe type casting
as any
for the framer-motion mock is appropriate since we're dealing with a dynamic import mock that may not have complete type information.
177-179
: Excellent: Per-test store configurationThe new mockStore approach provides better test isolation by configuring initial state per-test rather than mutating a shared global state. This makes tests more predictable and easier to debug.
Also applies to: 196-203, 221-222, 242-244
169-169
: LGTM: Updated rollout constantThe update to PUBLIC_QUICK_AVAILABILITY_ROLLOUT from a percentage to "100" aligns with the new expected behavior in the system.
packages/features/bookings/Booker/components/hooks/useSlots.ts (1)
5-5
: LGTM: Clean migration to context-based store accessThe migration from useBookerStore to useBookerStoreContext maintains identical selectors and shallow comparison patterns while enabling proper context-based state management. The hook's public interface and behavior remain unchanged.
Also applies to: 81-81, 84-92
packages/features/bookings/Booker/components/AvailableTimeSlots.tsx (3)
52-52
: Good: Callback pattern for centralized timeslot managementThe new onAvailableTimeSlotSelect callback prop enables centralized timeslot selection management while maintaining clear component boundaries and responsibilities.
Also applies to: 76-76
79-79
: LGTM: Context-based store access migrationThe migration from useBookerStore to useBookerStoreContext maintains the same selectors while enabling proper context-based state management consistent with the overall architectural changes.
Also applies to: 81-81, 83-83, 86-89
151-151
: Proper callback usage and dependenciesReplacing the direct setSelectedTimeslot call with the onAvailableTimeSlotSelect callback correctly delegates timeslot management to the parent component. The callback is appropriately included in the useCallback dependencies array.
Also applies to: 167-167
packages/features/bookings/Booker/components/EventMeta.tsx (4)
57-57
: LGTM: Explicit selectedTimeslot prop improves component clarityMaking the selected timeslot an explicit prop rather than internal state makes the component's dependencies clearer and enables better control over when the time selection is displayed.
Also applies to: 95-95
100-108
: Good: Context-based store access migrationThe migration from useBookerStore to useBookerStoreContext maintains the same selectors and shallow comparison patterns while enabling proper context-based state management.
206-216
: Appropriate conditional rendering for timeslotThe conditional rendering based on selectedTimeslot prop ensures the calendar block only appears when a timeslot is actually selected, improving the user interface clarity.
248-250
: Good: Dual timezone state managementThe timezone onChange handler correctly updates both the local timePreferences state and the Booker store context, maintaining consistency across the application's timezone handling.
packages/features/calendars/DatePicker.tsx (1)
7-7
: LGTM: Straightforward context-based store migrationThe migration from useBookerStore to useBookerStoreContext maintains identical selectors and behavior while enabling proper context-based state management. The calendar functionality remains unchanged.
Also applies to: 227-227, 395-395
packages/platform/atoms/hooks/useSlots.ts (3)
39-43
: Good move to per-instance store via contextSelectors + shallow look correct and avoid re-renders. This removes the shared global-store coupling.
55-55
: Selector usage LGTMAccessing seatedEventData from the context is consistent with the new store shape.
5-6
: AlluseSlots
invocations are already wrapped inBookerStoreProvider
I’ve verified that both consumers of
useSlots
live inside the provider:
- In
packages/platform/atoms/booker/BookerPlatformWrapper.tsx
, the hook call at line 386 is within the<BookerStoreProvider>
wrapper (lines 586–589).- In
packages/platform/atoms/booker/BookerWebWrapper.tsx
, the hook call at line 124 is inside the<BookerStoreProvider>
wrapper (lines 272–274).No further changes needed.
packages/features/bookings/Booker/Booker.tsx (3)
87-94
: Context migration looks goodReading
[state, setState]
,selectedDate
, andsetSelectedDate
fromuseBookerStoreContext
is correct and isolates Booker instances per provider.
107-115
: LGTM on additional selectorsUsing context for
[seatedEventData, setSeatedEventData]
and[dayCount, setDayCount]
aligns the component with the new store architecture.
194-197
: Timeslot selection flow is clear; watch out for cross-instance URL couplingHooking
AvailableTimeSlots
tosetSelectedTimeslot
and threadingselectedTimeslot
down to consumers is clean.However,
setSelectedTimeslot
updates the shared URL query param (slot
) in the store. With multiple Booker instances on the same page andallowUpdatingUrlParams=true
, changing one could inadvertently affect others via URL state.Consider:
- Defaulting
allowUpdatingUrlParams
to false when multiple providers are detected, or- Namespacing query params per instance (e.g.,
slot_<instanceId>
)Can you confirm multi-widget pages set
allowUpdatingUrlParams=false
for all instances in production scenarios, or adopt namespacing?Also applies to: 492-512
packages/features/bookings/Booker/BookerStoreProvider.tsx (2)
15-22
: Per-provider singleton store is correctUsing
useRef
to create one store instance per provider ensures isolation across multiple widgets on the page.
24-35
: Selector hook with guard is solidThrowing outside the provider prevents silent misuse.
useStore(context, selector, equalityFn)
keeps components efficient.packages/features/bookings/Booker/store.ts (3)
271-275
: Default dayCount initialization LGTMUsing BOOKER_NUMBER_OF_DAYS_TO_LOAD to conditionally seed
dayCount
is fine and keeps behavior configurable.
361-385
: Initialization logic is robust; nice handling for reschedule + durationDuration sync, clearing slot on reschedule, and URL updates look correct.
431-435
: Cross-instance coupling via URL paramsEven with isolated stores, updating query params (
date
,month
,slot
,duration
,bookingUid
) is a shared global channel. On pages with multiple Booker instances andallowUpdatingUrlParams=true
, one instance may influence another through URL-driven effects.Mitigations:
- Default
allowUpdatingUrlParams
to false when multiple providers are mounted.- Namespace params per instance (e.g.,
slot_<instanceId>
), withinstanceId
provided via props.- Gate reading of query params behind
isPlatform && allowUpdatingUrlParams
as you already do for writes.Do you want me to draft a minimal namespacing design (provider-level instanceId + helpers), or should we just flip allowUpdatingUrlParams to false by default for multi-instance scenarios?
Also applies to: 295-406
packages/platform/atoms/booker/BookerPlatformWrapper.tsx (3)
9-13
: Context provider imports look correct for per-instance isolationGood move importing BookerStoreProvider and the context hooks; aligns with the goal of isolating each widget’s state.
200-202
: LGTM: reading month and eventSlug from contextThis aligns with the per-instance store usage.
582-590
: LGTM: Provider at the public wrapper boundary ensures store isolation per widgetWrapping BookerPlatformWrapper with BookerStoreProvider correctly guarantees a separate store per widget instance. Verify that other wrappers (e.g., BookerWebWrapper) don’t double-wrap unintentionally.
To double-check isolation manually:
- Render two BookerPlatformWrapper instances side by side with allowUpdatingUrlParams={false}.
- Change selected date/duration in one; confirm the other stays unchanged.
const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => { | ||
let state: BookerStore = { | ||
username: null, | ||
eventSlug: null, | ||
eventId: null, | ||
verifiedEmail: null, | ||
setVerifiedEmail: vi.fn(), | ||
month: dayjs().format("YYYY-MM"), | ||
setMonth: vi.fn(), | ||
state: "loading", | ||
setState: vi.fn(), | ||
layout: BookerLayouts.MONTH_VIEW, | ||
setLayout: vi.fn(), | ||
selectedDate: null, | ||
setSelectedDate: vi.fn(), | ||
addToSelectedDate: vi.fn(), | ||
selectedDatesAndTimes: null, | ||
setSelectedDatesAndTimes: vi.fn(), | ||
durationConfig: null, | ||
selectedDuration: null, | ||
setSelectedDuration: vi.fn(), | ||
selectedTimeslot: null, | ||
setSelectedTimeslot: vi.fn(), | ||
tentativeSelectedTimeslots: [], | ||
setTentativeSelectedTimeslots: vi.fn(), | ||
recurringEventCount: null, | ||
setRecurringEventCount: vi.fn(), | ||
occurenceCount: null, | ||
setOccurenceCount: vi.fn(), | ||
dayCount: null, | ||
setDayCount: vi.fn(), | ||
rescheduleUid: null, | ||
rescheduledBy: null, | ||
bookingUid: null, | ||
bookingData: null, | ||
setBookingData: vi.fn(), | ||
initialize: vi.fn(), | ||
formValues: {}, | ||
setFormValues: vi.fn(), | ||
isTeamEvent: false, | ||
seatedEventData: {}, | ||
setSeatedEventData: vi.fn(), | ||
isInstantMeeting: false, | ||
org: null, | ||
setOrg: vi.fn(), | ||
timezone: null, | ||
setTimezone: vi.fn(), | ||
teamMemberEmail: null, | ||
crmOwnerRecordType: null, | ||
crmAppSlug: null, | ||
crmRecordId: null, | ||
isPlatform: false, | ||
allowUpdatingUrlParams: true, | ||
...initialState, | ||
}; | ||
|
||
state.setMonth = vi.fn((month: string | null) => { | ||
state.month = month; | ||
}); | ||
|
||
return { | ||
getState: () => state, | ||
setState: vi.fn((updater) => { | ||
if (typeof updater === "function") { | ||
const newState = updater(state); | ||
state = { ...state, ...newState }; | ||
} else { | ||
state = { ...state, ...updater }; | ||
} | ||
}), | ||
subscribe: vi.fn(), | ||
destroy: vi.fn(), | ||
} as unknown as StoreApi<BookerStore>; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mock StoreApi must implement subscribe and notify; setters should update state
Current mock store won’t work with useStore
:
subscribe
returns undefined;useStore
expects an unsubscribe function.setState
doesn’t notify subscribers.- Most setters are
vi.fn()
no-ops, so UI won’t re-render in tests.
Implement a minimal reactive store:
@@
-const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
- let state: BookerStore = {
+const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerStore> => {
+ type Listener = (next: BookerStore, prev: BookerStore) => void;
+ const listeners = new Set<Listener>();
+ let state: BookerStore = {
@@
- layout: BookerLayouts.MONTH_VIEW,
+ layout: BookerLayouts.MONTH_VIEW,
setLayout: vi.fn(),
@@
- setSelectedDate: vi.fn(),
+ setSelectedDate: vi.fn((params: { date: string | null }) => {
+ const next = { ...state, selectedDate: params.date ?? null };
+ const prev = state;
+ state = next;
+ listeners.forEach((l) => l(next, prev));
+ }),
@@
- selectedDuration: null,
- setSelectedDuration: vi.fn(),
+ selectedDuration: null,
+ setSelectedDuration: vi.fn((duration: number | null) => {
+ const next = { ...state, selectedDuration: duration };
+ const prev = state;
+ state = next;
+ listeners.forEach((l) => l(next, prev));
+ }),
@@
- selectedTimeslot: null,
- setSelectedTimeslot: vi.fn(),
+ selectedTimeslot: null,
+ setSelectedTimeslot: vi.fn((slot: string | null) => {
+ const next = { ...state, selectedTimeslot: slot };
+ const prev = state;
+ state = next;
+ listeners.forEach((l) => l(next, prev));
+ }),
@@
- seatedEventData: {},
+ seatedEventData: {
+ seatsPerTimeSlot: undefined,
+ attendees: undefined,
+ bookingUid: undefined,
+ showAvailableSeatsCount: true,
+ },
setSeatedEventData: vi.fn(),
@@
- state.setMonth = vi.fn((month: string | null) => {
- state.month = month;
- });
+ state.setMonth = vi.fn((month: string | null) => {
+ const next = { ...state, month };
+ const prev = state;
+ state = next;
+ listeners.forEach((l) => l(next, prev));
+ });
@@
- return {
- getState: () => state,
- setState: vi.fn((updater) => {
- if (typeof updater === "function") {
- const newState = updater(state);
- state = { ...state, ...newState };
- } else {
- state = { ...state, ...updater };
- }
- }),
- subscribe: vi.fn(),
- destroy: vi.fn(),
- } as unknown as StoreApi<BookerStore>;
+ return {
+ getState: () => state,
+ setState: vi.fn((updater: Partial<BookerStore> | ((s: BookerStore) => Partial<BookerStore>)) => {
+ const partial = typeof updater === "function" ? updater(state) : updater;
+ const next = { ...state, ...partial };
+ const prev = state;
+ state = next;
+ listeners.forEach((l) => l(next, prev));
+ }),
+ subscribe: vi.fn((listener: Listener) => {
+ listeners.add(listener);
+ return () => {
+ listeners.delete(listener);
+ };
+ }),
+ destroy: vi.fn(),
+ } as unknown as StoreApi<BookerStore>;
Optionally implement similar updates for: setDayCount, setFormValues, setBookingData, setState, setLayout—so tests relying on them re-render deterministically.
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/__tests__/test-utils.tsx around lines 18 to
91, the mock StoreApi doesn't implement subscription notification and most
setter fns are no-ops so useStore won't re-render in tests; implement a minimal
reactive store by keeping an internal array of subscribers, make subscribe push
a listener and return an unsubscribe function, update setState to compute the
new state (handling updater functions), assign it to state, then call all
subscribers with the new state, and replace vi.fn() no-op setters (at least
setMonth, setDayCount, setFormValues, setBookingData, setLayout and any other
setters used in tests) with functions that mutate state appropriately and then
notify subscribers via the same subscriber notifier so tests re-render
deterministically.
<BookEventForm | ||
key={key} | ||
timeslot={selectedTimeslot} | ||
shouldRenderCaptcha={shouldRenderCaptcha} | ||
onCancel={() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Prop threading sanity-check
Passing timeslot={selectedTimeslot}
to BookEventForm and selectedTimeslot
to EventMeta requires updated prop types/usages downstream. If not already done, ensure those components accept and use these props.
Run:
Also applies to: 407-407
🏁 Script executed:
#!/bin/bash
rg -n -A2 -B2 'BookEventForm\\(' packages/features/bookings
rg -n -A2 -B2 'interface .*BookEventForm.*props|type .*BookEventForm.*Props' packages/features/bookings
rg -n -A2 -B2 'EventMeta\\(' packages/features/bookings
Length of output: 904
🏁 Script executed:
#!/bin/bash
# Locate BookEventForm usage sites
rg -n 'BookEventForm\(' -A2 -B2 packages/features/bookings
# Inspect the BookEventFormProps definition and check for timeslot prop
rg -n 'type BookEventFormProps' -A10 packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
rg -n 'timeslot' -n packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx
# Locate EventMeta usage sites
rg -n 'EventMeta\(' -A2 -B2 packages/features/bookings
# Inspect the EventMetaProps (if defined) and check for timeslot handling
rg -n 'type EventMetaProps' -A10 packages/features/bookings
rg -n 'timeslot' -n packages/features/bookings
Length of output: 10075
🏁 Script executed:
#!/bin/bash
# Inspect BookEventForm usage context in Booker.tsx
rg -n 'BookEventForm' -A5 -B5 packages/features/bookings/Booker/Booker.tsx
# Search for EventMeta usages and its prop definitions across the repo
rg -n 'EventMeta' -R .
rg -n 'import.*EventMeta' -R packages/features/bookings
rg -n 'type EventMetaProps' -A10 -R packages/features
Length of output: 2807
🏁 Script executed:
#!/bin/bash
# Find where EventMeta is rendered based on bookerState
rg -n 'bookerState ===' -A5 packages/features/bookings/Booker/Booker.tsx
# Locate EventMeta usage and check passed props
rg -n 'EventMeta' -A5 -B5 packages/features/bookings/Booker/Booker.tsx
# Inspect EventMetaProps definition for timeslot
rg -n 'interface EventMetaProps' -A10 packages/features/bookings/Booker/components/EventMeta.tsx
# Verify if timeslot prop is declared or used in EventMeta component
rg -n 'timeslot' -n packages/features/bookings/Booker/components/EventMeta.tsx
Length of output: 4048
Update EventMeta to accept selectedTimeslot
It looks like you’ve correctly threaded the timeslot
prop into BookEventForm
(its props type already includes timeslot: string | null
), but the EventMeta
component itself doesn’t define or consume a selectedTimeslot
prop. You’ll need to:
• In packages/features/bookings/Booker/components/EventMeta.tsx
, add selectedTimeslot?: string | null
(or required) to its props definition.
• Destructure and use selectedTimeslot
inside EventMeta
wherever you need to display or conditionally render based on the currently selected slot.
This will keep the type system happy and ensure EventMeta
actually responds to the timeslot state you’re passing in.
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/components/EventMeta.tsx (where the
component props are defined), EventMeta currently lacks a selectedTimeslot prop
even though the parent passes timeslot/selectedTimeslot; add selectedTimeslot?:
string | null (or selectedTimeslot: string | null if required) to the
component's props type, destructure selectedTimeslot from the props in the
component signature, and then reference it where appropriate (render or
conditionally render UI based on the currently selected slot) so the component
consumes the passed timeslot and the TypeScript types align.
@@ -8,6 +8,7 @@ import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client"; | |||
import { useHandleBookEvent } from "@calcom/atoms/hooks/bookings/useHandleBookEvent"; | |||
import dayjs from "@calcom/dayjs"; | |||
import { sdkActionManager } from "@calcom/embed-core/embed-iframe"; | |||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Critical: onError still writes to the global store; breaks widget isolation
The error path uses useBookerStore.setState to update rescheduleUid and bookingData, which reintroduces the shared global store problem this PR fixes. Route these updates through the context store instead.
Suggested fix (assuming the store exposes actions setRescheduleUid and setBookingData):
@@
-import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
-import { useBookerStore } from "@calcom/features/bookings/Booker/store";
+import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
+import { shallow } from "zustand/shallow";
@@
- const duration = useBookerStoreContext((state) => state.selectedDuration);
+ const duration = useBookerStoreContext((state) => state.selectedDuration);
+ const [setRescheduleUidCtx, setBookingDataCtx] = useBookerStoreContext(
+ (s) => [s.setRescheduleUid, s.setBookingData],
+ shallow
+ );
@@
- if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
- useBookerStore.setState({
- rescheduleUid: error.data?.rescheduleUid,
- });
- useBookerStore.setState({
- bookingData: {
- uid: error.data?.rescheduleUid,
- startTime: error.data?.startTime,
- attendees: error.data?.attendees,
- } as unknown as GetBookingType,
- });
- }
+ if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
+ setRescheduleUidCtx(error.data.rescheduleUid);
+ setBookingDataCtx({
+ uid: error.data.rescheduleUid,
+ startTime: error.data.startTime,
+ attendees: error.data.attendees,
+ } as unknown as GetBookingType);
+ }
If these actions don't exist yet, please add them to the Booker store (and its type) to keep all mutations instance-scoped.
Also applies to: 305-314
🏁 Script executed:
#!/bin/bash
# Find any remaining direct store mutations that bypass the context (should be none).
rg -n --glob '!**/__tests__/**' 'useBookerStore\.setState' packages | sed 's/^/ /'
Length of output: 297
I’d like to verify whether the context store already exposes the setRescheduleUid
and setBookingData
actions.
🏁 Script executed:
#!/bin/bash
# Check if context or store files define the requested actions
rg -n 'setRescheduleUid' -A2 packages/features/bookings
rg -n 'setBookingData' -A2 packages/features/bookings
Length of output: 870
Replace direct useBookerStore.setState
calls with context actions and add setRescheduleUid
The hook still writes to the global store at lines 305 and 308, re-introducing shared state. We need to route these mutations through the Booker context and add a missing setRescheduleUid
action to the store and context.
• packages/features/bookings/Booker/components/hooks/useBookings.ts
– Remove useBookerStore.setState
calls at lines 305 and 308
– Destructure setRescheduleUid
and setBookingData
from useBookerStoreContext
using zustand/shallow
• packages/features/bookings/Booker/store.ts
– Add a setRescheduleUid: (uid: string | null) => void
action under the store type (around line 138)
– Implement it to set({ rescheduleUid: uid ?? null })
(mirror setBookingData
)
• BookerStoreProvider
– Ensure the new action is exposed via the context provider’s value and type
Suggested diff in useBookings.ts
:
@@
-import { useBookerStore } from "@calcom/features/bookings/Booker/store";
+import { shallow } from "zustand/shallow";
@@
- const duration = useBookerStoreContext((state) => state.selectedDuration);
+ const duration = useBookerStoreContext((s) => s.selectedDuration);
+ const [setRescheduleUid, setBookingData] = useBookerStoreContext(
+ (s) => [s.setRescheduleUid, s.setBookingData],
+ shallow
+ );
@@
- if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
- useBookerStore.setState({ rescheduleUid: error.data.rescheduleUid });
- useBookerStore.setState({
- bookingData: {
- uid: error.data.rescheduleUid,
- startTime: error.data.startTime,
- attendees: error.data.attendees,
- } as GetBookingType,
- });
- }
+ if (
+ error.message === ErrorCode.BookerLimitExceededReschedule &&
+ error.data?.rescheduleUid
+ ) {
+ setRescheduleUid(error.data.rescheduleUid);
+ setBookingData({
+ uid: error.data.rescheduleUid,
+ startTime: error.data.startTime,
+ attendees: error.data.attendees,
+ } as GetBookingType);
+ }
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/components/hooks/useBookings.ts around line
11 and usages at lines 305 and 308, remove direct useBookerStore.setState calls
and instead destructure setRescheduleUid and setBookingData from
useBookerStoreContext (import shallow from 'zustand/shallow' and use const {
setRescheduleUid, setBookingData } = useBookerStoreContext(state => ({
setRescheduleUid: state.setRescheduleUid, setBookingData: state.setBookingData
}), shallow)), then replace the two setState calls with calls to
setRescheduleUid(uid) and setBookingData(data); in
packages/features/bookings/Booker/store.ts add a new action setRescheduleUid:
(uid: string | null) => void to the store type around line 138 and implement it
as set({ rescheduleUid: uid ?? null }) mirroring setBookingData; finally ensure
BookerStoreProvider exposes setRescheduleUid in its provided value and type so
the hook can use it.
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { | ||
// unset selected date | ||
if (!selectedDate) { | ||
removeQueryParam("date"); | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: clearing selected date doesn’t update store state
When selectedDate
is falsy, you remove the date
query param but return without clearing selectedDate
in the store. This causes stale UI state (and affects setMonth
which calls this to clear the date).
- if (!selectedDate) {
- removeQueryParam("date");
- return;
- }
+ if (!selectedDate) {
+ removeQueryParam("date");
+ set({ selectedDate: null });
+ return;
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { | |
// unset selected date | |
if (!selectedDate) { | |
removeQueryParam("date"); | |
return; | |
} | |
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { | |
// unset selected date | |
if (!selectedDate) { | |
removeQueryParam("date"); | |
set({ selectedDate: null }); | |
return; | |
} |
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 196 to 201, when
selectedDate is falsy the code removes the "date" query param but returns
without clearing the store's selectedDate, leaving stale state; update the
branch so before returning it also clears the store's selectedDate (set it to
null/undefined per project convention) while preserving the
omitUpdatingParams/preventMonthSwitching behavior and side effects so the UI and
callers like setMonth no longer see the old date.
setSelectedDuration: (selectedDuration: number | null) => { | ||
set({ selectedDuration }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("duration", selectedDuration ?? ""); | ||
} | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consistency: remove duration
query param when unset
Currently setting selectedDuration
to null writes an empty duration=
param. Prefer removing it entirely for consistency with other unset paths.
- set({ selectedDuration });
- if (!get().isPlatform || get().allowUpdatingUrlParams) {
- updateQueryParam("duration", selectedDuration ?? "");
- }
+ set({ selectedDuration });
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ if (selectedDuration == null) removeQueryParam("duration");
+ else updateQueryParam("duration", String(selectedDuration));
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
setSelectedDuration: (selectedDuration: number | null) => { | |
set({ selectedDuration }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("duration", selectedDuration ?? ""); | |
} | |
}, | |
setSelectedDuration: (selectedDuration: number | null) => { | |
set({ selectedDuration }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
if (selectedDuration == null) { | |
removeQueryParam("duration"); | |
} else { | |
updateQueryParam("duration", String(selectedDuration)); | |
} | |
} | |
}, |
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 409 to 414, when
selectedDuration is set to null the code currently writes an empty "duration="
query param; change the call so the query param is removed instead of set empty
— i.e. when selectedDuration is null call the query-removal behavior (pass
undefined/null to updateQueryParam if that removes the key or call the existing
removeQueryParam helper) otherwise pass the numeric value; ensure the
conditional uses selectedDuration === null (or nullish) to trigger removal so
other unset params remain consistent.
setSelectedTimeslot: (selectedTimeslot: string | null) => { | ||
set({ selectedTimeslot }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("slot", selectedTimeslot ?? "", false); | ||
} | ||
} | ||
//removeQueryParam("layout"); | ||
}, | ||
durationConfig: null, | ||
selectedDuration: null, | ||
setSelectedDuration: (selectedDuration: number | null) => { | ||
set({ selectedDuration }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("duration", selectedDuration ?? ""); | ||
} | ||
}, | ||
setBookingData: (bookingData: GetBookingType | null | undefined) => { | ||
set({ bookingData: bookingData ?? null }); | ||
}, | ||
recurringEventCount: null, | ||
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }), | ||
occurenceCount: null, | ||
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }), | ||
rescheduleUid: null, | ||
bookingData: null, | ||
bookingUid: null, | ||
selectedTimeslot: getQueryParam("slot") || null, | ||
tentativeSelectedTimeslots: [], | ||
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => { | ||
set({ tentativeSelectedTimeslots }); | ||
}, | ||
setSelectedTimeslot: (selectedTimeslot: string | null) => { | ||
set({ selectedTimeslot }); | ||
if (!get().isPlatform || get().allowUpdatingUrlParams) { | ||
updateQueryParam("slot", selectedTimeslot ?? "", false); | ||
} | ||
}, | ||
formValues: {}, | ||
setFormValues: (formValues: Record<string, any>) => { | ||
set({ formValues }); | ||
}, | ||
org: null, | ||
setOrg: (org: string | null | undefined) => { | ||
set({ org }); | ||
}, | ||
isPlatform: false, | ||
allowUpdatingUrlParams: true, | ||
})); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consistency: remove slot
query param when unsetting timeslot
Writing slot=
on null can confuse consumers and complicate parsing. Remove the param instead.
- set({ selectedTimeslot });
- if (!get().isPlatform || get().allowUpdatingUrlParams) {
- updateQueryParam("slot", selectedTimeslot ?? "", false);
- }
+ set({ selectedTimeslot });
+ if (!get().isPlatform || get().allowUpdatingUrlParams) {
+ if (selectedTimeslot == null) removeQueryParam("slot");
+ else updateQueryParam("slot", selectedTimeslot, false);
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
setSelectedTimeslot: (selectedTimeslot: string | null) => { | |
set({ selectedTimeslot }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("slot", selectedTimeslot ?? "", false); | |
} | |
} | |
//removeQueryParam("layout"); | |
}, | |
durationConfig: null, | |
selectedDuration: null, | |
setSelectedDuration: (selectedDuration: number | null) => { | |
set({ selectedDuration }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("duration", selectedDuration ?? ""); | |
} | |
}, | |
setBookingData: (bookingData: GetBookingType | null | undefined) => { | |
set({ bookingData: bookingData ?? null }); | |
}, | |
recurringEventCount: null, | |
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }), | |
occurenceCount: null, | |
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }), | |
rescheduleUid: null, | |
bookingData: null, | |
bookingUid: null, | |
selectedTimeslot: getQueryParam("slot") || null, | |
tentativeSelectedTimeslots: [], | |
setTentativeSelectedTimeslots: (tentativeSelectedTimeslots: string[]) => { | |
set({ tentativeSelectedTimeslots }); | |
}, | |
setSelectedTimeslot: (selectedTimeslot: string | null) => { | |
set({ selectedTimeslot }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
updateQueryParam("slot", selectedTimeslot ?? "", false); | |
} | |
}, | |
formValues: {}, | |
setFormValues: (formValues: Record<string, any>) => { | |
set({ formValues }); | |
}, | |
org: null, | |
setOrg: (org: string | null | undefined) => { | |
set({ org }); | |
}, | |
isPlatform: false, | |
allowUpdatingUrlParams: true, | |
})); | |
}, | |
setSelectedTimeslot: (selectedTimeslot: string | null) => { | |
set({ selectedTimeslot }); | |
if (!get().isPlatform || get().allowUpdatingUrlParams) { | |
if (selectedTimeslot == null) removeQueryParam("slot"); | |
else updateQueryParam("slot", selectedTimeslot, false); | |
} | |
}, |
🤖 Prompt for AI Agents
In packages/features/bookings/Booker/store.ts around lines 430 to 435, the code
currently writes "slot=" when selectedTimeslot is null; change the
updateQueryParam call so it removes the param instead of setting it to an empty
string. Concretely, when selectedTimeslot is null/undefined call
updateQueryParam("slot", null, false) (or undefined if that matches the helper's
removal behavior) and otherwise pass the timeslot value; adjust the expression
(e.g. use selectedTimeslot ?? null) so the param is removed rather than set to
an empty string.
const [bookerState, setBookerState] = useBookerStoreContext( | ||
(state) => [state.state, state.setState], | ||
shallow | ||
); | ||
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate); | ||
const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration); | ||
const setBookingData = useBookerStoreContext((state) => state.setBookingData); | ||
const setOrg = useBookerStoreContext((state) => state.setOrg); | ||
const bookingData = useBookerStoreContext((state) => state.bookingData); | ||
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot); | ||
const setSelectedMonth = useBookerStoreContext((state) => state.setMonth); | ||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Mixed store usage: onBookerStateChange still subscribes to legacy global store — leaks across widgets
You correctly switched most reads/writes to useBookerStoreContext, but the onBookerStateChange effect still uses useBookerStore.subscribe/getState (legacy global store). That will cause callbacks to fire for changes from other widget instances.
Replace the legacy subscription with a context-scoped selection so the callback tracks only this instance.
Apply these diffs:
- Type the selector against the store type (and decouple from legacy hook type):
@@
- const getStateValues = useCallback(
- (state: ReturnType<typeof useBookerStore.getState>): BookerStoreValues => {
+ const getStateValues = useCallback(
+ (state: BookerStore): BookerStoreValues => {
return Object.fromEntries(
Object.entries(state).filter(([_, value]) => typeof value !== "function")
) as BookerStoreValues;
}, []);
- Replace the legacy subscription with a context-scoped effect:
@@
- 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]);
+ // Drive the callback from this component's scoped store values
+ const scopedStateValues = useBookerStoreContext(getStateValues);
+ useEffect(() => {
+ if (!onBookerStateChange) return;
+ debouncedStateChange(scopedStateValues, onBookerStateChange);
+ return () => {
+ debouncedStateChange.cancel();
+ };
+ }, [scopedStateValues, onBookerStateChange, debouncedStateChange]);
Add the necessary type import outside the changed hunk:
import type { BookerStore } from "@calcom/features/bookings/Booker/store";
Also remove the legacy useBookerStore import (see next comment).
🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 71 to
83, the effect still subscribes to the legacy global useBookerStore causing
cross-widget leaks; replace that subscription with a context-scoped selector
typed to the store and use a useEffect that subscribes via useBookerStoreContext
so callbacks only see this instance's state, type the selector against
BookerStore (import type { BookerStore } from
"@calcom/features/bookings/Booker/store"), and remove the legacy useBookerStore
import; ensure you update the selector signature to use the BookerStore type and
wire the new context-scoped subscription inside a useEffect with proper cleanup.
useInitializeBookerStoreContext({ | ||
...props, | ||
teamMemberEmail, | ||
crmAppSlug, | ||
crmOwnerRecordType, | ||
crmRecordId: props.crmRecordId, | ||
eventId: event?.data?.id, | ||
rescheduleUid: props.rescheduleUid ?? null, | ||
bookingUid: props.bookingUid ?? null, | ||
layout: layout, | ||
org: props.entity?.orgSlug, | ||
username, | ||
bookingData, | ||
isPlatform: true, | ||
allowUpdatingUrlParams, | ||
}); | ||
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double initialization: remove legacy useInitializeBookerStore call to avoid cross-instance coupling
You correctly initialize the context store with useInitializeBookerStoreContext here. However, the file still calls the legacy useInitializeBookerStore above (Lines 164–180), which initializes the global store and reintroduces cross-widget coupling.
- Remove the legacy initializer call block (Lines 164–180).
- Drop the legacy imports.
Suggested import cleanup outside this hunk:
-import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
+import type { BookerStore } from "@calcom/features/bookings/Booker/store";
And delete the legacy initializer block:
- useInitializeBookerStore({
- ...props,
- teamMemberEmail,
- crmAppSlug,
- crmOwnerRecordType,
- crmRecordId: props.crmRecordId,
- eventId: event?.data?.id,
- rescheduleUid: props.rescheduleUid ?? null,
- bookingUid: props.bookingUid ?? null,
- layout: layout,
- org: props.entity?.orgSlug,
- username,
- bookingData,
- isPlatform: true,
- allowUpdatingUrlParams,
- });
🤖 Prompt for AI Agents
In packages/platform/atoms/booker/BookerPlatformWrapper.tsx around lines 164 to
180 (legacy initializer) and 181 to 197 (current initializer), the legacy
useInitializeBookerStore call remains and causes global/store cross-instance
coupling; remove the entire legacy initializer block (lines 164–180) and any
associated legacy imports at the top of the file, leaving only the
useInitializeBookerStoreContext invocation shown at ~181–197, and ensure imports
no longer reference the removed legacy initializer to avoid unused imports or
lint errors.
What does this PR do?
Booker
atom wherein previously we were not able to use multiple instance of theBooker
widget on the same page. If we would use multipleBooker
widgets the state would change in both of them even though one of them would be toggledBooker
atom widget uses a store of its own instead of sharing a global instance that all the widgets can access which was the previous approachVisual Demo:
Screen.Recording.2025-08-11.at.6.14.02.PM.mov
Mandatory Tasks (DO NOT REMOVE)
How should this be tested?
For the web app these are the steps:
For platform these are the steps:
booking.tsx
in the examples app and copy the code for Booker atom here and paste it just under to make another instance of the sameBooker
widget appearBooker
atom widgets you need to either comment out the prop allowUpdatingUrlParams or set it to false