diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 0cd2731f37..91f702aa14 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -32,6 +32,23 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P hidePropertiesIn(defaultProperties, values, ["maxHeight", "overflowY"]); } + // Hide custom week range properties when view is set to 'standard' + if (values.view === "standard") { + hidePropertiesIn(defaultProperties, values, [ + "defaultViewCustom", + "showSunday", + "showMonday", + "showTuesday", + "showWednesday", + "showThursday", + "showFriday", + "showSaturday", + "customViewCaption" + ]); + } else { + hidePropertyIn(defaultProperties, values, "defaultViewStandard"); + } + // Show/hide title properties based on selection if (values.titleType === "attribute") { hidePropertyIn(defaultProperties, values, "titleExpression"); diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx index 06cf94cf31..f80ee07afe 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx @@ -1,7 +1,7 @@ import classnames from "classnames"; import * as dateFns from "date-fns"; import { ReactElement, createElement } from "react"; -import { Calendar, dateFnsLocalizer } from "react-big-calendar"; +import { Calendar, dateFnsLocalizer, EventPropGetter } from "react-big-calendar"; import { CalendarPreviewProps } from "../typings/CalendarProps"; import { CustomToolbar } from "./components/Toolbar"; import { constructWrapperStyle, WrapperStyleProps } from "./utils/style-utils"; @@ -73,15 +73,19 @@ export function preview(props: CalendarPreviewProps): ReactElement { const { class: className } = props; const wrapperStyle = constructWrapperStyle(props as WrapperStyleProps); + // Cast eventPropGetter to satisfy preview Calendar generic + const previewEventPropGetter = eventPropGetter as unknown as EventPropGetter<(typeof events)[0]>; + return (
); diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index 87461d416e..87732cee89 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -83,15 +83,24 @@ Enable create - + Initial selected view - Work week and agenda are only available in custom views + The default view to show when the calendar is loaded + + Day + Week + Month + Custom + Agenda + + + + Initial selected view + The default view to show when the calendar is loaded Day Week Month - (Work week) - (Agenda) @@ -101,6 +110,52 @@ + + Day start hour + The hour at which the day view starts (0-23) + + + Day end hour + The hour at which the day view ends (1-24) + + + + Custom view caption + Label used for the custom work-week button and title. Defaults to "Custom". + + Custom + + + + + + Monday + Show Monday in the custom work-week view + + + Tuesday + Show Tuesday in the custom work-week view + + + Wednesday + Show Wednesday in the custom work-week view + + + Thursday + Show Thursday in the custom work-week view + + + Friday + Show Friday in the custom work-week view + + + Sunday + Show Sunday in the custom work-week view + + + Saturday + Show Saturday in the custom work-week view + @@ -111,7 +166,7 @@ - + On click action @@ -130,7 +185,7 @@ - + On change action The change event is triggered on moving/dragging an item or changing the start or end time of by resizing an item @@ -214,6 +269,10 @@ Hidden + + Show all events + Auto-adjust calendar height to display all events without "more" links + diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index b602843a21..a7ba70143a 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -4,25 +4,41 @@ import { ListValueBuilder } from "@mendix/widget-plugin-test-utils"; import Calendar from "../Calendar"; import { CalendarContainerProps } from "../../typings/CalendarProps"; -const defaultProps: CalendarContainerProps = { +const customViewProps: CalendarContainerProps = { name: "calendar-test", class: "calendar-class", tabIndex: 0, databaseDataSource: new ListValueBuilder().withItems([]).build(), titleType: "attribute", - view: "standard", - defaultView: "month", + view: "custom", + defaultViewStandard: "month", + defaultViewCustom: "work_week", editable: "default", enableCreate: true, widthUnit: "percentage", width: 100, heightUnit: "pixels", height: 400, + minHour: 0, + maxHour: 24, minHeightUnit: "pixels", minHeight: 400, maxHeightUnit: "none", maxHeight: 400, - overflowY: "auto" + overflowY: "auto", + showSunday: false, + showMonday: true, + showTuesday: true, + showWednesday: true, + showThursday: true, + showFriday: true, + showSaturday: false, + showAllEvents: true +}; + +const standardViewProps: CalendarContainerProps = { + ...customViewProps, + view: "standard" }; beforeAll(() => { @@ -36,13 +52,18 @@ afterAll(() => { describe("Calendar", () => { it("renders correctly with basic props", () => { - const calendar = render(); + const calendar = render(); expect(calendar).toMatchSnapshot(); }); it("renders with correct class name", () => { - const { container } = render(); + const { container } = render(); expect(container.querySelector(".widget-calendar")).toBeTruthy(); expect(container.querySelector(".calendar-class")).toBeTruthy(); }); + + it("does not render custom view button in standard view", () => { + const { queryByText } = render(); + expect(queryByText("Custom")).toBeNull(); + }); }); 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 9120b741de..82e0a6328f 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 @@ -49,7 +49,7 @@ exports[`Calendar renders correctly with basic props 1`] = ` - April 2025 + Apr 28 – May 02
+ +
-
- - Sun - -
-
- - Mon - -
-
- - Tue - -
-
- - Wed - -
-
- - Thu - -
-
- - Fri - -
-
- - Sat - -
-
-
-
-
-
-
-
-
-
-
+ class="rbc-label rbc-time-header-gutter" + />
+
+
- +
+
+
+
+
- +
+
+
-
-
-
-
-
-
-
-
-
+ class="rbc-time-gutter rbc-time-column" + />
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
+
- -
+ class="rbc-events-container" + />
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
+
- -
+ class="rbc-events-container" + />
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
+ class="rbc-events-container" + />
-
@@ -800,7 +321,7 @@ exports[`Calendar renders correctly with basic props 1`] = ` - April 2025 + Apr 28 – May 02
+ +
-
- - Sun - -
-
- - Mon - -
-
- - Tue - -
-
- - Wed - -
-
- - Thu - -
-
- - Fri - -
-
- - Sat - -
-
-
-
-
-
-
-
-
-
-
+ class="rbc-label rbc-time-header-gutter" + />
+
+
- +
+
+
+
+
- +
+
+
-
-
-
-
-
-
-
-
-
+ class="rbc-time-gutter rbc-time-column" + />
-
-
- -
-
- -
-
- -
+
- -
-
- -
-
- -
-
- -
+ class="rbc-events-container" + />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
+
- -
-
- -
-
- -
-
- -
-
- -
+ class="rbc-events-container" + />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- -
+
- -
-
- -
-
- -
-
- -
-
- -
+ class="rbc-events-container" + />
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
- -
+
- -
-
- -
-
- -
-
- -
-
- -
-
- -
+ class="rbc-events-container" + />
-
diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index d9017e5f47..447d1adf86 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -1,19 +1,23 @@ import * as dateFns from "date-fns"; -import { Calendar, CalendarProps, dateFnsLocalizer, ViewsProps } from "react-big-calendar"; +import { ObjectItem } from "mendix"; +import { Calendar, CalendarProps, dateFnsLocalizer, NavigateAction, ViewsProps } from "react-big-calendar"; import withDragAndDrop, { withDragAndDropProps } from "react-big-calendar/lib/addons/dragAndDrop"; import { CalendarContainerProps } from "../../typings/CalendarProps"; import { CustomToolbar } from "../components/Toolbar"; +import { createElement, ReactElement } from "react"; +// @ts-expect-error - TimeGrid is not part of public typings +import TimeGrid from "react-big-calendar/lib/TimeGrid"; import "react-big-calendar/lib/addons/dragAndDrop/styles.css"; import "react-big-calendar/lib/css/react-big-calendar.css"; -// Define the event shape export interface CalEvent { title: string; start: Date; end: Date; allDay: boolean; color?: string; + item: ObjectItem; } // Configure date-fns localizer @@ -63,6 +67,82 @@ interface DragAndDropCalendarProps {} export function extractCalendarProps(props: CalendarContainerProps): DragAndDropCalendarProps { + const isCustomView = props.view === "custom"; + const defaultView = isCustomView ? props.defaultViewCustom : props.defaultViewStandard; + const customCaption: string = props.customViewCaption?.value ?? "Custom"; + const visibleSet = new Set(); + const dayProps = [ + { prop: props.showSunday, day: 0 }, + { prop: props.showMonday, day: 1 }, + { prop: props.showTuesday, day: 2 }, + { prop: props.showWednesday, day: 3 }, + { prop: props.showThursday, day: 4 }, + { prop: props.showFriday, day: 5 }, + { prop: props.showSaturday, day: 6 } + ]; + + dayProps.forEach(({ prop, day }) => { + if (prop) visibleSet.add(day); + }); + + function customRange(date: Date): Date[] { + const startOfWeekDate = dateFns.startOfWeek(date, { weekStartsOn: 0 }); + const range: Date[] = []; + for (let i = 0; i < 7; i++) { + const current = dateFns.addDays(startOfWeekDate, i); + if (visibleSet.has(current.getDay())) { + range.push(current); + } + } + return range; + } + + // Custom work-week view component based on TimeGrid + const CustomWeek = (viewProps: CalendarProps): ReactElement => { + const { date } = viewProps; + const range = customRange(date as Date); + + return createElement(TimeGrid as any, { ...viewProps, range, eventOffset: 15 }); + }; + + CustomWeek.range = customRange; + CustomWeek.navigate = (date: Date, action: NavigateAction): Date => { + switch (action) { + case "PREV": + return dateFns.addWeeks(date, -1); + case "NEXT": + return dateFns.addWeeks(date, 1); + default: + return date; + } + }; + + CustomWeek.title = (date: Date, options: any): string => { + const loc = options?.localizer ?? { + // Fallback localizer (EN) + format: (d: Date, _fmt: string) => d.toLocaleDateString(undefined, { month: "short", day: "2-digit" }) + }; + + const range = customRange(date); + + // Determine if the dates are contiguous (difference of 1 day between successive dates) + const isContiguous = range.every( + (curr, idx, arr) => idx === 0 || dateFns.differenceInCalendarDays(curr, arr[idx - 1]) === 1 + ); + + if (isContiguous) { + // Keep default first–last representation (e.g. "Mar 11 – Mar 15") + const first = range[0]; + const last = range[range.length - 1]; + return `${loc.format(first, "MMM dd")} – ${loc.format(last, "MMM dd")}`; + } + + // Non-contiguous selection → list individual weekday names (Mon, Wed, Fri) + const weekdayList = range.map(d => loc.format(d, "EEE")).join(", "); + + return weekdayList; + }; + const items = props.databaseDataSource?.items ?? []; const events: CalEvent[] = items.map(item => { const title = @@ -75,15 +155,22 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop const end = props.endAttribute?.get(item).value ?? start; const allDay = props.allDayAttribute?.get(item).value ?? false; const color = props.eventColor?.get(item).value; - return { title, start, end, allDay, color }; + return { title, start, end, allDay, color, item }; }); - const viewsOption: ViewsProps = - props.view === "standard" ? ["day", "week", "month"] : ["month", "week", "work_week", "day", "agenda"]; + const viewsOption: ViewsProps = isCustomView + ? { day: true, week: true, month: true, work_week: CustomWeek, agenda: true } + : { day: true, week: true, month: true }; + + // Compute minimum and maximum times for the day based on configured hours + const minTime = new Date(); + minTime.setHours(props.minHour ?? 0, 0, 0, 0); + const maxTime = new Date(); + maxTime.setHours(props.maxHour ?? 24, 0, 0, 0); const handleSelectEvent = (event: CalEvent): void => { - if (props.onClickEvent?.canExecute) { - props.onClickEvent.execute({ + if (props.onClickEvent?.get(event.item).canExecute) { + props.onClickEvent.get(event.item).execute({ startDate: event.start, endDate: event.end, allDay: event.allDay, @@ -103,8 +190,8 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop }; const handleEventDropOrResize = ({ event, start, end }: { event: CalEvent; start: Date; end: Date }): void => { - if (props.onChange?.canExecute) { - props.onChange.execute({ + if (props.onChange?.get(event.item).canExecute) { + props.onChange.get(event.item).execute({ oldStart: event.start, oldEnd: event.end, newStart: start, @@ -113,7 +200,7 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop } }; - const handleRangeChange = (date: Date, view: string): void => { + const handleRangeChange = (date: Date, view: string, _action: NavigateAction): void => { if (props.onRangeChange?.canExecute) { const { start, end } = getViewRange(view, date); props.onRangeChange.execute({ @@ -128,7 +215,10 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop components: { toolbar: CustomToolbar }, - defaultView: props.defaultView, + defaultView, + messages: { + work_week: customCaption + }, events, localizer, resizable: props.editable !== "never", @@ -143,6 +233,9 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop onSelectEvent: handleSelectEvent, onSelectSlot: handleSelectSlot, startAccessor: (event: CalEvent) => event.start, - titleAccessor: (event: CalEvent) => event.title + titleAccessor: (event: CalEvent) => event.title, + showAllEvents: props.showAllEvents, + min: minTime, + max: maxTime }; } diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index c29cf16cfb..ca3c7df14e 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, EditableValue, ListValue, Option, ListAttributeValue, ListExpressionValue } from "mendix"; +import { ActionValue, DynamicValue, EditableValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix"; export type TitleTypeEnum = "attribute" | "expression"; @@ -12,7 +12,9 @@ export type ViewEnum = "standard" | "custom"; export type EditableEnum = "default" | "never"; -export type DefaultViewEnum = "day" | "week" | "month" | "work_week" | "agenda"; +export type DefaultViewCustomEnum = "day" | "week" | "month" | "work_week" | "agenda"; + +export type DefaultViewStandardEnum = "day" | "week" | "month"; export type WidthUnitEnum = "pixels" | "percentage"; @@ -40,12 +42,23 @@ export interface CalendarContainerProps { view: ViewEnum; editable: EditableEnum; enableCreate: boolean; - defaultView: DefaultViewEnum; + defaultViewCustom: DefaultViewCustomEnum; + defaultViewStandard: DefaultViewStandardEnum; startDateAttribute?: EditableValue; + minHour: number; + maxHour: number; + customViewCaption?: DynamicValue; + showMonday: boolean; + showTuesday: boolean; + showWednesday: boolean; + showThursday: boolean; + showFriday: boolean; + showSunday: boolean; + showSaturday: boolean; eventDataAttribute?: EditableValue; - onClickEvent?: ActionValue<{ startDate: Option; endDate: Option; allDay: Option; title: Option }>; + onClickEvent?: ListActionValue<{ startDate: Option; endDate: Option; allDay: Option; title: Option }>; onCreateEvent?: ActionValue<{ startDate: Option; endDate: Option; allDay: Option }>; - onChange?: ActionValue<{ oldStart: Option; oldEnd: Option; newStart: Option; newEnd: Option }>; + onChange?: ListActionValue<{ oldStart: Option; oldEnd: Option; newStart: Option; newEnd: Option }>; onRangeChange?: ActionValue<{ rangeStart: Option; rangeEnd: Option; currentView: Option }>; widthUnit: WidthUnitEnum; width: number; @@ -56,6 +69,7 @@ export interface CalendarContainerProps { maxHeightUnit: MaxHeightUnitEnum; maxHeight: number; overflowY: OverflowYEnum; + showAllEvents: boolean; } export interface CalendarPreviewProps { @@ -80,8 +94,19 @@ export interface CalendarPreviewProps { view: ViewEnum; editable: EditableEnum; enableCreate: boolean; - defaultView: DefaultViewEnum; + defaultViewCustom: DefaultViewCustomEnum; + defaultViewStandard: DefaultViewStandardEnum; startDateAttribute: string; + minHour: number | null; + maxHour: number | null; + customViewCaption: string; + showMonday: boolean; + showTuesday: boolean; + showWednesday: boolean; + showThursday: boolean; + showFriday: boolean; + showSunday: boolean; + showSaturday: boolean; eventDataAttribute: string; onClickEvent: {} | null; onCreateEvent: {} | null; @@ -96,4 +121,5 @@ export interface CalendarPreviewProps { maxHeightUnit: MaxHeightUnitEnum; maxHeight: number | null; overflowY: OverflowYEnum; + showAllEvents: boolean; }