Skip to content

Commit 6af7f8b

Browse files
authored
fix: add recurringEventCount to URL params (calcom#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 (calcom#24991) * feat: display recurring event count badge in mobile * feat: update recurring event translation key for clarity
1 parent 421d03e commit 6af7f8b

7 files changed

Lines changed: 75 additions & 46 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3969,6 +3969,7 @@
39693969
"slots_taken": "{{takenSeats}}/{{totalSeats}} slots taken",
39703970
"team_invite_subtitle": "Invite team members to collaborate and schedule together",
39713971
"team_onboarding_details_subtitle": "Add your team's name and create a unique URL for your team",
3972+
"repeats_num_times": "Repeats {{count}} times",
39723973
"view_booking_details": "View Booking Details",
39733974
"view": "View",
39743975
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"

packages/features/bookings/Booker/__tests__/test-utils.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ const createMockStore = (initialState?: Partial<BookerStore>): StoreApi<BookerSt
4242
setTentativeSelectedTimeslots: vi.fn(),
4343
recurringEventCount: null,
4444
setRecurringEventCount: vi.fn(),
45-
occurenceCount: null,
46-
setOccurenceCount: vi.fn(),
45+
recurringEventCountQueryParam: null,
46+
setRecurringEventCountQueryParam: vi.fn(),
4747
dayCount: null,
4848
setDayCount: vi.fn(),
4949
rescheduleUid: null,

packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const BookEventFormWrapperComponent = ({
4646
const { i18n, t } = useLocale();
4747
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
4848
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
49+
const recurringEventCount = useBookerStoreContext((state) => state.recurringEventCount);
4950
const { timeFormat, timezone } = useBookerTime();
5051
if (!selectedTimeslot) {
5152
return null;
@@ -67,6 +68,16 @@ export const BookEventFormWrapperComponent = ({
6768
<span>{getDurationFormatted(selectedDuration || eventLength, t)}</span>
6869
</Badge>
6970
)}
71+
72+
{recurringEventCount && recurringEventCount > 1 && (
73+
<Badge variant="grayWithoutHover" startIcon="refresh-ccw" size="lg">
74+
<span>
75+
{t("repeats_num_times", {
76+
count: recurringEventCount,
77+
})}
78+
</span>
79+
</Badge>
80+
)}
7081
</div>
7182
{child}
7283
</>

packages/features/bookings/Booker/store.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
import { useEffect } from "react";
44
import { createWithEqualityFn } from "zustand/traditional";
55

6+
7+
68
import dayjs from "@calcom/dayjs";
79
import { BOOKER_NUMBER_OF_DAYS_TO_LOAD } from "@calcom/lib/constants";
810
import { BookerLayouts } from "@calcom/prisma/zod-utils";
911

12+
13+
1014
import type { GetBookingType } from "../lib/get-booking";
1115
import type { BookerState, BookerLayout } from "./types";
1216
import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query-param";
1317

18+
1419
/**
1520
* Arguments passed into store initializer, containing
1621
* the event data.
@@ -125,8 +130,8 @@ export type BookerStore = {
125130
/**
126131
* Input occurrence count.
127132
*/
128-
occurenceCount: number | null;
129-
setOccurenceCount(count: number | null): void;
133+
recurringEventCountQueryParam: number | null;
134+
setRecurringEventCountQueryParam(count: number | null): void;
130135
/**
131136
* The number of days worth of schedules to load.
132137
*/
@@ -421,8 +426,17 @@ export const createBookerStore = () =>
421426
},
422427
recurringEventCount: null,
423428
setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }),
424-
occurenceCount: null,
425-
setOccurenceCount: (occurenceCount: number | null) => set({ occurenceCount }),
429+
recurringEventCountQueryParam: Number(getQueryParam("recurringEventCount")) || null,
430+
setRecurringEventCountQueryParam: (recurringEventCountQueryParam: number | null) => {
431+
// Guard: only update state if value is valid (not NaN or null)
432+
if (recurringEventCountQueryParam !== null && !isNaN(recurringEventCountQueryParam)) {
433+
set({ recurringEventCountQueryParam });
434+
if (!get().isPlatform || get().allowUpdatingUrlParams) {
435+
updateQueryParam("recurringEventCount", recurringEventCountQueryParam);
436+
}
437+
}
438+
// If invalid, don't update state or URL - just ignore the call
439+
},
426440
rescheduleUid: null,
427441
bookingData: null,
428442
bookingUid: null,
@@ -523,4 +537,4 @@ export const useInitializeBookerStore = ({
523537
isPlatform,
524538
allowUpdatingUrlParams,
525539
]);
526-
};
540+
};

packages/features/bookings/components/event-meta/Occurences.tsx

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,42 @@ import { useBookerTime } from "../../Booker/components/hooks/useBookerTime";
1414
export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurringEvent"> }) => {
1515
const maxOccurences = event.recurringEvent?.count || null;
1616
const { t, i18n } = useLocale();
17-
const [setRecurringEventCount, recurringEventCount, setOccurenceCount, occurenceCount] =
17+
const [setRecurringEventCount, recurringEventCount, setRecurringEventCountQueryParam, recurringEventCountQueryParam] =
1818
useBookerStoreContext((state) => [
1919
state.setRecurringEventCount,
2020
state.recurringEventCount,
21-
state.setOccurenceCount,
22-
state.occurenceCount,
21+
state.setRecurringEventCountQueryParam,
22+
state.recurringEventCountQueryParam,
2323
]);
2424
const selectedTimeslot = useBookerStoreContext((state) => state.selectedTimeslot);
2525
const bookerState = useBookerStoreContext((state) => state.state);
2626
const { timezone, timeFormat } = useBookerTime();
2727
const [warning, setWarning] = useState(false);
28-
// Set initial value in booker store.
28+
29+
const validateAndSetRecurringEventCount = (value: number | string) => {
30+
const inputValue = parseInt(value as string);
31+
const isValid =
32+
!isNaN(inputValue) && inputValue >= 1 && maxOccurences !== null && inputValue <= maxOccurences;
33+
34+
if (isValid) {
35+
setRecurringEventCount(inputValue);
36+
setWarning(false);
37+
} else {
38+
setRecurringEventCount(maxOccurences);
39+
setWarning(true);
40+
}
41+
};
42+
2943
useEffect(() => {
3044
if (!event.recurringEvent?.count) return;
31-
setOccurenceCount(occurenceCount || event.recurringEvent.count);
32-
setRecurringEventCount(recurringEventCount || event.recurringEvent.count);
33-
if (occurenceCount && (occurenceCount > event.recurringEvent.count || occurenceCount < 1))
34-
setWarning(true);
35-
}, [setRecurringEventCount, event.recurringEvent, recurringEventCount, setOccurenceCount, occurenceCount]);
45+
46+
if (recurringEventCountQueryParam) {
47+
validateAndSetRecurringEventCount(recurringEventCountQueryParam);
48+
} else {
49+
setRecurringEventCount(maxOccurences);
50+
setRecurringEventCountQueryParam(maxOccurences);
51+
}
52+
}, [setRecurringEventCount, event.recurringEvent, recurringEventCount, recurringEventCountQueryParam]);
3653
if (!event.recurringEvent) return null;
3754

3855
if (bookerState === "booking" && recurringEventCount && selectedTimeslot) {
@@ -72,23 +89,12 @@ export const EventOccurences = ({ event }: { event: Pick<BookerEvent, "recurring
7289
type="number"
7390
min="1"
7491
max={event.recurringEvent.count}
75-
defaultValue={occurenceCount || event.recurringEvent.count}
92+
defaultValue={recurringEventCountQueryParam || event.recurringEvent.count}
7693
data-testid="occurrence-input"
7794
onChange={(event) => {
78-
const pattern = /^(?=.*[0-9])\S+$/;
7995
const inputValue = parseInt(event.target.value);
80-
setOccurenceCount(inputValue);
81-
if (
82-
!pattern.test(event.target.value) ||
83-
inputValue < 1 ||
84-
(maxOccurences && inputValue > maxOccurences)
85-
) {
86-
setWarning(true);
87-
setRecurringEventCount(maxOccurences);
88-
} else {
89-
setWarning(false);
90-
setRecurringEventCount(inputValue);
91-
}
96+
setRecurringEventCountQueryParam(inputValue);
97+
validateAndSetRecurringEventCount(event.target.value);
9298
}}
9399
/>
94100

packages/platform/atoms/booker/BookerWebWrapper.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,20 +185,17 @@ const BookerPlatformWrapperComponent = (props: BookerWebWrapperAtomProps) => {
185185
// Toggle query param for overlay calendar
186186
const onOverlaySwitchStateChange = useCallback(
187187
(state: boolean) => {
188-
const current = new URLSearchParams(Array.from(searchParams?.entries() ?? []));
188+
const url = new URL(window.location.href);
189189
if (state) {
190-
current.set("overlayCalendar", "true");
190+
url.searchParams.set("overlayCalendar", "true");
191191
localStorage.setItem("overlayCalendarSwitchDefault", "true");
192192
} else {
193-
current.delete("overlayCalendar");
193+
url.searchParams.delete("overlayCalendar");
194194
localStorage.removeItem("overlayCalendarSwitchDefault");
195195
}
196-
// cast to string
197-
const value = current.toString();
198-
const query = value ? `?${value}` : "";
199-
router.push(`${pathname}${query}`);
196+
router.push(`${url.pathname}${url.search}`);
200197
},
201-
[searchParams, pathname, router]
198+
[router]
202199
);
203200
useBrandColors({
204201
brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,

packages/platform/atoms/booker/types.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import type React from "react";
22

3+
4+
35
import type { BookerProps } from "@calcom/features/bookings/Booker";
46
import type { BookerStore } from "@calcom/features/bookings/Booker/store";
57
import type { Timezone, VIEW_TYPE } from "@calcom/features/bookings/Booker/types";
68
import type { BookingCreateBody } from "@calcom/features/bookings/lib/bookingCreateBodySchema";
79
import type { BookingResponse } from "@calcom/platform-libraries";
8-
import type {
9-
ApiSuccessResponse,
10-
ApiErrorResponse,
11-
ApiSuccessResponseWithoutData,
12-
RoutingFormSearchParams,
13-
} from "@calcom/platform-types";
10+
import type { ApiSuccessResponse, ApiErrorResponse, ApiSuccessResponseWithoutData, RoutingFormSearchParams } from "@calcom/platform-types";
1411
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
1512

13+
14+
1615
import type { UseCreateBookingInput } from "../hooks/bookings/useCreateBooking";
1716

17+
1818
// Type that includes only the data values from BookerStore (excluding functions)
1919
export type BookerStoreValues = Omit<
2020
BookerStore,
@@ -32,7 +32,7 @@ export type BookerStoreValues = Omit<
3232
| "setSelectedDuration"
3333
| "setBookingData"
3434
| "setRecurringEventCount"
35-
| "setOccurenceCount"
35+
| "setRecurringEventCountQueryParam"
3636
| "setTentativeSelectedTimeslots"
3737
| "setSelectedTimeslot"
3838
| "setFormValues"
@@ -101,4 +101,4 @@ export type BookerPlatformWrapperAtomPropsForTeam = BookerPlatformWrapperAtomPro
101101
isTeamEvent: true;
102102
teamId: number;
103103
routingFormSearchParams?: RoutingFormSearchParams;
104-
};
104+
};

0 commit comments

Comments
 (0)