fix: add recurringEventCount to URL params (#24986)
* fix: validate occurrence count from URL params and refactor validation logic - Initialize occurrence count from URL query parameter on page load * fix: change query parameter name from occurenceCount to recurringEventCount * refactor: simplify occurrence count validation logic * fix: prevent NaN from being set in recurring event count query parameter * fix: prevent overlay calendar toggle from overwriting query params * fix: sync occurrence count state with max occurrences limit * refactor: rename occurenceCount to recurringEventCountQueryParam * fix: prevent invalid recurring event count from updating state - Added validation guard to only update state when recurringEventCountQueryParam is valid (not null or NaN) - Simplified URL update logic by removing unnecessary empty string fallback * feat: add recurring event count badge for mobile (#24991) * feat: display recurring event count badge in mobile * feat: update recurring event translation key for clarity
This commit is contained in:
@@ -3969,6 +3969,7 @@
|
||||
"slots_taken": "{{takenSeats}}/{{totalSeats}} slots taken",
|
||||
"team_invite_subtitle": "Invite team members to collaborate and schedule together",
|
||||
"team_onboarding_details_subtitle": "Add your team's name and create a unique URL for your team",
|
||||
"repeats_num_times": "Repeats {{count}} times",
|
||||
"view_booking_details": "View Booking Details",
|
||||
"view": "View",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
|
||||
@@ -42,8 +42,8 @@ const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerSt
|
||||
setTentativeSelectedTimeslots: vi.fn(),
|
||||
recurringEventCount: null,
|
||||
setRecurringEventCount: vi.fn(),
|
||||
occurenceCount: null,
|
||||
setOccurenceCount: vi.fn(),
|
||||
recurringEventCountQueryParam: null,
|
||||
setRecurringEventCountQueryParam: vi.fn(),
|
||||
dayCount: null,
|
||||
setDayCount: vi.fn(),
|
||||
rescheduleUid: null,
|
||||
|
||||
@@ -46,6 +46,7 @@ export const BookEventFormWrapperComponent = ({
|
||||
const { i18n, t } = useLocale();
|
||||
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
|
||||
const recurringEventCount = useBookerStoreContext((state) => state.recurringEventCount);
|
||||
const { timeFormat, timezone } = useBookerTime();
|
||||
if (!selectedTimeslot) {
|
||||
return null;
|
||||
@@ -67,6 +68,16 @@ export const BookEventFormWrapperComponent = ({
|
||||
<span>{getDurationFormatted(selectedDuration || eventLength, t)}</span>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{recurringEventCount && recurringEventCount > 1 && (
|
||||
<Badge variant="grayWithoutHover" startIcon="refresh-ccw" size="lg">
|
||||
<span>
|
||||
{t("repeats_num_times", {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{child}
|
||||
</>
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
import { useEffect } from "react";
|
||||
import { createWithEqualityFn } from "zustand/traditional";
|
||||
|
||||
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BOOKER_NUMBER_OF_DAYS_TO_LOAD } from "@calcom/lib/constants";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
|
||||
|
||||
import type { GetBookingType } from "../lib/get-booking";
|
||||
import type { BookerState, BookerLayout } from "./types";
|
||||
import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query-param";
|
||||
|
||||
|
||||
/**
|
||||
* Arguments passed into store initializer, containing
|
||||
* the event data.
|
||||
@@ -125,8 +130,8 @@ export type BookerStore = {
|
||||
/**
|
||||
* Input occurrence count.
|
||||
*/
|
||||
occurenceCount: number | null;
|
||||
setOccurenceCount(count: number | null): void;
|
||||
recurringEventCountQueryParam: number | null;
|
||||
setRecurringEventCountQueryParam(count: number | null): void;
|
||||
/**
|
||||
* The number of days worth of schedules to load.
|
||||
*/
|
||||
@@ -421,8 +426,17 @@ export const createBookerStore = () =>
|
||||
},
|
||||
recurringEventCount: null,
|
||||
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
|
||||
occurenceCount: null,
|
||||
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
|
||||
recurringEventCountQueryParam: Number(getQueryParam("recurringEventCount")) || null,
|
||||
setRecurringEventCountQueryParam: (recurringEventCountQueryParam: number | null) => {
|
||||
// Guard: only update state if value is valid (not NaN or null)
|
||||
if (recurringEventCountQueryParam !== null && !isNaN(recurringEventCountQueryParam)) {
|
||||
set({ recurringEventCountQueryParam });
|
||||
if (!get().isPlatform || get().allowUpdatingUrlParams) {
|
||||
updateQueryParam("recurringEventCount", recurringEventCountQueryParam);
|
||||
}
|
||||
}
|
||||
// If invalid, don't update state or URL - just ignore the call
|
||||
},
|
||||
rescheduleUid: null,
|
||||
bookingData: null,
|
||||
bookingUid: null,
|
||||
@@ -523,4 +537,4 @@ export const useInitializeBookerStore = ({
|
||||
isPlatform,
|
||||
allowUpdatingUrlParams,
|
||||
]);
|
||||
};
|
||||
};
|
||||
@@ -14,25 +14,42 @@ import { useBookerTime } from "../../Booker/components/hooks/useBookerTime";
|
||||
export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurringEvent"> }) => {
|
||||
const maxOccurences = event.recurringEvent?.count || null;
|
||||
const { t, i18n } = useLocale();
|
||||
const [setRecurringEventCount, recurringEventCount, setOccurenceCount, occurenceCount] =
|
||||
const [setRecurringEventCount, recurringEventCount, setRecurringEventCountQueryParam, recurringEventCountQueryParam] =
|
||||
useBookerStoreContext((state) => [
|
||||
state.setRecurringEventCount,
|
||||
state.recurringEventCount,
|
||||
state.setOccurenceCount,
|
||||
state.occurenceCount,
|
||||
state.setRecurringEventCountQueryParam,
|
||||
state.recurringEventCountQueryParam,
|
||||
]);
|
||||
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
|
||||
const bookerState = useBookerStoreContext((state) => state.state);
|
||||
const { timezone, timeFormat } = useBookerTime();
|
||||
const [warning, setWarning] = useState(false);
|
||||
// Set initial value in booker store.
|
||||
|
||||
const validateAndSetRecurringEventCount = (value: number | string) => {
|
||||
const inputValue = parseInt(value as string);
|
||||
const isValid =
|
||||
!isNaN(inputValue) && inputValue >= 1 && maxOccurences !== null && inputValue <= maxOccurences;
|
||||
|
||||
if (isValid) {
|
||||
setRecurringEventCount(inputValue);
|
||||
setWarning(false);
|
||||
} else {
|
||||
setRecurringEventCount(maxOccurences);
|
||||
setWarning(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!event.recurringEvent?.count) return;
|
||||
setOccurenceCount(occurenceCount || event.recurringEvent.count);
|
||||
setRecurringEventCount(recurringEventCount || event.recurringEvent.count);
|
||||
if (occurenceCount && (occurenceCount > event.recurringEvent.count || occurenceCount < 1))
|
||||
setWarning(true);
|
||||
}, [setRecurringEventCount, event.recurringEvent, recurringEventCount, setOccurenceCount, occurenceCount]);
|
||||
|
||||
if (recurringEventCountQueryParam) {
|
||||
validateAndSetRecurringEventCount(recurringEventCountQueryParam);
|
||||
} else {
|
||||
setRecurringEventCount(maxOccurences);
|
||||
setRecurringEventCountQueryParam(maxOccurences);
|
||||
}
|
||||
}, [setRecurringEventCount, event.recurringEvent, recurringEventCount, recurringEventCountQueryParam]);
|
||||
if (!event.recurringEvent) return null;
|
||||
|
||||
if (bookerState === "booking" && recurringEventCount && selectedTimeslot) {
|
||||
@@ -72,23 +89,12 @@ export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurring
|
||||
type="number"
|
||||
min="1"
|
||||
max={event.recurringEvent.count}
|
||||
defaultValue={occurenceCount || event.recurringEvent.count}
|
||||
defaultValue={recurringEventCountQueryParam || event.recurringEvent.count}
|
||||
data-testid="occurrence-input"
|
||||
onChange={(event) => {
|
||||
const pattern = /^(?=.*[0-9])\S+$/;
|
||||
const inputValue = parseInt(event.target.value);
|
||||
setOccurenceCount(inputValue);
|
||||
if (
|
||||
!pattern.test(event.target.value) ||
|
||||
inputValue < 1 ||
|
||||
(maxOccurences && inputValue > maxOccurences)
|
||||
) {
|
||||
setWarning(true);
|
||||
setRecurringEventCount(maxOccurences);
|
||||
} else {
|
||||
setWarning(false);
|
||||
setRecurringEventCount(inputValue);
|
||||
}
|
||||
setRecurringEventCountQueryParam(inputValue);
|
||||
validateAndSetRecurringEventCount(event.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -185,20 +185,17 @@ const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
// Toggle query param for overlay calendar
|
||||
const onOverlaySwitchStateChange = useCallback(
|
||||
(state: boolean) => {
|
||||
const current = new URLSearchParams(Array.from(searchParams?.entries() ?? []));
|
||||
const url = new URL(window.location.href);
|
||||
if (state) {
|
||||
current.set("overlayCalendar", "true");
|
||||
url.searchParams.set("overlayCalendar", "true");
|
||||
localStorage.setItem("overlayCalendarSwitchDefault", "true");
|
||||
} else {
|
||||
current.delete("overlayCalendar");
|
||||
url.searchParams.delete("overlayCalendar");
|
||||
localStorage.removeItem("overlayCalendarSwitchDefault");
|
||||
}
|
||||
// cast to string
|
||||
const value = current.toString();
|
||||
const query = value ? `?${value}` : "";
|
||||
router.push(`${pathname}${query}`);
|
||||
router.push(`${url.pathname}${url.search}`);
|
||||
},
|
||||
[searchParams, pathname, router]
|
||||
[router]
|
||||
);
|
||||
useBrandColors({
|
||||
brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type React from "react";
|
||||
|
||||
|
||||
|
||||
import type { BookerProps } from "@calcom/features/bookings/Booker";
|
||||
import type { BookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import type { Timezone, VIEW_TYPE } from "@calcom/features/bookings/Booker/types";
|
||||
import type { BookingCreateBody } from "@calcom/features/bookings/lib/bookingCreateBodySchema";
|
||||
import type { BookingResponse } from "@calcom/platform-libraries";
|
||||
import type {
|
||||
ApiSuccessResponse,
|
||||
ApiErrorResponse,
|
||||
ApiSuccessResponseWithoutData,
|
||||
RoutingFormSearchParams,
|
||||
} from "@calcom/platform-types";
|
||||
import type { ApiSuccessResponse, ApiErrorResponse, ApiSuccessResponseWithoutData, RoutingFormSearchParams } from "@calcom/platform-types";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||
|
||||
|
||||
|
||||
import type { UseCreateBookingInput } from "../hooks/bookings/useCreateBooking";
|
||||
|
||||
|
||||
// Type that includes only the data values from BookerStore (excluding functions)
|
||||
export type BookerStoreValues = Omit<
|
||||
BookerStore,
|
||||
@@ -32,7 +32,7 @@ export type BookerStoreValues = Omit<
|
||||
| "setSelectedDuration"
|
||||
| "setBookingData"
|
||||
| "setRecurringEventCount"
|
||||
| "setOccurenceCount"
|
||||
| "setRecurringEventCountQueryParam"
|
||||
| "setTentativeSelectedTimeslots"
|
||||
| "setSelectedTimeslot"
|
||||
| "setFormValues"
|
||||
@@ -101,4 +101,4 @@ export type BookerPlatformWrapperAtomPropsForTeam = BookerPlatformWrapperAtomPro
|
||||
isTeamEvent: true;
|
||||
teamId: number;
|
||||
routingFormSearchParams?: RoutingFormSearchParams;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user