diff --git a/.changeset/fresh-rivers-glow.md b/.changeset/fresh-rivers-glow.md new file mode 100644 index 00000000000000..e7642f9315a30a --- /dev/null +++ b/.changeset/fresh-rivers-glow.md @@ -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. + diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index d7dd3671577d51..a1d71a2c8fe504 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -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" ? ( { setSelectedTimeslot(null); @@ -312,9 +325,7 @@ const BookerComponent = ({ return ( <> {event.data && !isPlatform ? : <>} - {(isBookingDryRunProp || isBookingDryRun(searchParams)) && } -
)} )}
- <> {verifyCode && formEmail ? ( )} - setSelectedTimeslot(null)} visible={bookerState === "booking" && shouldShowFormInDialog}> diff --git a/packages/features/bookings/Booker/BookerStoreProvider.tsx b/packages/features/bookings/Booker/BookerStoreProvider.tsx new file mode 100644 index 00000000000000..b99d119d96f79b --- /dev/null +++ b/packages/features/bookings/Booker/BookerStoreProvider.tsx @@ -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 | null>(null); + +export interface BookerStoreProviderProps { + children: ReactNode; +} + +export const BookerStoreProvider = ({ children }: BookerStoreProviderProps) => { + const storeRef = useRef>(); + if (!storeRef.current) { + storeRef.current = createBookerStore(); + } + + return {children}; +}; + +export const useBookerStoreContext = ( + 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, + ]); +}; diff --git a/packages/features/bookings/Booker/__tests__/Booker.test.tsx b/packages/features/bookings/Booker/__tests__/Booker.test.tsx index 7912574b684414..f1b6810e851508 100644 --- a/packages/features/bookings/Booker/__tests__/Booker.test.tsx +++ b/packages/features/bookings/Booker/__tests__/Booker.test.tsx @@ -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(, { + mockStore: { state: "loading" }, }); - - const { container } = render(); 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(); + render(, { + 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(); + render(, { + 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(, { + mockStore: { state: "booking" }, }); - - render(); const bookEventForm = screen.getByTestId("book-event-form"); await expect(bookEventForm).toHaveAttribute("data-unavailable", "true"); }); diff --git a/packages/features/bookings/Booker/__tests__/test-utils.tsx b/packages/features/bookings/Booker/__tests__/test-utils.tsx new file mode 100644 index 00000000000000..49202e56c7b351 --- /dev/null +++ b/packages/features/bookings/Booker/__tests__/test-utils.tsx @@ -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 { + mockStore?: Partial; +} + +const createMockStore = (initialState?: Partial): StoreApi => { + 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; +}; + +export const renderWithBookerStore = (ui: ReactElement, options?: CustomRenderOptions) => { + const mockStore = createMockStore(options?.mockStore); + + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + return render(ui, { wrapper: Wrapper, ...options }); +}; + +export * from "@testing-library/react"; + +export { renderWithBookerStore as render }; diff --git a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx index a1c5b6efc9bd8f..2fccf7e04f7a23 100644 --- a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx +++ b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx @@ -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(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, ] ); diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index 36fb8eb8a97899..d3834b3d063a99 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -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 & { 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(); diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx index dce506f198846f..2e4ae64ee19c64 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx @@ -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; diff --git a/packages/features/bookings/Booker/components/DatePicker.test.tsx b/packages/features/bookings/Booker/components/DatePicker.test.tsx index 26c645dee3e464..ffb49b26fb959a 100644 --- a/packages/features/bookings/Booker/components/DatePicker.test.tsx +++ b/packages/features/bookings/Booker/components/DatePicker.test.tsx @@ -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( { ], }} 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() ); diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx index cf257b5c73532a..e4025bc44a06fd 100644 --- a/packages/features/bookings/Booker/components/DatePicker.tsx +++ b/packages/features/bookings/Booker/components/DatePicker.tsx @@ -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 ); diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index c5584653c05af2..99298dca26d876 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -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 ); diff --git a/packages/features/bookings/Booker/components/Header.tsx b/packages/features/bookings/Booker/components/Header.tsx index 238bc3228132d3..52f8e1a32e9ba6 100644 --- a/packages/features/bookings/Booker/components/Header.tsx +++ b/packages/features/bookings/Booker/components/Header.tsx @@ -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; diff --git a/packages/features/bookings/Booker/components/LargeCalendar.tsx b/packages/features/bookings/Booker/components/LargeCalendar.tsx index 8d71521eb4c2bd..0151d399d2fb3d 100644 --- a/packages/features/bookings/Booker/components/LargeCalendar.tsx +++ b/packages/features/bookings/Booker/components/LargeCalendar.tsx @@ -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 | 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"); diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx index f449e97c8ddd0a..e6c449b08c3ca3 100644 --- a/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx +++ b/packages/features/bookings/Booker/components/OverlayCalendar/OverlayCalendarSwitch.tsx @@ -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; /** diff --git a/packages/features/bookings/Booker/components/Section.tsx b/packages/features/bookings/Booker/components/Section.tsx index ed8f96c2c30d01..d3593d1ce2f30d 100644 --- a/packages/features/bookings/Booker/components/Section.tsx +++ b/packages/features/bookings/Booker/components/Section.tsx @@ -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(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") { diff --git a/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts b/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts index d1d101d1badad1..c228bf1334c55a 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookerLayout.ts @@ -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; 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)"); diff --git a/packages/features/bookings/Booker/components/hooks/useBookerTime.ts b/packages/features/bookings/Booker/components/hooks/useBookerTime.ts index c5b403b6e4659c..1faf164dd28502 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookerTime.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookerTime.ts @@ -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, diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index 61556025e0e89e..6ca4358374bce0 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -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 rescheduleUid = useBookerStore((state) => state.rescheduleUid); - const rescheduledBy = useBookerStore((state) => state.rescheduledBy); - const bookingData = useBookerStore((state) => state.bookingData); - const timeslot = useBookerStore((state) => state.selectedTimeslot); + const eventSlug = useBookerStoreContext((state) => state.eventSlug); + const eventTypeId = useBookerStoreContext((state) => state.eventId); + const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting); + + 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(null); const [instantMeetingTokenExpiryTime, setExpiryTime] = useState(); const [instantVideoMeetingUrl, setInstantVideoMeetingUrl] = useState(); - const duration = useBookerStore((state) => state.selectedDuration); + const duration = useBookerStoreContext((state) => state.selectedDuration); const isRescheduling = !!rescheduleUid && !!bookingData; diff --git a/packages/features/bookings/Booker/components/hooks/useSlots.ts b/packages/features/bookings/Booker/components/hooks/useSlots.ts index fa055aabba3f4b..faebef50984eec 100644 --- a/packages/features/bookings/Booker/components/hooks/useSlots.ts +++ b/packages/features/bookings/Booker/components/hooks/useSlots.ts @@ -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; 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, diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 0a9eb641295e1e..ac95db747d28ad 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -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((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 ?? ""); - } +export const createBookerStore = () => + createWithEqualityFn((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; + } - // Setting month make sure small calendar in fullscreen layouts also updates. - // preventMonthSwitching is true in monthly view - if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { - set({ month: newSelection.format("YYYY-MM") }); + const currentSelection = dayjs(get().selectedDate); + const newSelection = dayjs(selectedDate); + set({ selectedDate }); if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { - updateQueryParam("month", newSelection.format("YYYY-MM")); + updateQueryParam("date", selectedDate ?? ""); } - } - }, - selectedDatesAndTimes: null, - setSelectedDatesAndTimes: (selectedDatesAndTimes) => { - set({ selectedDatesAndTimes }); - }, - addToSelectedDate: (days: number) => { - const currentSelection = dayjs(get().selectedDate); - let newSelection = currentSelection.add(days, "day"); - - // If newSelection is before the current date, set it to today - if (newSelection.isBefore(dayjs(), "day")) { - newSelection = dayjs(); - } - const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); + // Setting month make sure small calendar in fullscreen layouts also updates. + // preventMonthSwitching is true in monthly view + if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { + set({ month: newSelection.format("YYYY-MM") }); + if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { + updateQueryParam("month", newSelection.format("YYYY-MM")); + } + } + }, + selectedDatesAndTimes: null, + setSelectedDatesAndTimes: (selectedDatesAndTimes) => { + set({ selectedDatesAndTimes }); + }, + addToSelectedDate: (days: number) => { + const currentSelection = dayjs(get().selectedDate); + let newSelection = currentSelection.add(days, "day"); - if (newSelection.month() !== currentSelection.month()) { - set({ month: newSelection.format("YYYY-MM") }); - if (!get().isPlatform || get().allowUpdatingUrlParams) { - updateQueryParam("month", newSelection.format("YYYY-MM")); + // If newSelection is before the current date, set it to today + if (newSelection.isBefore(dayjs(), "day")) { + newSelection = dayjs(); } - } - set({ selectedDate: newSelectionFormatted }); - if (!get().isPlatform || get().allowUpdatingUrlParams) { - updateQueryParam("date", newSelectionFormatted); - } - }, - username: null, - eventSlug: null, - eventId: null, - rescheduledBy: null, - verifiedEmail: null, - setVerifiedEmail: (email: string | null) => { - set({ verifiedEmail: email }); - }, - month: - getQueryParam("month") || - (getQueryParam("date") && dayjs(getQueryParam("date")).isValid() - ? dayjs(getQueryParam("date")).format("YYYY-MM") - : null) || - dayjs().format("YYYY-MM"), - setMonth: (month: string | null) => { - if (!month) { - removeQueryParam("month"); - return; - } - set({ month, selectedTimeslot: null }); - if (!get().isPlatform || get().allowUpdatingUrlParams) { - updateQueryParam("month", month ?? ""); - } - get().setSelectedDate({ date: null }); - }, - dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null, - setDayCount: (dayCount: number | null) => { - set({ dayCount }); - }, - isTeamEvent: false, - seatedEventData: { - seatsPerTimeSlot: undefined, - attendees: undefined, - bookingUid: undefined, - showAvailableSeatsCount: true, - }, - setSeatedEventData: (seatedEventData: SeatedEventData) => { - set({ seatedEventData }); - if (!get().isPlatform || get().allowUpdatingUrlParams) { - updateQueryParam("bookingUid", seatedEventData.bookingUid ?? "null"); - } - }, - // This is different from timeZone in timePreferencesStore, because timeZone in timePreferencesStore is the preferred timezone of the booker, - // it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link. - // it is the timezone configured through query param. So, this is in a way the preference of the person who shared the link. - timezone: getQueryParam("cal.tz") ?? null, - setTimezone: (timezone: string | null) => { - set({ timezone }); - }, - initialize: ({ - username, - eventSlug, - month, - eventId, - rescheduleUid = null, - rescheduledBy = null, - bookingUid = null, - bookingData = null, - layout, - isTeamEvent, - durationConfig, - org, - isInstantMeeting, - timezone = null, - teamMemberEmail, - crmOwnerRecordType, - crmAppSlug, - crmRecordId, - isPlatform = false, - allowUpdatingUrlParams = true, - }: StoreInitializeType) => { - const selectedDateInStore = get().selectedDate; + const newSelectionFormatted = newSelection.format("YYYY-MM-DD"); + + if (newSelection.month() !== currentSelection.month()) { + set({ month: newSelection.format("YYYY-MM") }); + if (!get().isPlatform || get().allowUpdatingUrlParams) { + updateQueryParam("month", newSelection.format("YYYY-MM")); + } + } - if ( - get().username === username && - get().eventSlug === eventSlug && - get().month === month && - get().eventId === eventId && - get().rescheduleUid === rescheduleUid && - get().bookingUid === bookingUid && - get().bookingData?.responses.email === bookingData?.responses.email && - get().layout === layout && - get().timezone === timezone && - get().rescheduledBy === rescheduledBy && - get().teamMemberEmail === teamMemberEmail && - get().crmOwnerRecordType === crmOwnerRecordType && - get().crmAppSlug === crmAppSlug && - get().crmRecordId === crmRecordId - ) - return; - set({ + set({ selectedDate: newSelectionFormatted }); + if (!get().isPlatform || get().allowUpdatingUrlParams) { + updateQueryParam("date", newSelectionFormatted); + } + }, + 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) => { - set({ formValues }); - }, - org: null, - setOrg: (org: string | null | undefined) => { - set({ org }); - }, - isPlatform: false, - allowUpdatingUrlParams: true, -})); + }, + formValues: {}, + setFormValues: (formValues: Record) => { + 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, diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts index 5a69e6926e058f..a289b1c04e7580 100644 --- a/packages/features/bookings/Booker/utils/event.ts +++ b/packages/features/bookings/Booker/utils/event.ts @@ -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; export type useScheduleForEventReturnType = ReturnType; @@ -19,7 +19,7 @@ export type useScheduleForEventReturnType = ReturnType { - 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 ); diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index 2f4caa943e554f..89d7436f2a8eb4 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -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, diff --git a/packages/features/bookings/components/AvailableTimesHeader.tsx b/packages/features/bookings/components/AvailableTimesHeader.tsx index 74e11494ec0277..c422a4c3aed892 100644 --- a/packages/features/bookings/components/AvailableTimesHeader.tsx +++ b/packages/features/bookings/components/AvailableTimesHeader.tsx @@ -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"); diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index e3fea1fba13c30..4936c2c2f5a0fe 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -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) { diff --git a/packages/features/calendars/__tests__/DatePicker.test.tsx b/packages/features/calendars/__tests__/DatePicker.test.tsx index 4fa6791f6eb4bd..0f2f938fa0efe6 100644 --- a/packages/features/calendars/__tests__/DatePicker.test.tsx +++ b/packages/features/calendars/__tests__/DatePicker.test.tsx @@ -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( - - - + + + + + ); 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( - - - + + + + + ); 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( - - - + + + + + ); const selectedMonthLabel = getByTestId("selected-month-label"); @@ -85,22 +92,24 @@ describe("Tests for DatePicker Component", () => { ]); const { getAllByTestId } = render( - - - + + + + + ); const dayElements = getAllByTestId("day"); @@ -127,22 +136,24 @@ describe("Tests for DatePicker Component", () => { ]); const { getAllByTestId, queryByText } = render( - - - + + + + + ); 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( - - - + + + + + ); const dayElements = getAllByTestId("day"); diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 6feddac8251f90..3bc39082461860 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -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(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 ( - !open && handleDialogClose(), - } - : { - // Must not set name when noQueryParam mode as required by Dialog component - name: "embed", - clearQueryParamsOnClose: queryParamsForDialog, - })}> - {!embedParams.embedType ? ( - - ) : ( - - )} - + + !open && handleDialogClose(), + } + : { + // Must not set name when noQueryParam mode as required by Dialog component + name: "embed", + clearQueryParamsOnClose: queryParamsForDialog, + })}> + {!embedParams.embedType ? ( + + ) : ( + + )} + + ); }; diff --git a/packages/features/troubleshooter/Troubleshooter.tsx b/packages/features/troubleshooter/Troubleshooter.tsx index 029064d9d92764..e9c060908a952c 100644 --- a/packages/features/troubleshooter/Troubleshooter.tsx +++ b/packages/features/troubleshooter/Troubleshooter.tsx @@ -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 ; + return ( + + + + ); }; diff --git a/packages/platform/atoms/availability/AvailabilitySettings.tsx b/packages/platform/atoms/availability/AvailabilitySettings.tsx index 051c4d46194e15..a11c458ced261f 100644 --- a/packages/platform/atoms/availability/AvailabilitySettings.tsx +++ b/packages/platform/atoms/availability/AvailabilitySettings.tsx @@ -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 {enableOverrides && ( - + + + )}
diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx index ddee3f25ba551a..5916310d14e0cb 100644 --- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx +++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx @@ -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 ( + + + + ); +}; + function formatUsername(username: string | string[]): string { if (typeof username === "string") { return username; diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index b2015ea1582d41..19607722f77ae6 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -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>>; }; -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 ( + + + + ); +}; diff --git a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts index 4ee8b59728fbd0..740856e0404e98 100644 --- a/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts +++ b/packages/platform/atoms/hooks/bookings/useHandleBookEvent.ts @@ -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"); diff --git a/packages/platform/atoms/hooks/useSlots.ts b/packages/platform/atoms/hooks/useSlots.ts index 477801e1ddaf53..005fad29b353d4 100644 --- a/packages/platform/atoms/hooks/useSlots.ts +++ b/packages/platform/atoms/hooks/useSlots.ts @@ -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();