Skip to content

Commit b2bc193

Browse files
committed
fix(calendar-web): resolve onRangeChange errors, input validation, and currentView tracking
1 parent edfe61e commit b2bc193

6 files changed

Lines changed: 98 additions & 61 deletions

File tree

packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils";
33

44
import MxCalendar from "../Calendar";
55
import { CalendarContainerProps } from "../../typings/CalendarProps";
6+
import { CalendarPropsBuilder } from "../helpers/CalendarPropsBuilder";
67

78
// Mock react-big-calendar to avoid View.title issues
89
jest.mock("react-big-calendar", () => {
@@ -19,6 +20,8 @@ jest.mock("react-big-calendar", () => {
1920
min,
2021
max,
2122
events,
23+
step,
24+
timeslots,
2225
...domProps
2326
}: any) => (
2427
<div
@@ -31,6 +34,8 @@ jest.mock("react-big-calendar", () => {
3134
data-min={min?.toISOString()}
3235
data-max={max?.toISOString()}
3336
data-events-count={events?.length ?? 0}
37+
data-step={step}
38+
data-timeslots={timeslots}
3439
{...domProps}
3540
>
3641
{children}
@@ -124,4 +129,71 @@ describe("Calendar", () => {
124129
// Since we're mocking the calendar, we can't test for specific text content
125130
// but we can verify the component renders without errors
126131
});
132+
133+
it("passes step and timeslots to the calendar", () => {
134+
const { getByTestId } = render(<MxCalendar {...customViewProps} />);
135+
const calendar = getByTestId("mock-calendar");
136+
expect(calendar.getAttribute("data-step")).toBe("60");
137+
expect(calendar.getAttribute("data-timeslots")).toBe("2");
138+
});
139+
});
140+
141+
describe("CalendarPropsBuilder validation", () => {
142+
const mockLocalizer = {
143+
format: jest.fn(),
144+
parse: jest.fn(),
145+
startOfWeek: jest.fn(),
146+
getDay: jest.fn(),
147+
messages: {}
148+
} as any;
149+
150+
const buildWithStepTimeslots = (step: number, timeslots: number) => {
151+
const props = { ...customViewProps, step, timeslots };
152+
const builder = new CalendarPropsBuilder(props);
153+
return builder.build(mockLocalizer, "en");
154+
};
155+
156+
it("clamps step=0 to 1", () => {
157+
const result = buildWithStepTimeslots(0, 2);
158+
expect(result.step).toBe(1);
159+
expect(result.timeslots).toBe(2);
160+
});
161+
162+
it("clamps negative step to 1", () => {
163+
const result = buildWithStepTimeslots(-5, 1);
164+
expect(result.step).toBe(1);
165+
});
166+
167+
it("clamps step above 60 to 60", () => {
168+
const result = buildWithStepTimeslots(100, 1);
169+
expect(result.step).toBe(60);
170+
});
171+
172+
it("clamps timeslots=0 to 1", () => {
173+
const result = buildWithStepTimeslots(30, 0);
174+
expect(result.timeslots).toBe(1);
175+
});
176+
177+
it("clamps timeslots above 4 to 4", () => {
178+
const result = buildWithStepTimeslots(30, 100);
179+
expect(result.timeslots).toBe(4);
180+
});
181+
182+
it("preserves boundary values (step=1, timeslots=1)", () => {
183+
const result = buildWithStepTimeslots(1, 1);
184+
expect(result.step).toBe(1);
185+
expect(result.timeslots).toBe(1);
186+
});
187+
188+
it("preserves upper boundary values (step=60, timeslots=4)", () => {
189+
const result = buildWithStepTimeslots(60, 4);
190+
expect(result.step).toBe(60);
191+
expect(result.timeslots).toBe(4);
192+
});
193+
194+
it("accepts valid step and timeslots without clamping", () => {
195+
const result = buildWithStepTimeslots(30, 2);
196+
expect(result.step).toBe(30);
197+
expect(result.timeslots).toBe(2);
198+
});
127199
});

packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ exports[`Calendar renders correctly with basic props 1`] = `
1515
data-resizable="true"
1616
data-selectable="true"
1717
data-show-all-events="true"
18+
data-step="60"
1819
data-testid="mock-calendar"
20+
data-timeslots="2"
1921
formats="[object Object]"
2022
localizer="[object Object]"
2123
messages="[object Object]"
22-
step="60"
23-
timeslots="2"
2424
views="[object Object]"
2525
/>
2626
</div>

packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@ export class CalendarPropsBuilder {
2525
this.minTime = this.buildTime(props.minHour ?? 0);
2626
this.maxTime = this.buildTime(props.maxHour ?? 24);
2727
this.toolbarItems = this.buildToolbarItems();
28-
this.step = props.step;
29-
this.timeSlots = props.timeslots;
28+
this.step = Math.max(1, Math.min(props.step, 60));
29+
this.timeSlots = Math.max(1, Math.min(props.timeslots, 4));
30+
31+
if (props.step !== this.step) {
32+
console.warn(`[Calendar] step value ${props.step} was clamped to ${this.step}. Must be between 1 and 60.`);
33+
}
34+
if (props.timeslots !== this.timeSlots) {
35+
console.warn(
36+
`[Calendar] timeslots value ${props.timeslots} was clamped to ${this.timeSlots}. Must be between 1 and 4.`
37+
);
38+
}
3039
}
3140

3241
updateProps(props: CalendarContainerProps): void {

packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { addWeeks, differenceInCalendarDays, getRange } from "../utils/calendar-
77
type CustomWeekComponent = ((viewProps: CalendarProps) => ReactElement) & {
88
navigate: (date: Date, action: NavigateAction) => Date;
99
title: (date: Date, options: any) => string;
10+
range: (date: Date, options?: { localizer?: any }) => Date[];
1011
};
1112

1213
export class CustomWeekController {
@@ -76,6 +77,7 @@ export class CustomWeekController {
7677
Component.navigate = CustomWeekController.navigate;
7778
Component.title = (date: Date, options: any): string =>
7879
CustomWeekController.title(date, options, visibleDays, titlePattern);
80+
Component.range = (date: Date): Date[] => getRange(date, visibleDays);
7981

8082
return Component;
8183
}

packages/pluggableWidgets/calendar-web/src/helpers/useCalendarEvents.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
22
import { CalendarEvent, EventDropOrResize } from "../utils/typings";
33
import { CalendarContainerProps } from "../../typings/CalendarProps";
44
import { CalendarProps, NavigateAction, View } from "react-big-calendar";
5-
import { getViewRange } from "../utils/calendar-utils";
65

76
type CalendarEventHandlers = Pick<
87
CalendarProps<CalendarEvent>,
@@ -122,21 +121,14 @@ export function useCalendarEvents(props: CalendarContainerProps): CalendarEventH
122121
[onDragDropResize]
123122
);
124123

125-
const handleNavigate = useCallback(
126-
(date: Date, view: string, _action: NavigateAction) => {
127-
const action = onViewRangeChange;
124+
// Track the current view so we can pass it to onRangeChange.
125+
// RBC calls onNavigate (with view) synchronously before onRangeChange (without view)
126+
// during navigation, so the ref is always up-to-date when handleRangeChange reads it.
127+
const currentViewRef = useRef<string | undefined>(undefined);
128128

129-
if (action?.canExecute) {
130-
const { start, end } = getViewRange(view, date);
131-
action.execute({
132-
rangeStart: start,
133-
rangeEnd: end,
134-
currentView: view
135-
});
136-
}
137-
},
138-
[onViewRangeChange]
139-
);
129+
const handleNavigate = useCallback((_date: Date, view: string, _action: NavigateAction) => {
130+
currentViewRef.current = view;
131+
}, []);
140132

141133
const handleRangeChange = useCallback(
142134
(range: Date[] | { start: Date; end: Date }, view?: View) => {
@@ -145,11 +137,12 @@ export function useCalendarEvents(props: CalendarContainerProps): CalendarEventH
145137
if (action?.canExecute) {
146138
const start = Array.isArray(range) ? range[0] : range.start;
147139
const end = Array.isArray(range) ? range[range.length - 1] : range.end;
140+
const resolvedView = view ?? currentViewRef.current;
148141

149142
action.execute({
150143
rangeStart: start,
151144
rangeEnd: end,
152-
currentView: view
145+
currentView: resolvedView
153146
});
154147
}
155148
},

packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@ import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop";
33
import { CalendarEvent } from "./typings";
44
import "react-big-calendar/lib/addons/dragAndDrop/styles.css";
55
import "react-big-calendar/lib/css/react-big-calendar.css";
6-
import {
7-
addDays,
8-
addWeeks,
9-
differenceInCalendarDays,
10-
endOfMonth,
11-
endOfWeek,
12-
format,
13-
getDay,
14-
parse,
15-
startOfMonth,
16-
startOfWeek
17-
} from "date-fns";
6+
import { addDays, addWeeks, differenceInCalendarDays, format, getDay, parse, startOfWeek } from "date-fns";
187
import type { MXLocaleDates, MXLocaleNumbers, MXLocalePatterns, MXSessionData } from "../../typings/global";
198

209
// Utility to lighten hex colors. Accepts #RGB or #RRGGBB.
@@ -41,18 +30,7 @@ function lightenColor(color: string, amount = 0.2): string {
4130
return color;
4231
}
4332

44-
export {
45-
format,
46-
parse,
47-
startOfWeek,
48-
getDay,
49-
addDays,
50-
startOfMonth,
51-
endOfMonth,
52-
endOfWeek,
53-
addWeeks,
54-
differenceInCalendarDays
55-
};
33+
export { format, parse, startOfWeek, getDay, addDays, addWeeks, differenceInCalendarDays };
5634

5735
export const DnDCalendar = withDragAndDrop(Calendar<CalendarEvent>);
5836

@@ -90,23 +68,6 @@ export function getRange(date: Date, visibleDays: Set<number>): Date[] {
9068
);
9169
}
9270

93-
export function getViewRange(view: string, date: Date): { start: Date; end: Date } {
94-
switch (view) {
95-
case "month":
96-
return { start: startOfMonth(date), end: endOfMonth(date) };
97-
case "week":
98-
return { start: startOfWeek(date), end: endOfWeek(date) };
99-
case "work_week": {
100-
const start = startOfWeek(date);
101-
return { start, end: addDays(start, 4) };
102-
}
103-
case "day":
104-
return { start: date, end: date };
105-
default:
106-
return { start: date, end: date };
107-
}
108-
}
109-
11071
/**
11172
* Converts empty or whitespace-only strings to undefined.
11273
* Useful for handling optional textTemplate values from Mendix.

0 commit comments

Comments
 (0)