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:
32 files changed
+859
-541
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user