diff --git a/packages/pluggableWidgets/calendar-web/CHANGELOG.md b/packages/pluggableWidgets/calendar-web/CHANGELOG.md index 4bd6ccc4ac..a2538870ad 100644 --- a/packages/pluggableWidgets/calendar-web/CHANGELOG.md +++ b/packages/pluggableWidgets/calendar-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- Improved handling of the start date attribute to ensure correct calendar initialization. + ## [2.3.0] - 2026-02-17 ### Added diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx index a7d8d31d52..878bb94fb4 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useMemo } from "react"; +import { Fragment, ReactElement, useMemo } from "react"; import classNames from "classnames"; import { CalendarContainerProps } from "../typings/CalendarProps"; import { CalendarPropsBuilder } from "./helpers/CalendarPropsBuilder"; @@ -26,9 +26,16 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement }, [props, calendarController, localizer, culture]); const calendarEvents = useCalendarEvents(props); + return ( -
- -
+ + {props.startDateAttribute?.status === "loading" ? ( + + ) : ( +
+ +
+ )} +
); } diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index 0af2436b56..25e9204adf 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -62,13 +62,6 @@ - - Start date attribute - The start date that should be shown in the view - - - - @@ -139,6 +132,13 @@ Time slots 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 + + Start date attribute + The DateTime attribute used on initial load + + + + diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index 5898ddd82e..6174878704 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -1,4 +1,4 @@ -import { render } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils"; import MxCalendar from "../Calendar"; @@ -10,37 +10,39 @@ jest.mock("react-big-calendar", () => { const originalModule = jest.requireActual("react-big-calendar"); return { ...originalModule, - Calendar: ({ - children, - defaultView, - culture, - resizable, - selectable, - showAllEvents, - min, - max, - events, - step, - timeslots, - ...domProps - }: any) => ( -
- {children} -
- ), + Calendar: (mockProps: any) => { + const { + children, + defaultView, + defaultDate, + culture, + resizable, + selectable, + showAllEvents, + events, + step, + timeslots, + ...domProps + } = mockProps; + + return ( +
+ {children} +
+ ); + }, dateFnsLocalizer: () => ({ format: jest.fn(), parse: jest.fn(), @@ -58,7 +60,7 @@ jest.mock("react-big-calendar", () => { }); jest.mock("react-big-calendar/lib/addons/dragAndDrop", () => { - return jest.fn((Component: any) => Component); + return jest.fn(Component => Component); }); const customViewProps: CalendarContainerProps = { @@ -97,11 +99,6 @@ const customViewProps: CalendarContainerProps = { topBarDateFormat: undefined }; -const standardViewProps: CalendarContainerProps = { - ...customViewProps, - view: "standard" -}; - beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(new Date("2025-04-28T12:00:00Z")); @@ -123,19 +120,78 @@ describe("Calendar", () => { expect(container.querySelector(".calendar-class")).toBeTruthy(); }); - it("does not render custom view button in standard view", () => { - const { container } = render(); - expect(container).toBeTruthy(); - // Since we're mocking the calendar, we can't test for specific text content - // but we can verify the component renders without errors - }); - it("passes step and timeslots to the calendar", () => { const { getByTestId } = render(); const calendar = getByTestId("mock-calendar"); expect(calendar.getAttribute("data-step")).toBe("60"); expect(calendar.getAttribute("data-timeslots")).toBe("2"); }); + + it("renders loading bar when startDateAttribute is loading", () => { + const props = { + ...customViewProps, + startDateAttribute: { + status: "loading" + } as any + }; + + const { container } = render(); + + expect(container.querySelector(".widget-calendar-loading-bar")).toBeTruthy(); + expect(container.querySelector("progress.widget-calendar-loading-bar")).toBeTruthy(); + expect(screen.queryByTestId("mock-calendar")).toBeFalsy(); + }); + + it("renders calendar when startDateAttribute is available", () => { + const props = { + ...customViewProps, + startDateAttribute: { + status: "available", + value: new Date("2025-05-01T00:00:00.000Z") + } as any + }; + + render(); + + expect(screen.getByTestId("mock-calendar")).toBeTruthy(); + expect(screen.queryByRole("progressbar")).toBeFalsy(); + }); + + it("renders calendar when startDateAttribute is unavailable", () => { + const props = { + ...customViewProps, + startDateAttribute: { + status: "unavailable" + } as any + }; + + render(); + + expect(screen.getByTestId("mock-calendar")).toBeTruthy(); + expect(screen.queryByRole("progressbar")).toBeFalsy(); + }); + + it("renders calendar when startDateAttribute is undefined", () => { + render(); + + expect(screen.getByTestId("mock-calendar")).toBeTruthy(); + expect(screen.queryByRole("progressbar")).toBeFalsy(); + }); + + it("passes defaultDate from startDateAttribute value", () => { + const defaultDate = new Date("2025-06-10T08:30:00.000Z"); + const props = { + ...customViewProps, + startDateAttribute: { + status: "available", + value: defaultDate + } as any + }; + + render(); + + expect(screen.getByTestId("mock-calendar").getAttribute("data-default-date")).toBe("2025-06-10T08:30:00.000Z"); + }); }); describe("CalendarPropsBuilder validation", () => { @@ -147,7 +203,10 @@ describe("CalendarPropsBuilder validation", () => { messages: {} } as any; - const buildWithStepTimeslots = (step: number, timeslots: number) => { + const buildWithStepTimeslots = ( + step: number, + timeslots: number + ): ReturnType => { const props = { ...customViewProps, step, timeslots }; const builder = new CalendarPropsBuilder(props); return builder.build(mockLocalizer, "en"); diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap b/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap index 53096ea284..941e99e4ec 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/__snapshots__/Calendar.spec.tsx.snap @@ -10,8 +10,6 @@ exports[`Calendar renders correctly with basic props 1`] = ` data-culture="en-US" data-default-view="day" data-events-count="0" - data-max="2025-04-28T23:59:59.000Z" - data-min="2025-04-28T00:00:00.000Z" data-resizable="true" data-selectable="true" data-show-all-events="true" @@ -20,7 +18,9 @@ exports[`Calendar renders correctly with basic props 1`] = ` data-timeslots="2" formats="[object Object]" localizer="[object Object]" + max="Mon Apr 28 2025 23:59:59 GMT+0000 (Coordinated Universal Time)" messages="[object Object]" + min="Mon Apr 28 2025 00:00:00 GMT+0000 (Coordinated Universal Time)" views="[object Object]" /> diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 6d3c119b8e..d6176e11a6 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -16,6 +16,7 @@ export class CalendarPropsBuilder { private toolbarItems?: ResolvedToolbarItem[]; private step: number; private timeSlots: number; + private defaultDate?: Date; constructor(private props: CalendarContainerProps) { this.isCustomView = props.view === "custom"; @@ -36,6 +37,7 @@ export class CalendarPropsBuilder { `[Calendar] timeslots value ${props.timeslots} was clamped to ${this.timeSlots}. Must be between 1 and 4.` ); } + this.defaultDate = props.startDateAttribute?.value; } updateProps(props: CalendarContainerProps): void { @@ -43,6 +45,7 @@ export class CalendarPropsBuilder { this.props = props; this.events = this.buildEvents(props.databaseDataSource?.items ?? []); this.toolbarItems = this.buildToolbarItems(); + this.defaultDate = props.startDateAttribute?.value; } build(localizer: DateLocalizer, culture: string): DragAndDropCalendarProps { @@ -86,7 +89,8 @@ export class CalendarPropsBuilder { min: this.minTime, max: this.maxTime, step: this.step, - timeslots: this.timeSlots + timeslots: this.timeSlots, + ...(this.defaultDate ? { defaultDate: this.defaultDate } : {}) }; } diff --git a/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss b/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss index d324b682cd..72181b73be 100644 --- a/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss +++ b/packages/pluggableWidgets/calendar-web/src/ui/Calendar.scss @@ -1,4 +1,5 @@ @use "sass:color"; +$brand-primary: #264ae5 !default; .widget-calendar { $cal-form-group-margin-bottom: 15px !default; @@ -79,4 +80,75 @@ .rbc-event { background-color: var(--brand-primary, $cal-brand-primary); } + + &-loading-bar { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: var(--border-color-default, #ced0d3); + border: none; + border-radius: 2px; + color: var(--brand-primary, $brand-primary); + height: 4px; + width: 100%; + + &::-webkit-progress-bar { + background-color: transparent; + } + + &::-webkit-progress-value { + background-color: currentColor; + transition: all 0.2s; + } + + &::-moz-progress-bar { + background-color: currentColor; + transition: all 0.2s; + } + + &::-ms-fill { + border: none; + background-color: currentColor; + transition: all 0.2s; + } + + &:indeterminate { + background-size: 200% 100%; + background-image: linear-gradient( + to right, + transparent 50%, + currentColor 50%, + currentColor 60%, + transparent 60%, + transparent 71.5%, + currentColor 71.5%, + currentColor 84%, + transparent 84% + ); + animation: progress-linear 3s infinite linear; + } + + &:indeterminate::-moz-progress-bar { + background-color: transparent; + } + + &:indeterminate::-ms-fill { + animation-name: none; + } + + @keyframes progress-linear { + 0% { + background-size: 200% 100%; + background-position: left -31.25% top 0%; + } + 50% { + background-size: 800% 100%; + background-position: left -49% top 0%; + } + 100% { + background-size: 400% 100%; + background-position: left -102% top 0%; + } + } + } } diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index b0a1628d5e..323b344cff 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -4,7 +4,7 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { ActionValue, DynamicValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix"; +import { ActionValue, DynamicValue, EditableValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix"; export type TitleTypeEnum = "attribute" | "expression"; @@ -79,7 +79,6 @@ export interface CalendarContainerProps { startAttribute?: ListAttributeValue; endAttribute?: ListAttributeValue; eventColor?: ListAttributeValue; - startDateAttribute?: ListAttributeValue; editable: DynamicValue; view: ViewEnum; defaultViewStandard: DefaultViewStandardEnum; @@ -92,6 +91,7 @@ export interface CalendarContainerProps { showAllEvents: boolean; step: number; timeslots: number; + startDateAttribute?: EditableValue; toolbarItems: ToolbarItemsType[]; customViewShowMonday: boolean; customViewShowTuesday: boolean; @@ -134,7 +134,6 @@ export interface CalendarPreviewProps { startAttribute: string; endAttribute: string; eventColor: string; - startDateAttribute: string; editable: string; view: ViewEnum; defaultViewStandard: DefaultViewStandardEnum; @@ -147,6 +146,7 @@ export interface CalendarPreviewProps { showAllEvents: boolean; step: number | null; timeslots: number | null; + startDateAttribute: string; toolbarItems: ToolbarItemsPreviewType[]; customViewShowMonday: boolean; customViewShowTuesday: boolean;