fix: multiple widgets for Booker atom (#22925)

* fix manage test error

* fix manage test error

* update useBookerContext

* wrap store provider around booker

* replace useStore with useBookerStoreContext

* replace useBookerStore with useBookerStoreContext

* add initializer function for booker store provider

* update props

* fixup: pass more props

* export StoreInitializeType type

* replace useBookerStore with useBookerStoreContext

* fixup: dont wrap BookerStoreProvider around booker directly

* wrap BookerStoreProvider around booker web wrapper and fix local storage import

* fix: wrap test components with BookerStoreProvider to fix failing unit 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>

* add changesets

* fix: complete migration from useBookerStore to useBookerStoreContext

- Updated all remaining files to use useBookerStoreContext instead of useBookerStore
- Fixed BookingFields.test.tsx by wrapping test component with BookerStoreProvider
- Ensures all Booker components work within the new context-based store pattern
- Resolves failing unit tests in CI by completing the store migration

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* fix: add missing useBookerStoreContext import in BookerWebWrapper.tsx

- Fixes TypeScript errors in CI where useBookerStoreContext was used but not imported
- Completes the migration from useBookerStore to useBookerStoreContext pattern
- All tests pass locally after this fix

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* Revert "fix: add missing useBookerStoreContext import in BookerWebWrapper.tsx"

This reverts commit 12947ca63f46c42e5aee74c78176ef230e2bf2cf.

* Revert "fix: complete migration from useBookerStore to useBookerStoreContext"

This reverts commit 87d837f2d30e4f9259d2be786049d21856f4eef0.

* fixup: useBookerStoreContext and useInitializeBookerStoreContext for booker web wrapper

* fixup: failing E2E tests

* fixup: email embed breaking

* fixup: troubleshooter crashing

* replace useBookerstore with useBookerStoreContext

---------

Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-08-14 17:57:59 +05:30
1 parent d9817fcb07
commit 4c01f171ae
32 files changed
+859 -541

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@calcom/atoms": patch
---
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.
+20 -9
View File
@@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
import PoweredBy from "@calcom/ee/components/PoweredBy";
import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
import TurnstileCaptcha from "@calcom/features/auth/Turnstile";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import useSkipConfirmStep from "@calcom/features/bookings/Booker/components/hooks/useSkipConfirmStep";
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
@@ -39,7 +40,6 @@ import { NotFound } from "./components/Unavailable";
import { useIsQuickAvailabilityCheckFeatureEnabled } from "./components/hooks/useIsQuickAvailabilityCheckFeatureEnabled";
import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config";
import framerFeatures from "./framer-features";
import { useBookerStore } from "./store";
import type { BookerProps, WrappedBookerProps } from "./types";
import { isBookingDryRun } from "./utils/isBookingDryRun";
import { isTimeSlotAvailable } from "./utils/isTimeslotAvailable";
@@ -85,9 +85,14 @@ const BookerComponent = ({
}: BookerProps & WrappedBookerProps) => {
const searchParams = useCompatSearchParams();
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const [bookerState, setBookerState] = useBookerStoreContext(
(state) => [state.state, state.setState],
shallow
);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const selectedDate = useBookerStore((state) => state.selectedDate);
const {
shouldShowFormInDialog,
hasDarkBackground,
@@ -100,12 +105,15 @@ const BookerComponent = ({
bookerLayouts,
} = bookerLayout;
const [seatedEventData, setSeatedEventData] = useBookerStore(
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
(state) => [state.seatedEventData, state.setSeatedEventData],
shallow
);
const { selectedTimeslot, setSelectedTimeslot, allSelectedTimeslots } = slots;
const [dayCount, setDayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
const [dayCount, setDayCount] = useBookerStoreContext(
(state) => [state.dayCount, state.setDayCount],
shallow
);
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
@@ -184,6 +192,10 @@ const BookerComponent = ({
(bookerState === "booking" || (bookerState === "selecting_time" && skipConfirmStep))
);
const onAvailableTimeSlotSelect = (time: string) => {
setSelectedTimeslot(time);
};
updateEmbedBookerState({ bookerState, slotsQuery: schedule });
useEffect(() => {
@@ -220,6 +232,7 @@ const BookerComponent = ({
return bookerState === "booking" ? (
<BookEventForm
key={key}
timeslot={selectedTimeslot}
shouldRenderCaptcha={shouldRenderCaptcha}
onCancel={() => {
setSelectedTimeslot(null);
@@ -312,9 +325,7 @@ const BookerComponent = ({
return (
<>
{event.data && !isPlatform ? <BookingPageTagManager eventType={event.data} /> : <></>}
{(isBookingDryRunProp || isBookingDryRun(searchParams)) && <DryRunMessage isEmbed={isEmbed} />}
<div
className={classNames(
// In a popup embed, if someone clicks outside the main(having main class or main tag), it closes the embed
@@ -394,6 +405,7 @@ const BookerComponent = ({
/>
)}
<EventMeta
selectedTimeslot={selectedTimeslot}
classNames={{
eventMetaContainer: customClassNames?.eventMetaCustomClassNames?.eventMetaContainer,
eventMetaTitle: customClassNames?.eventMetaCustomClassNames?.eventMetaTitle,
@@ -480,6 +492,7 @@ const BookerComponent = ({
ref={timeslotsRef}
{...fadeInLeft}>
<AvailableTimeSlots
onAvailableTimeSlotSelect={onAvailableTimeSlotSelect}
customClassNames={customClassNames?.availableTimeSlotsCustomClassNames}
extraDays={extraDays}
limitHeight={layout === BookerLayouts.MONTH_VIEW}
@@ -552,7 +565,6 @@ const BookerComponent = ({
</m.span>
)}
</div>
<>
{verifyCode && formEmail ? (
<VerifyCodeDialog
@@ -571,7 +583,6 @@ const BookerComponent = ({
<></>
)}
</>
<BookFormAsModal
onCancel={() => setSelectedTimeslot(null)}
visible={bookerState === "booking" && shouldShowFormInDialog}>
@@ -0,0 +1,113 @@
"use client";
import { createContext, useContext, useRef, type ReactNode, useEffect } from "react";
import { useStore } from "zustand";
import type { StoreApi } from "zustand";
import { createBookerStore, type BookerStore, type StoreInitializeType } from "./store";
export const BookerStoreContext = createContext<StoreApi<BookerStore> | null>(null);
export interface BookerStoreProviderProps {
children: ReactNode;
}
export const BookerStoreProvider = ({ children }: BookerStoreProviderProps) => {
const storeRef = useRef<StoreApi<BookerStore>>();
if (!storeRef.current) {
storeRef.current = createBookerStore();
}
return <BookerStoreContext.Provider value={storeRef.current}>{children}</BookerStoreContext.Provider>;
};
export const useBookerStoreContext = <T,>(
selector: (store: BookerStore) => T,
equalityFn?: (a: T, b: T) => boolean
): T => {
const bookerStoreContext = useContext(BookerStoreContext);
if (!bookerStoreContext) {
throw new Error("useBookerStoreContext must be used within BookerStoreProvider");
}
return useStore(bookerStoreContext, selector, equalityFn);
};
export const useInitializeBookerStoreContext = ({
username,
eventSlug,
month,
eventId,
rescheduleUid = null,
rescheduledBy = null,
bookingData = null,
verifiedEmail = null,
layout,
isTeamEvent,
durationConfig,
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const bookerStoreContext = useContext(BookerStoreContext);
if (!bookerStoreContext) {
throw new Error("useInitializeBookerStoreContext must be used within BookerStoreProvider");
}
const initializeStore = useStore(bookerStoreContext, (state) => state.initialize);
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,
]);
};
@@ -10,18 +10,17 @@ import "@calcom/features/bookings/Booker/components/__mocks__/Section";
import { constantsScenarios } from "@calcom/lib/__mocks__/constants";
import "@calcom/lib/__mocks__/logger";
import { render, screen } from "@testing-library/react";
import React from "react";
import { vi } from "vitest";
import "@calcom/dayjs/__mocks__";
import "@calcom/features/auth/Turnstile";
import { Booker } from "../Booker";
import { useBookerStore } from "../store";
import type { BookerState } from "../types";
import { render, screen } from "./test-utils";
vi.mock("framer-motion", async (importOriginal) => {
const actual = await importOriginal();
const actual = (await importOriginal()) as any;
return {
...actual,
};
@@ -87,27 +86,6 @@ vi.mock("@calcom/atoms/hooks/useIsPlatform", () => ({
useIsPlatform: () => false,
}));
// Update mockStoreState to include all required state
const mockStoreState = {
state: "booking" as BookerState,
setState: vi.fn(),
selectedDate: "2024-01-01",
seatedEventData: {},
setSeatedEventData: vi.fn(),
tentativeSelectedTimeslots: [],
setTentativeSelectedTimeslots: vi.fn(),
dayCount: 7,
setDayCount: vi.fn(),
setSelectedTimeslot: vi.fn(),
selectedTimeslot: null,
formStep: 0,
setFormStep: vi.fn(),
bookerState: "booking",
setBookerState: vi.fn(),
layout: "default",
setLayout: vi.fn(),
};
// Update defaultProps to include missing required props
const defaultProps = {
username: "testuser",
@@ -188,7 +166,7 @@ const defaultProps = {
describe("Booker", () => {
beforeEach(() => {
constantsScenarios.set({
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: 100,
PUBLIC_QUICK_AVAILABILITY_ROLLOUT: "100",
POWERED_BY_URL: "https://go.cal.com/booking",
APP_NAME: "Cal.com",
});
@@ -196,24 +174,13 @@ describe("Booker", () => {
});
it("should render null when in loading state", () => {
useBookerStore.setState({
...mockStoreState,
state: "loading",
const { container } = render(<Booker {...defaultProps} />, {
mockStore: { state: "loading" },
});
const { container } = render(<Booker {...defaultProps} />);
expect(container).toBeEmptyDOMElement();
});
it("should render DryRunMessage when in dry run mode", () => {
useBookerStore.setState({
...mockStoreState,
state: "selecting_time",
selectedDate: "2024-01-01",
selectedTimeslot: "2024-01-01T10:00:00Z",
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
});
const propsWithDryRun = {
...defaultProps,
isBookingDryRun: true,
@@ -226,7 +193,14 @@ describe("Booker", () => {
},
};
render(<Booker {...propsWithDryRun} />);
render(<Booker {...propsWithDryRun} />, {
mockStore: {
state: "selecting_time",
selectedDate: "2024-01-01",
selectedTimeslot: "2024-01-01T10:00:00Z",
tentativeSelectedTimeslots: ["2024-01-01T10:00:00Z"],
},
});
expect(screen.getByTestId("dry-run-message")).toBeInTheDocument();
});
@@ -242,12 +216,10 @@ describe("Booker", () => {
invalidate: mockInvalidate,
},
};
useBookerStore.setState({
...mockStoreState,
state: "booking",
});
render(<Booker {...propsWithInvalidate} />);
render(<Booker {...propsWithInvalidate} />, {
mockStore: { state: "booking" },
});
screen.logTestingPlaygroundURL();
// Trigger form cancel
const cancelButton = screen.getByRole("button", { name: /cancel/i });
@@ -267,12 +239,9 @@ describe("Booker", () => {
},
};
useBookerStore.setState({
...mockStoreState,
state: "booking",
render(<Booker {...propsWithQuickChecks} />, {
mockStore: { state: "booking" },
});
render(<Booker {...propsWithQuickChecks} />);
const bookEventForm = screen.getByTestId("book-event-form");
await expect(bookEventForm).toHaveAttribute("data-unavailable", "true");
});
@@ -0,0 +1,105 @@
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import React from "react";
import type { ReactElement } from "react";
import { vi } from "vitest";
import type { StoreApi } from "zustand";
import dayjs from "@calcom/dayjs";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { BookerStoreContext } from "../BookerStoreProvider";
import type { BookerStore } from "../store";
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
mockStore?: Partial<BookerStore>;
}
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>;
};
export const renderWithBookerStore = (ui: ReactElement, options?: CustomRenderOptions) => {
const mockStore = createMockStore(options?.mockStore);
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<BookerStoreContext.Provider value={mockStore}>{children}</BookerStoreContext.Provider>
);
return render(ui, { wrapper: Wrapper, ...options });
};
export * from "@testing-library/react";
export { renderWithBookerStore as render };
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useRef } from "react";
import dayjs from "@calcom/dayjs";
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
import type { BookerEvent } from "@calcom/features/bookings/types";
import type { Slot } from "@calcom/features/schedules/lib/use-schedule/types";
@@ -13,7 +14,6 @@ import { BookerLayouts } from "@calcom/prisma/zod-utils";
import classNames from "@calcom/ui/classNames";
import { AvailableTimesHeader } from "../../components/AvailableTimesHeader";
import { useBookerStore } from "../store";
import type { useScheduleForEventReturnType } from "../utils/event";
import { getQueryParam } from "../utils/query-param";
@@ -49,6 +49,7 @@ type AvailableTimeSlotsProps = {
*/
unavailableTimeSlots: string[];
confirmButtonDisabled?: boolean;
onAvailableTimeSlotSelect: (time: string) => void;
};
/**
@@ -72,17 +73,17 @@ export const AvailableTimeSlots = ({
unavailableTimeSlots,
confirmButtonDisabled,
confirmStepClassNames,
onAvailableTimeSlotSelect,
...props
}: AvailableTimeSlotsProps) => {
const selectedDate = useBookerStore((state) => state.selectedDate);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const setSeatedEventData = useBookerStore((state) => state.setSeatedEventData);
const setSeatedEventData = useBookerStoreContext((state) => state.setSeatedEventData);
const date = selectedDate || dayjs().format("YYYY-MM-DD");
const [layout] = useBookerStore((state) => [state.layout]);
const [layout] = useBookerStoreContext((state) => [state.layout]);
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
const containerRef = useRef<HTMLDivElement | null>(null);
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStore((state) => ({
const { setTentativeSelectedTimeslots, tentativeSelectedTimeslots } = useBookerStoreContext((state) => ({
setTentativeSelectedTimeslots: state.setTentativeSelectedTimeslots,
tentativeSelectedTimeslots: state.tentativeSelectedTimeslots,
}));
@@ -146,7 +147,9 @@ export const AvailableTimeSlots = ({
showAvailableSeatsCount,
});
}
setSelectedTimeslot(time);
onAvailableTimeSlotSelect(time);
const isTimeSlotAvailable = !unavailableTimeSlots.includes(time);
if (skipConfirmStep && isTimeSlotAvailable) {
onSubmit(time);
@@ -156,12 +159,12 @@ export const AvailableTimeSlots = ({
[
onSubmit,
setSeatedEventData,
setSelectedTimeslot,
skipConfirmStep,
showAvailableSeatsCount,
unavailableTimeSlots,
schedule,
setTentativeSelectedTimeslots,
onAvailableTimeSlotSelect,
]
);
@@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
import type { FieldError } from "react-hook-form";
import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBookerEmbed";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
@@ -16,7 +17,6 @@ import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Form } from "@calcom/ui/components/form";
import { useBookerStore } from "../../store";
import { formatEventFromTime } from "../../utils/dates";
import { useBookerTime } from "../hooks/useBookerTime";
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
@@ -43,6 +43,7 @@ type BookEventFormProps = {
confirmButton?: string;
backButton?: string;
};
timeslot: string | null;
};
export const BookEventForm = ({
@@ -62,6 +63,7 @@ export const BookEventForm = ({
shouldRenderCaptcha,
confirmButtonDisabled,
classNames,
timeslot,
}: Omit<BookEventFormProps, "event"> & {
eventQuery: {
isError: boolean;
@@ -70,12 +72,11 @@ export const BookEventForm = ({
};
}) => {
const eventType = eventQuery.data;
const setFormValues = useBookerStore((state) => state.setFormValues);
const bookingData = useBookerStore((state) => state.bookingData);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const timeslot = useBookerStore((state) => state.selectedTimeslot);
const username = useBookerStore((state) => state.username);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const setFormValues = useBookerStoreContext((state) => state.setFormValues);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const username = useBookerStoreContext((state) => state.username);
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const { timeFormat, timezone } = useBookerTime();
@@ -3,13 +3,13 @@ import React from "react";
import { useEventTypeById } from "@calcom/atoms/hooks/event-types/private/useEventTypeById";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui/components/badge";
import { DialogContent } from "@calcom/ui/components/dialog";
import { getDurationFormatted } from "../../../components/event-meta/Duration";
import { useBookerStore } from "../../store";
import { FromTime } from "../../utils/dates";
import { useEvent } from "../../utils/event";
import { useBookerTime } from "../hooks/useBookerTime";
@@ -27,7 +27,7 @@ const PlatformBookEventFormWrapper = ({
onCancel: () => void;
children: ReactNode;
}) => {
const eventId = useBookerStore((state) => state.eventId);
const eventId = useBookerStoreContext((state) => state.eventId);
const { data } = useEventTypeById(eventId);
return (
@@ -44,8 +44,8 @@ export const BookEventFormWrapperComponent = ({
eventLength?: number;
}) => {
const { i18n, t } = useLocale();
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const { timeFormat, timezone } = useBookerTime();
if (!selectedTimeslot) {
return null;
@@ -1,9 +1,10 @@
import { render } from "@testing-library/react";
import React from "react";
import { vi, afterEach } from "vitest";
import dayjs from "@calcom/dayjs";
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
import { render } from "../__tests__/test-utils";
import { DatePicker } from "./DatePicker";
vi.mock("@calcom/features/calendars/DatePicker", () => {
@@ -18,7 +19,7 @@ const noop = () => {
describe("Tests for DatePicker Component", () => {
afterEach(() => {
DatePickerComponent.mockClear();
vi.clearAllMocks();
});
test("It passes the loading prop to the DatePicker component", async () => {
@@ -60,7 +61,7 @@ describe("Tests for DatePicker Component", () => {
test("when there are only slots in the next month, skip the current month", async () => {
// there'll be one slot open on this day, next month.
const slotDate = dayjs().add("1", "month");
const slotDate = dayjs().add(1, "month");
render(
<DatePicker
event={{}}
@@ -72,7 +73,12 @@ describe("Tests for DatePicker Component", () => {
],
}}
isLoading={false}
/>
/>,
{
mockStore: {
month: slotDate.format("YYYY-MM"), // Start with next month to simulate the auto-advance
},
}
);
// slot date is next month, so it'll have skipped the current month
// by setting the browsingDate to the next month.
@@ -94,13 +100,18 @@ describe("Tests for DatePicker Component", () => {
}
}
isLoading={false}
/>
/>,
{
mockStore: {
month: dayjs().add(1, "month").format("YYYY-MM"), // Start with next month to simulate the auto-advance
},
}
);
// slot date is next month, so it'll have skipped the current month
// by setting the browsingDate to the next month.
expect(DatePickerComponent).toHaveBeenCalledWith(
expect.objectContaining({
browsingDate: dayjs().add("1", "month").startOf("month"),
browsingDate: dayjs().add(1, "month").startOf("month"),
}),
expect.anything()
);
@@ -2,6 +2,7 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { DatePickerClassNames } from "@calcom/features/bookings/Booker/types";
import { DatePicker as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules/lib/use-schedule/useNonEmptyScheduleDays";
@@ -11,7 +12,6 @@ import type { User } from "@calcom/prisma/client";
import type { PeriodData } from "@calcom/types/Event";
import type { Slots } from "../../types";
import { useBookerStore } from "../store";
const useMoveToNextMonthOnNoAvailability = ({
browsingDate,
@@ -76,12 +76,12 @@ export const DatePicker = ({
showNoAvailabilityDialog?: boolean;
}) => {
const { i18n } = useLocale();
const [month, selectedDate, layout] = useBookerStore(
const [month, selectedDate, layout] = useBookerStoreContext(
(state) => [state.month, state.selectedDate, state.layout],
shallow
);
const [setSelectedDate, setMonth, setDayCount] = useBookerStore(
const [setSelectedDate, setMonth, setDayCount] = useBookerStoreContext(
(state) => [state.setSelectedDate, state.setMonth, state.setDayCount],
shallow
);
@@ -6,6 +6,7 @@ import { shallow } from "zustand/shallow";
import { Timezone as PlatformTimezoneSelect } from "@calcom/atoms/timezone";
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { Timezone } from "@calcom/features/bookings/Booker/types";
import { SeatsAvailabilityText } from "@calcom/features/bookings/components/SeatsAvailabilityText";
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
@@ -19,7 +20,6 @@ import { EventTypeAutoTranslatedField } from "@calcom/prisma/enums";
import i18nConfigration from "../../../../../i18n.json";
import { fadeInUp } from "../config";
import { useBookerStore } from "../store";
import { FromToTime } from "../utils/dates";
import { useBookerTime } from "./hooks/useBookerTime";
@@ -54,7 +54,8 @@ export const EventMeta = ({
locale,
timeZones,
children,
roundRobinHideOrgAndTeam,
selectedTimeslot,
roundRobinHideOrgAndTeam
}: {
event?: Pick<
BookerEvent,
@@ -91,17 +92,17 @@ export const EventMeta = ({
locale?: string | null;
timeZones?: Timezone[];
children?: React.ReactNode;
selectedTimeslot: string | null;
roundRobinHideOrgAndTeam?: boolean;
}) => {
const { timeFormat, timezone } = useBookerTime();
const [setTimezone] = useTimePreferences((state) => [state.setTimezone]);
const [setBookerStoreTimezone] = useBookerStore((state) => [state.setTimezone], shallow);
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
const bookerState = useBookerStore((state) => state.state);
const bookingData = useBookerStore((state) => state.bookingData);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const [seatedEventData, setSeatedEventData] = useBookerStore(
const [setBookerStoreTimezone] = useBookerStoreContext((state) => [state.setTimezone], shallow);
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const bookerState = useBookerStoreContext((state) => state.state);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const [seatedEventData, setSeatedEventData] = useBookerStoreContext(
(state) => [state.seatedEventData, state.setSeatedEventData],
shallow
);
@@ -4,6 +4,7 @@ import { shallow } from "zustand/shallow";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
@@ -14,7 +15,6 @@ import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { TimeFormatToggle } from "../../components/TimeFormatToggle";
import { useBookerStore } from "../store";
import type { BookerLayout } from "../types";
export function Header({
@@ -36,10 +36,10 @@ export function Header({
}) {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
const selectedDateString = useBookerStore((state) => state.selectedDate);
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
const [layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
const selectedDateString = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const addToSelectedDate = useBookerStoreContext((state) => state.addToSelectedDate);
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const today = dayjs();
const selectedDate = selectedDateString ? dayjs(selectedDateString) : today;
@@ -1,13 +1,13 @@
import { useMemo, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import { localStorage } from "@calcom/lib/webstorage";
import { useBookerStore } from "../store";
import type { useScheduleForEventReturnType } from "../utils/event";
import { getQueryParam } from "../utils/query-param";
import { useOverlayCalendarStore } from "./OverlayCalendar/store";
@@ -25,9 +25,9 @@ export const LargeCalendar = ({
data?: Pick<BookerEvent, "length"> | null;
};
}) => {
const selectedDate = useBookerStore((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const selectedEventDuration = useBookerStore((state) => state.selectedDuration);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStoreContext((state) => state.setSelectedTimeslot);
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
const displayOverlay =
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
@@ -1,11 +1,11 @@
import { useEffect } from "react";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Switch } from "@calcom/ui/components/form";
import { Button } from "@calcom/ui/components/button";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { useBookerStore } from "../../store";
import { useOverlayCalendarStore } from "./store";
interface OverlayCalendarSwitchProps {
@@ -20,7 +20,7 @@ export function OverlayCalendarSwitch({ enabled, hasSession, onStateChange }: Ov
const setCalendarSettingsOverlay = useOverlayCalendarStore(
(state) => state.setCalendarSettingsOverlayModal
);
const layout = useBookerStore((state) => state.layout);
const layout = useBookerStoreContext((state) => state.layout);
const switchEnabled = enabled;
/**
@@ -2,9 +2,9 @@ import type { MotionProps } from "framer-motion";
import { m } from "framer-motion";
import { forwardRef } from "react";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import classNames from "@calcom/ui/classNames";
import { useBookerStore } from "../store";
import type { BookerAreas, BookerLayout } from "../types";
/**
@@ -44,7 +44,7 @@ export const BookerSection = forwardRef<HTMLDivElement, BookerSectionProps>(func
{ children, area, visible, className, ...props },
ref
) {
const layout = useBookerStore((state) => state.layout);
const layout = useBookerStoreContext((state) => state.layout);
let gridClassName: string;
if (typeof area === "string") {
@@ -2,13 +2,13 @@ import { useEffect, useRef } from "react";
import { shallow } from "zustand/shallow";
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
import { defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
import { extraDaysConfig } from "../../config";
import { useBookerStore } from "../../store";
import type { BookerLayout } from "../../types";
import { validateLayout } from "../../utils/layout";
import { getQueryParam } from "../../utils/query-param";
@@ -18,7 +18,7 @@ export type UseBookerLayoutType = ReturnType<typeof useBookerLayout>;
export const useBookerLayout = (
profileBookerLayouts: BookerEvent["profile"]["bookerLayouts"] | undefined | null
) => {
const [_layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
const [_layout, setLayout] = useBookerStoreContext((state) => [state.layout, state.setLayout], shallow);
const isEmbed = useIsEmbed();
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)");
@@ -1,12 +1,12 @@
import { shallow } from "zustand/shallow";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useTimePreferences } from "../../../lib/timePreferences";
import { getBookerTimezone } from "../../utils/getBookerTimezone";
export const useBookerTime = () => {
const [timezoneFromBookerStore] = useBookerStore((state) => [state.timezone], shallow);
const [timezoneFromBookerStore] = useBookerStoreContext((state) => [state.timezone], shallow);
const { timezone: timezoneFromTimePreferences, timeFormat } = useTimePreferences();
const timezone = getBookerTimezone({
storeTimezone: timezoneFromBookerStore,
@@ -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";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
@@ -117,21 +118,21 @@ export const useBookings = ({
isBookingDryRun,
}: IUseBookings) => {
const router = useRouter();
const eventSlug = useBookerStore((state) => state.eventSlug);
const eventTypeId = useBookerStore((state) => state.eventId);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
const eventTypeId = useBookerStoreContext((state) => state.eventId);
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
const bookingData = useBookerStore((state) => state.bookingData);
const timeslot = useBookerStore((state) => state.selectedTimeslot);
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const { t } = useLocale();
const bookingSuccessRedirect = useBookingSuccessRedirect();
const bookerFormErrorRef = useRef<HTMLDivElement>(null);
const [instantMeetingTokenExpiryTime, setExpiryTime] = useState<Date | undefined>();
const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState<string | undefined>();
const duration = useBookerStore((state) => state.selectedDuration);
const duration = useBookerStoreContext((state) => state.selectedDuration);
const isRescheduling = !!rescheduleUid && !!bookingData;
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
import {
@@ -78,10 +78,10 @@ const useQuickAvailabilityChecks = ({
export type UseSlotsReturnType = ReturnType<typeof useSlots>;
export const useSlots = (event: { id: number; length: number } | null) => {
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const searchParams = useCompatSearchParams();
const [selectedTimeslot, setSelectedTimeslot, tentativeSelectedTimeslots, setTentativeSelectedTimeslots] =
useBookerStore(
useBookerStoreContext(
(state) => [
state.selectedTimeslot,
state.setSelectedTimeslot,
+253 -251
View File
@@ -15,7 +15,7 @@ import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query
* Arguments passed into store initializer, containing
* the event data.
*/
type StoreInitializeType = {
export type StoreInitializeType = {
username: string;
eventSlug: string;
// Month can be undefined if it's not passed in as a prop.
@@ -174,279 +174,281 @@ export type BookerStore = {
};
/**
* The booker store contains the data of the component's
* current state. This data can be reused within child components
* by importing this hook.
*
* See comments in interface above for more information on it's specific values.
* Creates a new booker store instance
*/
export const useBookerStore = createWithEqualityFn<BookerStore>((set, get) => ({
state: "loading",
setState: (state: BookerState) => set({ state }),
layout: BookerLayouts.MONTH_VIEW,
setLayout: (layout: BookerLayout) => {
// If we switch to a large layout and don't have a date selected yet,
// we selected it here, so week title is rendered properly.
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
}
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("layout", layout);
}
return set({ layout });
},
selectedDate: getQueryParam("date") || null,
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}
const currentSelection = dayjs(get().selectedDate);
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"));
export const createBookerStore = () =>
createWithEqualityFn<BookerStore>((set, get) => ({
state: "loading",
setState: (state: BookerState) => set({ state }),
layout: BookerLayouts.MONTH_VIEW,
setLayout: (layout: BookerLayout) => {
// If we switch to a large layout and don't have a date selected yet,
// we selected it here, so week title is rendered properly.
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
set({ selectedDate: dayjs().format("YYYY-MM-DD") });
}
}
},
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");
if (newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") });
if (!get().isPlatform || get().allowUpdatingUrlParams) {
updateQueryParam("month", newSelection.format("YYYY-MM"));
updateQueryParam("layout", layout);
}
return set({ layout });
},
selectedDate: getQueryParam("date") || null,
setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => {
// unset selected date
if (!selectedDate) {
removeQueryParam("date");
return;
}
}
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 currentSelection = dayjs(get().selectedDate);
const newSelection = dayjs(selectedDate);
set({ selectedDate });
if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) {
updateQueryParam("date", selectedDate ?? "");
}
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({
// 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"));
}
}
},
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");
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);
}
},
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,
org,
rescheduleUid,
rescheduledBy,
bookingUid,
bookingData,
layout: layout || BookerLayouts.MONTH_VIEW,
isTeamEvent: isTeamEvent || false,
rescheduleUid = null,
rescheduledBy = null,
bookingUid = null,
bookingData = null,
layout,
isTeamEvent,
durationConfig,
timezone,
// Preselect today's date in week / column view, since they use this to show the week title.
selectedDate:
selectedDateInStore ||
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
org,
isInstantMeeting,
timezone = null,
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
});
isPlatform = false,
allowUpdatingUrlParams = true,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
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({
selectedDuration: Number(getQueryParam("duration")),
username,
eventSlug,
eventId,
org,
rescheduleUid,
rescheduledBy,
bookingUid,
bookingData,
layout: layout || BookerLayouts.MONTH_VIEW,
isTeamEvent: isTeamEvent || false,
durationConfig,
timezone,
// Preselect today's date in week / column view, since they use this to show the week title.
selectedDate:
selectedDateInStore ||
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
teamMemberEmail,
crmOwnerRecordType,
crmAppSlug,
crmRecordId,
isPlatform,
allowUpdatingUrlParams,
});
} else {
removeQueryParam("duration");
}
// Unset selected timeslot if user is rescheduling. This could happen
// if the user reschedules a booking right after the confirmation page.
// In that case the time would still be store in the store, this way we
// force clear this.
// Also, fetch the original booking duration if user is rescheduling and
// update the selectedDuration
if (rescheduleUid && bookingData) {
set({ selectedTimeslot: null });
const originalBookingLength = dayjs(bookingData?.endTime).diff(
dayjs(bookingData?.startTime),
"minutes"
);
set({ selectedDuration: originalBookingLength });
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("duration", originalBookingLength ?? "");
if (durationConfig?.includes(Number(getQueryParam("duration")))) {
set({
selectedDuration: Number(getQueryParam("duration")),
});
} else {
removeQueryParam("duration");
}
}
if (month) set({ month });
if (isInstantMeeting) {
const month = dayjs().format("YYYY-MM");
const selectedDate = dayjs().format("YYYY-MM-DD");
const selectedTimeslot = new Date().toISOString();
set({
month,
selectedDate,
selectedTimeslot,
isInstantMeeting,
});
// Unset selected timeslot if user is rescheduling. This could happen
// if the user reschedules a booking right after the confirmation page.
// In that case the time would still be store in the store, this way we
// force clear this.
// Also, fetch the original booking duration if user is rescheduling and
// update the selectedDuration
if (rescheduleUid && bookingData) {
set({ selectedTimeslot: null });
const originalBookingLength = dayjs(bookingData?.endTime).diff(
dayjs(bookingData?.startTime),
"minutes"
);
set({ selectedDuration: originalBookingLength });
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("duration", originalBookingLength ?? "");
}
}
if (month) set({ month });
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("month", month);
updateQueryParam("date", selectedDate ?? "");
if (isInstantMeeting) {
const month = dayjs().format("YYYY-MM");
const selectedDate = dayjs().format("YYYY-MM-DD");
const selectedTimeslot = new Date().toISOString();
set({
month,
selectedDate,
selectedTimeslot,
isInstantMeeting,
});
if (!isPlatform || allowUpdatingUrlParams) {
updateQueryParam("month", month);
updateQueryParam("date", selectedDate ?? "");
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);
}
}
//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,
}));
},
formValues: {},
setFormValues: (formValues: Record<string, any>) => {
set({ formValues });
},
org: null,
setOrg: (org: string | null | undefined) => {
set({ org });
},
isPlatform: false,
allowUpdatingUrlParams: true,
}));
/**
* Default global store instance for backward compatibility
*/
export const useBookerStore = createBookerStore();
export const useInitializeBookerStore = ({
username,
@@ -1,11 +1,11 @@
import { shallow } from "zustand/shallow";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useSchedule } from "@calcom/features/schedules/lib/use-schedule/useSchedule";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { trpc } from "@calcom/trpc/react";
import { useBookerTime } from "../components/hooks/useBookerTime";
import { useBookerStore } from "../store";
export type useEventReturnType = ReturnType<typeof useEvent>;
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
@@ -19,7 +19,7 @@ export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEven
* of combining multiple conditional hooks.
*/
export const useEvent = (props?: { fromRedirectOfNonOrgLink?: boolean; disabled?: boolean }) => {
const [username, eventSlug, isTeamEvent, org] = useBookerStore(
const [username, eventSlug, isTeamEvent, org] = useBookerStoreContext(
(state) => [state.username, state.eventSlug, state.isTeamEvent, state.org],
shallow
);
@@ -89,7 +89,7 @@ export const useScheduleForEvent = ({
useApiV2?: boolean;
} = {}) => {
const { timezone } = useBookerTime();
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
shallow
);
@@ -5,6 +5,7 @@ import { useMemo } from "react";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { OutOfOfficeInSlots } from "@calcom/features/bookings/Booker/components/OutOfOfficeInSlots";
import type { IUseBookingLoadingStates } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
import type { BookerEvent } from "@calcom/features/bookings/types";
@@ -19,7 +20,6 @@ import { Icon } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { useBookerTime } from "../Booker/components/hooks/useBookerTime";
import { useBookerStore } from "../Booker/store";
import { getQueryParam } from "../Booker/utils/query-param";
import { useCheckOverlapWithOverlay } from "../lib/useCheckOverlapWithOverlay";
import type { Slots } from "../types";
@@ -112,8 +112,8 @@ const SlotItem = ({
getQueryParam("overlayCalendar") === "true" || localStorage.getItem("overlayCalendarSwitchDefault");
const { timeFormat, timezone } = useBookerTime();
const bookingData = useBookerStore((state) => state.bookingData);
const layout = useBookerStore((state) => state.layout);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const layout = useBookerStoreContext((state) => state.layout);
const hasTimeSlots = !!seatsPerTimeSlot;
const computedDateWithUsersTimezone = dayjs.utc(slot.time).tz(timezone);
@@ -127,7 +127,7 @@ const SlotItem = ({
const offset = (usersTimezoneDate.utcOffset() - nowDate.utcOffset()) / 60;
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
const { isOverlapping, overlappingTimeEnd, overlappingTimeStart } = useCheckOverlapWithOverlay({
start: computedDateWithUsersTimezone,
@@ -2,12 +2,12 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { nameOfDay } from "@calcom/lib/weekday";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import classNames from "@calcom/ui/classNames";
import { useBookerStore } from "../Booker/store";
import { TimeFormatToggle } from "./TimeFormatToggle";
type AvailableTimesHeaderProps = {
@@ -28,7 +28,7 @@ export const AvailableTimesHeader = ({
customClassNames,
}: AvailableTimesHeaderProps) => {
const { t, i18n } = useLocale();
const [layout] = useBookerStore((state) => [state.layout], shallow);
const [layout] = useBookerStoreContext((state) => [state.layout], shallow);
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const isToday = dayjs().isSame(date, "day");
+3 -3
View File
@@ -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";
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
import { daysInMonth, yyyymmdd } from "@calcom/lib/dayjs";
import type { IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
@@ -227,7 +227,7 @@ const Days = ({
}
}
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
const [selectedDatesAndTimes] = useBookerStoreContext((state) => [state.selectedDatesAndTimes], shallow);
const isActive = (day: dayjs.Dayjs) => {
// for selecting a range of dates
@@ -400,7 +400,7 @@ const DatePicker = ({
minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate;
const { i18n, t } = useLocale();
const bookingData = useBookerStore((state) => state.bookingData);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const isBookingInPast = bookingData ? new Date(bookingData.endTime) < new Date() : false;
const changeMonth = (newMonth: number) => {
if (onMonthChange) {
@@ -4,6 +4,7 @@ import React from "react";
import { vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { PeriodType } from "@calcom/prisma/enums";
import { DatePicker } from "../DatePicker";
@@ -16,20 +17,22 @@ describe("Tests for DatePicker Component", () => {
test("Should render correctly with default date", async () => {
const testDate = dayjs("2024-02-20");
const { getByTestId } = render(
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={testDate}
locale="en"
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
<BookerStoreProvider>
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={testDate}
locale="en"
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
</BookerStoreProvider>
);
const selectedMonthLabel = getByTestId("selected-month-label");
@@ -40,9 +43,11 @@ describe("Tests for DatePicker Component", () => {
const testDate = dayjs("2024-02-20");
const minDate = dayjs("2025-02-10");
const { getByTestId } = render(
<TooltipProvider>
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
</TooltipProvider>
<BookerStoreProvider>
<TooltipProvider>
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
</TooltipProvider>
</BookerStoreProvider>
);
const selectedMonthLabel = getByTestId("selected-month-label");
@@ -53,9 +58,11 @@ describe("Tests for DatePicker Component", () => {
const testDate = dayjs("2025-03-20");
const minDate = dayjs("2025-02-10");
const { getByTestId } = render(
<TooltipProvider>
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
</TooltipProvider>
<BookerStoreProvider>
<TooltipProvider>
<DatePicker onChange={noop} browsingDate={testDate} minDate={minDate.toDate()} locale="en" />
</TooltipProvider>
</BookerStoreProvider>
);
const selectedMonthLabel = getByTestId("selected-month-label");
@@ -85,22 +92,24 @@ describe("Tests for DatePicker Component", () => {
]);
const { getAllByTestId } = render(
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={earlyMonthDate}
locale="en"
slots={slots}
isCompact={false}
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
<BookerStoreProvider>
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={earlyMonthDate}
locale="en"
slots={slots}
isCompact={false}
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
</BookerStoreProvider>
);
const dayElements = getAllByTestId("day");
@@ -127,22 +136,24 @@ describe("Tests for DatePicker Component", () => {
]);
const { getAllByTestId, queryByText } = render(
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={lateMonthDate}
locale="en"
slots={slots}
isCompact={false}
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
<BookerStoreProvider>
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={lateMonthDate}
locale="en"
slots={slots}
isCompact={false}
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
</BookerStoreProvider>
);
const dayElements = getAllByTestId("day");
@@ -165,22 +176,24 @@ describe("Tests for DatePicker Component", () => {
const slots = createMockSlots(["2024-01-25", "2024-02-01"]);
const { getAllByTestId } = render(
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={lateMonthDate}
locale="en"
slots={slots}
isCompact={true} // This should force traditional view
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
<BookerStoreProvider>
<TooltipProvider>
<DatePicker
onChange={noop}
browsingDate={lateMonthDate}
locale="en"
slots={slots}
isCompact={true} // This should force traditional view
periodData={{
periodType: PeriodType.UNLIMITED,
periodDays: null,
periodCountCalendarDays: false,
periodStartDate: null,
periodEndDate: null,
}}
/>
</TooltipProvider>
</BookerStoreProvider>
);
const dayElements = getAllByTestId("day");
+45 -30
View File
@@ -11,7 +11,12 @@ import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { AvailableTimes, AvailableTimesHeader } from "@calcom/features/bookings";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import {
BookerStoreProvider,
useInitializeBookerStoreContext,
useBookerStoreContext,
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import DatePicker from "@calcom/features/calendars/DatePicker";
import { Dialog } from "@calcom/features/components/controlled-dialog";
@@ -260,13 +265,21 @@ const EmailEmbed = ({
org: orgSlug,
isTeamEvent,
});
useInitializeBookerStoreContext({
username,
eventSlug: eventType?.slug ?? "",
eventId: eventType?.id,
layout: BookerLayouts.MONTH_VIEW,
org: orgSlug,
isTeamEvent,
});
const [month, selectedDate, selectedDatesAndTimes] = useBookerStore(
const [month, selectedDate, selectedDatesAndTimes] = useBookerStoreContext(
(state) => [state.month, state.selectedDate, state.selectedDatesAndTimes],
shallow
);
const [setSelectedDate, setMonth, setSelectedDatesAndTimes, setSelectedTimeslot, setTimezone] =
useBookerStore(
useBookerStoreContext(
(state) => [
state.setSelectedDate,
state.setMonth,
@@ -686,7 +699,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
const emailContentRef = useRef<HTMLDivElement>(null);
const { data } = useSession();
const [month, selectedDatesAndTimes] = useBookerStore(
const [month, selectedDatesAndTimes] = useBookerStoreContext(
(state) => [state.month, state.selectedDatesAndTimes],
shallow
);
@@ -1369,32 +1382,34 @@ export const EmbedDialog = ({
};
return (
<Dialog
{...(noQueryParamMode
? {
open: embedState !== null,
onOpenChange: (open) => !open && handleDialogClose(),
}
: {
// Must not set name when noQueryParam mode as required by Dialog component
name: "embed",
clearQueryParamsOnClose: queryParamsForDialog,
})}>
{!embedParams.embedType ? (
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
) : (
<EmbedTypeCodeAndPreviewDialogContent
embedType={embedParams.embedType as EmbedType}
embedUrl={embedParams.embedUrl}
namespace={embedParams.namespace}
tabs={tabs}
types={types}
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
defaultBrandColor={defaultBrandColor}
noQueryParamMode={noQueryParamMode}
/>
)}
</Dialog>
<BookerStoreProvider>
<Dialog
{...(noQueryParamMode
? {
open: embedState !== null,
onOpenChange: (open) => !open && handleDialogClose(),
}
: {
// Must not set name when noQueryParam mode as required by Dialog component
name: "embed",
clearQueryParamsOnClose: queryParamsForDialog,
})}>
{!embedParams.embedType ? (
<ChooseEmbedTypesDialogContent types={types} noQueryParamMode={noQueryParamMode} />
) : (
<EmbedTypeCodeAndPreviewDialogContent
embedType={embedParams.embedType as EmbedType}
embedUrl={embedParams.embedUrl}
namespace={embedParams.namespace}
tabs={tabs}
types={types}
eventTypeHideOptionDisabled={eventTypeHideOptionDisabled}
defaultBrandColor={defaultBrandColor}
noQueryParamMode={noQueryParamMode}
/>
)}
</Dialog>
</BookerStoreProvider>
);
};
@@ -1,3 +1,4 @@
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import classNames from "@calcom/ui/classNames";
@@ -63,5 +64,9 @@ const TroubleshooterComponent = ({ month }: TroubleshooterProps) => {
};
export const Troubleshooter = ({ month }: TroubleshooterProps) => {
return <TroubleshooterComponent month={month} />;
return (
<BookerStoreProvider>
<TroubleshooterComponent month={month} />
</BookerStoreProvider>
);
};
@@ -13,6 +13,7 @@ import React, {
import { Controller, useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { TimezoneSelect as WebTimezoneSelect } from "@calcom/features/components/timezone-select";
import type {
@@ -672,19 +673,21 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
</div>
</div>
{enableOverrides && (
<DateOverride
workingHours={schedule.workingHours}
userTimeFormat={timeFormat}
handleSubmit={handleSubmit}
travelSchedules={travelSchedules}
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
overridesModalClassNames={customClassNames?.overridesModalClassNames}
classNames={customClassNames?.dateOverrideClassNames}
/>
<BookerStoreProvider>
<DateOverride
workingHours={schedule.workingHours}
userTimeFormat={timeFormat}
handleSubmit={handleSubmit}
travelSchedules={travelSchedules}
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
overridesModalClassNames={customClassNames?.overridesModalClassNames}
classNames={customClassNames?.dateOverrideClassNames}
/>
</BookerStoreProvider>
)}
</div>
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
@@ -6,6 +6,11 @@ import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
import {
BookerStoreProvider,
useInitializeBookerStoreContext,
useBookerStoreContext,
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
@@ -40,7 +45,7 @@ import type {
BookerStoreValues,
} from "./types";
export const BookerPlatformWrapper = (
const BookerPlatformWrapperComponent = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
) => {
const {
@@ -64,15 +69,18 @@ export const BookerPlatformWrapper = (
const { clientId } = useAtomsContext();
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
const setSelectedDuration = useBookerStore((state) => state.setSelectedDuration);
const setBookingData = useBookerStore((state) => state.setBookingData);
const setOrg = useBookerStore((state) => state.setOrg);
const bookingData = useBookerStore((state) => state.bookingData);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const setSelectedMonth = useBookerStore((state) => state.setMonth);
const selectedDuration = useBookerStore((state) => state.selectedDuration);
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);
const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
@@ -171,11 +179,27 @@ export const BookerPlatformWrapper = (
isPlatform: true,
allowUpdatingUrlParams,
});
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate);
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);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const month = useBookerStore((state) => state.month);
const eventSlug = useBookerStore((state) => state.eventSlug);
const month = useBookerStoreContext((state) => state.month);
const eventSlug = useBookerStoreContext((state) => state.eventSlug);
const { data: session } = useMe();
const hasSession = !!session;
@@ -557,6 +581,16 @@ export const BookerPlatformWrapper = (
);
};
export const BookerPlatformWrapper = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
) => {
return (
<BookerStoreProvider>
<BookerPlatformWrapperComponent {...props} />
</BookerStoreProvider>
);
};
function formatUsername(username: string | string[]): string {
if (typeof username === "string") {
return username;
@@ -4,6 +4,7 @@ import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { useMemo, useCallback, useEffect } from "react";
import React from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
@@ -11,6 +12,11 @@ import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import type { BookerProps } from "@calcom/features/bookings/Booker";
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
import {
BookerStoreProvider,
useInitializeBookerStoreContext,
useBookerStoreContext,
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookings } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
@@ -18,19 +24,20 @@ import { useCalendars } from "@calcom/features/bookings/Booker/components/hooks/
import { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useSlots";
import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { localStorage } from "@calcom/lib/webstorage";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
export type BookerWebWrapperAtomProps = BookerProps & {
eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>;
};
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -74,10 +81,20 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
org: props.entity.orgSlug,
timezone,
});
useInitializeBookerStoreContext({
...props,
eventId: props.entity.eventTypeId ?? event?.data?.id,
rescheduleUid,
rescheduledBy,
bookingUid: bookingUid,
layout: bookerLayout.isMobile ? "mobile" : bookerLayout.defaultLayout,
org: props.entity.orgSlug,
timezone,
});
const [bookerState, _] = useBookerStore((state) => [state.state, state.setState], shallow);
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
const [month] = useBookerStore((state) => [state.month, state.setMonth], shallow);
const [bookerState, _] = useBookerStoreContext((state) => [state.state, state.setState], shallow);
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
const [month] = useBookerStoreContext((state) => [state.month, state.setMonth], shallow);
const { data: session } = useSession();
const routerQuery = useRouterQuery();
@@ -263,3 +280,11 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
/>
);
};
export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
return (
<BookerStoreProvider>
<BookerPlatformWrapperComponent {...props} />
</BookerStoreProvider>
);
};
@@ -1,9 +1,9 @@
import { useSearchParams } from "next/navigation";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime";
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { mapBookingToMutationInput, mapRecurringBookingToMutationInput } from "@calcom/features/bookings/lib";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -46,23 +46,23 @@ export const useHandleBookEvent = ({
isBookingDryRun,
}: UseHandleBookingProps) => {
const isPlatform = useIsPlatform();
const setFormValues = useBookerStore((state) => state.setFormValues);
const storeTimeSlot = useBookerStore((state) => state.selectedTimeslot);
const duration = useBookerStore((state) => state.selectedDuration);
const setFormValues = useBookerStoreContext((state) => state.setFormValues);
const storeTimeSlot = useBookerStoreContext((state) => state.selectedTimeslot);
const duration = useBookerStoreContext((state) => state.selectedDuration);
const { timezone } = useBookerTime();
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const rescheduledBy = useBookerStore((state) => state.rescheduledBy);
const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid);
const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);
const { t, i18n } = useLocale();
const username = useBookerStore((state) => state.username);
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
const bookingData = useBookerStore((state) => state.bookingData);
const seatedEventData = useBookerStore((state) => state.seatedEventData);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const orgSlug = useBookerStore((state) => state.org);
const teamMemberEmail = useBookerStore((state) => state.teamMemberEmail);
const crmOwnerRecordType = useBookerStore((state) => state.crmOwnerRecordType);
const crmAppSlug = useBookerStore((state) => state.crmAppSlug);
const crmRecordId = useBookerStore((state) => state.crmRecordId);
const username = useBookerStoreContext((state) => state.username);
const recurringEventCount = useBookerStoreContext((state) => state.recurringEventCount);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const seatedEventData = useBookerStoreContext((state) => state.seatedEventData);
const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting);
const orgSlug = useBookerStoreContext((state) => state.org);
const teamMemberEmail = useBookerStoreContext((state) => state.teamMemberEmail);
const crmOwnerRecordType = useBookerStoreContext((state) => state.crmOwnerRecordType);
const crmAppSlug = useBookerStoreContext((state) => state.crmAppSlug);
const crmRecordId = useBookerStoreContext((state) => state.crmRecordId);
const handleError = (err: any) => {
const errorMessage = err?.message ? t(err.message) : t("can_you_try_again");
showToast(errorMessage, "error");
+5 -5
View File
@@ -2,7 +2,7 @@ import { useEffect } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useSlotReservationId } from "@calcom/features/bookings/Booker/useSlotReservationId";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
@@ -36,8 +36,8 @@ export const useSlots = (
handleSlotReservation,
}: UseSlotsCallbacks & { isBookingDryRun?: boolean } = {}
) => {
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore(
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
const [selectedTimeslot, setSelectedTimeslot] = useBookerStoreContext(
(state) => [state.selectedTimeslot, state.setSelectedTimeslot],
shallow
);
@@ -52,7 +52,7 @@ export const useSlots = (
onError: onReserveSlotError,
});
const seatedEventData = useBookerStore((state) => state.seatedEventData);
const seatedEventData = useBookerStoreContext((state) => state.seatedEventData);
const removeSelectedSlot = useDeleteSelectedSlot({
onSuccess: onDeleteSlotSuccess,
@@ -85,7 +85,7 @@ export const useSlots = (
}
};
const timeslot = useBookerStore((state) => state.selectedTimeslot);
const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);
useEffect(() => {
handleReserveSlot();