Skip to content

Commit d906658

Browse files
[WC-3208]: Calendar module Start Date Attribute implementation (#2108)
2 parents 08db452 + 7e721a6 commit d906658

8 files changed

Lines changed: 209 additions & 63 deletions

File tree

packages/pluggableWidgets/calendar-web/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Improved handling of the start date attribute to ensure correct calendar initialization.
12+
913
## [2.3.0] - 2026-02-17
1014

1115
### Added

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactElement, useMemo } from "react";
1+
import { Fragment, ReactElement, useMemo } from "react";
22
import classNames from "classnames";
33
import { CalendarContainerProps } from "../typings/CalendarProps";
44
import { CalendarPropsBuilder } from "./helpers/CalendarPropsBuilder";
@@ -26,9 +26,16 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement
2626
}, [props, calendarController, localizer, culture]);
2727

2828
const calendarEvents = useCalendarEvents(props);
29+
2930
return (
30-
<div className={classNames("widget-calendar", props.class)} style={wrapperStyle}>
31-
<DnDCalendar {...calendarProps} {...calendarEvents} />
32-
</div>
31+
<Fragment>
32+
{props.startDateAttribute?.status === "loading" ? (
33+
<progress className="widget-calendar-loading-bar" />
34+
) : (
35+
<div className={classNames("widget-calendar", props.class)} style={wrapperStyle}>
36+
<DnDCalendar {...calendarProps} {...calendarEvents} />
37+
</div>
38+
)}
39+
</Fragment>
3340
);
3441
}

packages/pluggableWidgets/calendar-web/src/Calendar.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@
6262
<attributeType name="String" />
6363
</attributeTypes>
6464
</property>
65-
<property key="startDateAttribute" type="attribute" dataSource="databaseDataSource" required="false">
66-
<caption>Start date attribute</caption>
67-
<description>The start date that should be shown in the view</description>
68-
<attributeTypes>
69-
<attributeType name="DateTime" />
70-
</attributeTypes>
71-
</property>
7265
</propertyGroup>
7366
</propertyGroup>
7467
<propertyGroup caption="View">
@@ -139,6 +132,13 @@
139132
<caption>Time slots</caption>
140133
<description>The number of slots per "section" in the time grid views. Adjust with step to change the default of 1 hour long groups, with 30 minute slots</description>
141134
</property>
135+
<property key="startDateAttribute" type="attribute" required="false">
136+
<caption>Start date attribute</caption>
137+
<description>The DateTime attribute used on initial load</description>
138+
<attributeTypes>
139+
<attributeType name="DateTime" />
140+
</attributeTypes>
141+
</property>
142142
</propertyGroup>
143143
</propertyGroup>
144144
<propertyGroup caption="Custom view">

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

Lines changed: 105 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render } from "@testing-library/react";
1+
import { render, screen } from "@testing-library/react";
22
import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils";
33

44
import MxCalendar from "../Calendar";
@@ -10,37 +10,39 @@ jest.mock("react-big-calendar", () => {
1010
const originalModule = jest.requireActual("react-big-calendar");
1111
return {
1212
...originalModule,
13-
Calendar: ({
14-
children,
15-
defaultView,
16-
culture,
17-
resizable,
18-
selectable,
19-
showAllEvents,
20-
min,
21-
max,
22-
events,
23-
step,
24-
timeslots,
25-
...domProps
26-
}: any) => (
27-
<div
28-
data-testid="mock-calendar"
29-
data-default-view={defaultView}
30-
data-culture={culture}
31-
data-resizable={resizable}
32-
data-selectable={selectable}
33-
data-show-all-events={showAllEvents}
34-
data-min={min?.toISOString()}
35-
data-max={max?.toISOString()}
36-
data-events-count={events?.length ?? 0}
37-
data-step={step}
38-
data-timeslots={timeslots}
39-
{...domProps}
40-
>
41-
{children}
42-
</div>
43-
),
13+
Calendar: (mockProps: any) => {
14+
const {
15+
children,
16+
defaultView,
17+
defaultDate,
18+
culture,
19+
resizable,
20+
selectable,
21+
showAllEvents,
22+
events,
23+
step,
24+
timeslots,
25+
...domProps
26+
} = mockProps;
27+
28+
return (
29+
<div
30+
data-testid="mock-calendar"
31+
data-default-view={defaultView}
32+
data-default-date={defaultDate?.toISOString()}
33+
data-culture={culture}
34+
data-resizable={resizable}
35+
data-selectable={selectable}
36+
data-show-all-events={showAllEvents}
37+
data-events-count={events?.length ?? 0}
38+
data-step={step}
39+
data-timeslots={timeslots}
40+
{...domProps}
41+
>
42+
{children}
43+
</div>
44+
);
45+
},
4446
dateFnsLocalizer: () => ({
4547
format: jest.fn(),
4648
parse: jest.fn(),
@@ -58,7 +60,7 @@ jest.mock("react-big-calendar", () => {
5860
});
5961

6062
jest.mock("react-big-calendar/lib/addons/dragAndDrop", () => {
61-
return jest.fn((Component: any) => Component);
63+
return jest.fn(Component => Component);
6264
});
6365

6466
const customViewProps: CalendarContainerProps = {
@@ -97,11 +99,6 @@ const customViewProps: CalendarContainerProps = {
9799
topBarDateFormat: undefined
98100
};
99101

100-
const standardViewProps: CalendarContainerProps = {
101-
...customViewProps,
102-
view: "standard"
103-
};
104-
105102
beforeAll(() => {
106103
jest.useFakeTimers();
107104
jest.setSystemTime(new Date("2025-04-28T12:00:00Z"));
@@ -123,19 +120,78 @@ describe("Calendar", () => {
123120
expect(container.querySelector(".calendar-class")).toBeTruthy();
124121
});
125122

126-
it("does not render custom view button in standard view", () => {
127-
const { container } = render(<MxCalendar {...standardViewProps} />);
128-
expect(container).toBeTruthy();
129-
// Since we're mocking the calendar, we can't test for specific text content
130-
// but we can verify the component renders without errors
131-
});
132-
133123
it("passes step and timeslots to the calendar", () => {
134124
const { getByTestId } = render(<MxCalendar {...customViewProps} />);
135125
const calendar = getByTestId("mock-calendar");
136126
expect(calendar.getAttribute("data-step")).toBe("60");
137127
expect(calendar.getAttribute("data-timeslots")).toBe("2");
138128
});
129+
130+
it("renders loading bar when startDateAttribute is loading", () => {
131+
const props = {
132+
...customViewProps,
133+
startDateAttribute: {
134+
status: "loading"
135+
} as any
136+
};
137+
138+
const { container } = render(<MxCalendar {...props} />);
139+
140+
expect(container.querySelector(".widget-calendar-loading-bar")).toBeTruthy();
141+
expect(container.querySelector("progress.widget-calendar-loading-bar")).toBeTruthy();
142+
expect(screen.queryByTestId("mock-calendar")).toBeFalsy();
143+
});
144+
145+
it("renders calendar when startDateAttribute is available", () => {
146+
const props = {
147+
...customViewProps,
148+
startDateAttribute: {
149+
status: "available",
150+
value: new Date("2025-05-01T00:00:00.000Z")
151+
} as any
152+
};
153+
154+
render(<MxCalendar {...props} />);
155+
156+
expect(screen.getByTestId("mock-calendar")).toBeTruthy();
157+
expect(screen.queryByRole("progressbar")).toBeFalsy();
158+
});
159+
160+
it("renders calendar when startDateAttribute is unavailable", () => {
161+
const props = {
162+
...customViewProps,
163+
startDateAttribute: {
164+
status: "unavailable"
165+
} as any
166+
};
167+
168+
render(<MxCalendar {...props} />);
169+
170+
expect(screen.getByTestId("mock-calendar")).toBeTruthy();
171+
expect(screen.queryByRole("progressbar")).toBeFalsy();
172+
});
173+
174+
it("renders calendar when startDateAttribute is undefined", () => {
175+
render(<MxCalendar {...customViewProps} startDateAttribute={undefined} />);
176+
177+
expect(screen.getByTestId("mock-calendar")).toBeTruthy();
178+
expect(screen.queryByRole("progressbar")).toBeFalsy();
179+
});
180+
181+
it("passes defaultDate from startDateAttribute value", () => {
182+
const defaultDate = new Date("2025-06-10T08:30:00.000Z");
183+
const props = {
184+
...customViewProps,
185+
startDateAttribute: {
186+
status: "available",
187+
value: defaultDate
188+
} as any
189+
};
190+
191+
render(<MxCalendar {...props} />);
192+
193+
expect(screen.getByTestId("mock-calendar").getAttribute("data-default-date")).toBe("2025-06-10T08:30:00.000Z");
194+
});
139195
});
140196

141197
describe("CalendarPropsBuilder validation", () => {
@@ -147,7 +203,10 @@ describe("CalendarPropsBuilder validation", () => {
147203
messages: {}
148204
} as any;
149205

150-
const buildWithStepTimeslots = (step: number, timeslots: number) => {
206+
const buildWithStepTimeslots = (
207+
step: number,
208+
timeslots: number
209+
): ReturnType<typeof CalendarPropsBuilder.prototype.build> => {
151210
const props = { ...customViewProps, step, timeslots };
152211
const builder = new CalendarPropsBuilder(props);
153212
return builder.build(mockLocalizer, "en");

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
@@ -10,8 +10,6 @@ exports[`Calendar renders correctly with basic props 1`] = `
1010
data-culture="en-US"
1111
data-default-view="day"
1212
data-events-count="0"
13-
data-max="2025-04-28T23:59:59.000Z"
14-
data-min="2025-04-28T00:00:00.000Z"
1513
data-resizable="true"
1614
data-selectable="true"
1715
data-show-all-events="true"
@@ -20,7 +18,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
2018
data-timeslots="2"
2119
formats="[object Object]"
2220
localizer="[object Object]"
21+
max="Mon Apr 28 2025 23:59:59 GMT+0000 (Coordinated Universal Time)"
2322
messages="[object Object]"
23+
min="Mon Apr 28 2025 00:00:00 GMT+0000 (Coordinated Universal Time)"
2424
views="[object Object]"
2525
/>
2626
</div>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class CalendarPropsBuilder {
1616
private toolbarItems?: ResolvedToolbarItem[];
1717
private step: number;
1818
private timeSlots: number;
19+
private defaultDate?: Date;
1920

2021
constructor(private props: CalendarContainerProps) {
2122
this.isCustomView = props.view === "custom";
@@ -36,13 +37,15 @@ export class CalendarPropsBuilder {
3637
`[Calendar] timeslots value ${props.timeslots} was clamped to ${this.timeSlots}. Must be between 1 and 4.`
3738
);
3839
}
40+
this.defaultDate = props.startDateAttribute?.value;
3941
}
4042

4143
updateProps(props: CalendarContainerProps): void {
4244
// Update the props object, skipping props that are static (on construction only)
4345
this.props = props;
4446
this.events = this.buildEvents(props.databaseDataSource?.items ?? []);
4547
this.toolbarItems = this.buildToolbarItems();
48+
this.defaultDate = props.startDateAttribute?.value;
4649
}
4750

4851
build(localizer: DateLocalizer, culture: string): DragAndDropCalendarProps<CalendarEvent> {
@@ -86,7 +89,8 @@ export class CalendarPropsBuilder {
8689
min: this.minTime,
8790
max: this.maxTime,
8891
step: this.step,
89-
timeslots: this.timeSlots
92+
timeslots: this.timeSlots,
93+
...(this.defaultDate ? { defaultDate: this.defaultDate } : {})
9094
};
9195
}
9296

packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@use "sass:color";
2+
$brand-primary: #264ae5 !default;
23

34
.widget-calendar {
45
$cal-form-group-margin-bottom: 15px !default;
@@ -79,4 +80,75 @@
7980
.rbc-event {
8081
background-color: var(--brand-primary, $cal-brand-primary);
8182
}
83+
84+
&-loading-bar {
85+
-webkit-appearance: none;
86+
-moz-appearance: none;
87+
appearance: none;
88+
background-color: var(--border-color-default, #ced0d3);
89+
border: none;
90+
border-radius: 2px;
91+
color: var(--brand-primary, $brand-primary);
92+
height: 4px;
93+
width: 100%;
94+
95+
&::-webkit-progress-bar {
96+
background-color: transparent;
97+
}
98+
99+
&::-webkit-progress-value {
100+
background-color: currentColor;
101+
transition: all 0.2s;
102+
}
103+
104+
&::-moz-progress-bar {
105+
background-color: currentColor;
106+
transition: all 0.2s;
107+
}
108+
109+
&::-ms-fill {
110+
border: none;
111+
background-color: currentColor;
112+
transition: all 0.2s;
113+
}
114+
115+
&:indeterminate {
116+
background-size: 200% 100%;
117+
background-image: linear-gradient(
118+
to right,
119+
transparent 50%,
120+
currentColor 50%,
121+
currentColor 60%,
122+
transparent 60%,
123+
transparent 71.5%,
124+
currentColor 71.5%,
125+
currentColor 84%,
126+
transparent 84%
127+
);
128+
animation: progress-linear 3s infinite linear;
129+
}
130+
131+
&:indeterminate::-moz-progress-bar {
132+
background-color: transparent;
133+
}
134+
135+
&:indeterminate::-ms-fill {
136+
animation-name: none;
137+
}
138+
139+
@keyframes progress-linear {
140+
0% {
141+
background-size: 200% 100%;
142+
background-position: left -31.25% top 0%;
143+
}
144+
50% {
145+
background-size: 800% 100%;
146+
background-position: left -49% top 0%;
147+
}
148+
100% {
149+
background-size: 400% 100%;
150+
background-position: left -102% top 0%;
151+
}
152+
}
153+
}
82154
}

0 commit comments

Comments
 (0)