From be43d5e4d49bd02ef4de33904500b2153ac6ebb5 Mon Sep 17 00:00:00 2001 From: Rahman Unver Date: Fri, 13 Jun 2025 13:47:59 +0200 Subject: [PATCH 01/14] feat(calendar-web): add hour scope for the day view --- .../pluggableWidgets/calendar-web/src/Calendar.xml | 8 ++++++++ .../calendar-web/src/utils/calendar-utils.ts | 10 +++++++++- .../calendar-web/typings/CalendarProps.d.ts | 4 ++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index fc7da33603..2177fc6bc3 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -105,6 +105,14 @@ + + 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) + diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 31a3d7c927..2eaca61f6a 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -81,6 +81,12 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop const viewsOption: ViewsProps = props.view === "standard" ? ["day", "week", "month"] : ["month", "week", "work_week", "day", "agenda"]; + // 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({ @@ -146,6 +152,8 @@ 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, + 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 3b51c3846d..16f86547eb 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -43,6 +43,8 @@ export interface CalendarContainerProps { showEventDate: boolean; defaultView: DefaultViewEnum; startDateAttribute?: EditableValue; + minHour: number; + maxHour: number; eventDataAttribute?: EditableValue; onClickEvent?: ActionValue<{ startDate: Option; endDate: Option; allDay: Option; title: Option }>; onCreateEvent?: ActionValue<{ startDate: Option; endDate: Option; allDay: Option }>; @@ -84,6 +86,8 @@ export interface CalendarPreviewProps { showEventDate: boolean; defaultView: DefaultViewEnum; startDateAttribute: string; + minHour: number | null; + maxHour: number | null; eventDataAttribute: string; onClickEvent: {} | null; onCreateEvent: {} | null; From 49feedeb4b10289b801f7b9062e6f919ab882763 Mon Sep 17 00:00:00 2001 From: Rahman Unver Date: Fri, 13 Jun 2025 15:55:13 +0200 Subject: [PATCH 02/14] feat(calendar-web): add showAllEvents prop for month view --- packages/pluggableWidgets/calendar-web/src/Calendar.xml | 4 ++++ .../pluggableWidgets/calendar-web/src/utils/calendar-utils.ts | 1 + .../pluggableWidgets/calendar-web/typings/CalendarProps.d.ts | 2 ++ 3 files changed, 7 insertions(+) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index 2177fc6bc3..49abbde2a3 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -226,6 +226,10 @@ Hidden + + Show all events + Auto-adjust calendar height to display all events without "more" links + diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 2eaca61f6a..86ea5a937a 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -153,6 +153,7 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop onSelectSlot: handleSelectSlot, startAccessor: (event: CalEvent) => event.start, 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 16f86547eb..75f11515a9 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -59,6 +59,7 @@ export interface CalendarContainerProps { maxHeightUnit: MaxHeightUnitEnum; maxHeight: number; overflowY: OverflowYEnum; + showAllEvents: boolean; } export interface CalendarPreviewProps { @@ -102,4 +103,5 @@ export interface CalendarPreviewProps { maxHeightUnit: MaxHeightUnitEnum; maxHeight: number | null; overflowY: OverflowYEnum; + showAllEvents: boolean; } From d23f18dc2064c7a7b0460c1851ab80dedd3efeec Mon Sep 17 00:00:00 2001 From: Rahman Unver Date: Tue, 17 Jun 2025 10:58:50 +0200 Subject: [PATCH 03/14] feat(calendar-web): add custom week day selection --- .../calendar-web/src/Calendar.editorConfig.ts | 13 +++ .../src/Calendar.editorPreview.tsx | 10 +- .../calendar-web/src/Calendar.xml | 47 +++++++- .../src/__tests__/Calendar.spec.tsx | 16 ++- .../calendar-web/src/utils/calendar-utils.ts | 109 ++++++++++++++++-- .../calendar-web/typings/CalendarProps.d.ts | 22 +++- 6 files changed, 195 insertions(+), 22 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 0cd2731f37..f8509ee87a 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -32,6 +32,19 @@ 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, [ + "showSunday", + "showMonday", + "showTuesday", + "showWednesday", + "showThursday", + "showFriday", + "showSaturday" + ]); + } + // 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..dd517960c5 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,6 +73,9 @@ 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 49abbde2a3..86303367d4 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -94,8 +94,8 @@ Day Week Month - (Work week) - (Agenda) + Custom + Agenda @@ -113,6 +113,45 @@ Day end hour The hour at which the day view ends (1-24) + + + Custom view caption + View + 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 + @@ -123,7 +162,7 @@ - + On click action @@ -142,7 +181,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 diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index f656c5e60a..aa0e3f4faa 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -10,20 +10,30 @@ const defaultProps: CalendarContainerProps = { tabIndex: 0, databaseDataSource: new ListValueBuilder().withItems([]).build(), titleType: "attribute", - view: "standard", - defaultView: "month", + view: "custom", + defaultView: "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", - showEventDate: true + showEventDate: true, + showSunday: false, + showMonday: true, + showTuesday: true, + showWednesday: true, + showThursday: true, + showFriday: true, + showSaturday: false, + showAllEvents: true }; beforeAll(() => { diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 86ea5a937a..6a91ae2cb0 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 visibleSet = new Set(); + // Caption for custom work week button / title + + const customCaption: string = props.customViewCaption ?? "Custom"; + 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,11 +155,19 @@ 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 }; }); + // Update button label inside localizer messages + (localizer as any).messages = { + ...localizer.messages, + work_week: customCaption + }; + const viewsOption: ViewsProps = - props.view === "standard" ? ["day", "week", "month"] : ["month", "week", "work_week", "day", "agenda"]; + props.view === "standard" + ? { day: true, week: true, month: true } + : { day: true, week: true, month: true, work_week: CustomWeek, agenda: true }; // Compute minimum and maximum times for the day based on configured hours const minTime = new Date(); @@ -88,8 +176,8 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop 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, @@ -109,8 +197,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, @@ -119,7 +207,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({ @@ -137,6 +225,9 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop toolbar: CustomToolbar }, defaultView: props.defaultView, + messages: { + work_week: customCaption + }, events, formats, localizer, diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index 75f11515a9..24e82e7a03 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, EditableValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix"; export type TitleTypeEnum = "attribute" | "expression"; @@ -45,10 +45,18 @@ export interface CalendarContainerProps { startDateAttribute?: EditableValue; minHour: number; maxHour: number; + customViewCaption?: any; + 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; @@ -89,6 +97,14 @@ export interface CalendarPreviewProps { startDateAttribute: string; minHour: number | null; maxHour: number | null; + customViewCaption: any; + showMonday: boolean; + showTuesday: boolean; + showWednesday: boolean; + showThursday: boolean; + showFriday: boolean; + showSunday: boolean; + showSaturday: boolean; eventDataAttribute: string; onClickEvent: {} | null; onCreateEvent: {} | null; From 678d4b4aab152bef51677e4b26004810b8a29d4e Mon Sep 17 00:00:00 2001 From: Rahman Unver Date: Tue, 17 Jun 2025 14:54:00 +0200 Subject: [PATCH 04/14] feat(calendar-web): split standard and custom config --- .../calendar-web/src/Calendar.editorConfig.ts | 6 +++++- .../calendar-web/src/Calendar.xml | 11 +++++++++- .../calendar-web/src/utils/calendar-utils.ts | 21 +++++++------------ .../calendar-web/typings/CalendarProps.d.ts | 10 ++++++--- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index f8509ee87a..91f702aa14 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -35,14 +35,18 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P // 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" + "showSaturday", + "customViewCaption" ]); + } else { + hidePropertyIn(defaultProperties, values, "defaultViewStandard"); } // Show/hide title properties based on selection diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index 86303367d4..dccaa17f98 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -87,7 +87,7 @@ Show event date range Show the start and end date of the event - + Initial selected view Work week and agenda are only available in custom views @@ -98,6 +98,15 @@ Agenda + + Default view + The default view to show when the calendar is loaded + + Day + Week + Month + + Start date attribute The start date that should be shown in the view diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 6a91ae2cb0..e1b19fb683 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -67,10 +67,10 @@ interface DragAndDropCalendarProps {} export function extractCalendarProps(props: CalendarContainerProps): DragAndDropCalendarProps { - const visibleSet = new Set(); - // Caption for custom work week button / title - + const isCustomView = props.view === "custom"; + const defaultView = isCustomView ? props.defaultViewCustom : props.defaultViewStandard; const customCaption: string = props.customViewCaption ?? "Custom"; + const visibleSet = new Set(); const dayProps = [ { prop: props.showSunday, day: 0 }, { prop: props.showMonday, day: 1 }, @@ -158,16 +158,9 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop return { title, start, end, allDay, color, item }; }); - // Update button label inside localizer messages - (localizer as any).messages = { - ...localizer.messages, - work_week: customCaption - }; - - const viewsOption: ViewsProps = - props.view === "standard" - ? { day: true, week: true, month: true } - : { day: true, week: true, month: true, work_week: CustomWeek, agenda: true }; + 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(); @@ -224,7 +217,7 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop components: { toolbar: CustomToolbar }, - defaultView: props.defaultView, + defaultView, messages: { work_week: customCaption }, diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index 24e82e7a03..f3cadfe31a 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -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"; @@ -41,7 +43,8 @@ export interface CalendarContainerProps { editable: EditableEnum; enableCreate: boolean; showEventDate: boolean; - defaultView: DefaultViewEnum; + defaultViewCustom: DefaultViewCustomEnum; + defaultViewStandard: DefaultViewStandardEnum; startDateAttribute?: EditableValue; minHour: number; maxHour: number; @@ -93,7 +96,8 @@ export interface CalendarPreviewProps { editable: EditableEnum; enableCreate: boolean; showEventDate: boolean; - defaultView: DefaultViewEnum; + defaultViewCustom: DefaultViewCustomEnum; + defaultViewStandard: DefaultViewStandardEnum; startDateAttribute: string; minHour: number | null; maxHour: number | null; From bdc5aba812c2e08c3d9dd705c23f57e545524f96 Mon Sep 17 00:00:00 2001 From: Rahman Unver Date: Tue, 17 Jun 2025 15:53:26 +0200 Subject: [PATCH 05/14] feat(calendar-web): editorconfig edits --- .../calendar-web/src/Calendar.editorPreview.tsx | 2 +- packages/pluggableWidgets/calendar-web/src/Calendar.xml | 7 +++---- .../calendar-web/src/__tests__/Calendar.spec.tsx | 3 ++- .../calendar-web/src/utils/calendar-utils.ts | 2 +- .../calendar-web/typings/CalendarProps.d.ts | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx index dd517960c5..f80ee07afe 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx @@ -80,7 +80,7 @@ export function preview(props: CalendarPreviewProps): ReactElement {
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 @@ -99,7 +99,7 @@ - Default view + Initial selected view The default view to show when the calendar is loaded Day @@ -123,9 +123,8 @@ The hour at which the day view ends (1-24) - + Custom view caption - View Label used for the custom work-week button and title. Defaults to "Custom". Custom diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index aa0e3f4faa..913348ed60 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -11,7 +11,8 @@ const defaultProps: CalendarContainerProps = { databaseDataSource: new ListValueBuilder().withItems([]).build(), titleType: "attribute", view: "custom", - defaultView: "work_week", + defaultViewStandard: "month", + defaultViewCustom: "work_week", editable: "default", enableCreate: true, widthUnit: "percentage", diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index e1b19fb683..82d59462f7 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -69,7 +69,7 @@ interface DragAndDropCalendarProps { const isCustomView = props.view === "custom"; const defaultView = isCustomView ? props.defaultViewCustom : props.defaultViewStandard; - const customCaption: string = props.customViewCaption ?? "Custom"; + const customCaption: string = props.customViewCaption?.value ?? "Custom"; const visibleSet = new Set(); const dayProps = [ { prop: props.showSunday, day: 0 }, diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index f3cadfe31a..c042e7c7fe 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, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix"; +import { ActionValue, DynamicValue, EditableValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix"; export type TitleTypeEnum = "attribute" | "expression"; @@ -48,7 +48,7 @@ export interface CalendarContainerProps { startDateAttribute?: EditableValue; minHour: number; maxHour: number; - customViewCaption?: any; + customViewCaption?: DynamicValue; showMonday: boolean; showTuesday: boolean; showWednesday: boolean; @@ -101,7 +101,7 @@ export interface CalendarPreviewProps { startDateAttribute: string; minHour: number | null; maxHour: number | null; - customViewCaption: any; + customViewCaption: string; showMonday: boolean; showTuesday: boolean; showWednesday: boolean; From d01b187066253a447f971105897461b942bffa91 Mon Sep 17 00:00:00 2001 From: Rahman Unver Date: Thu, 19 Jun 2025 11:41:39 +0200 Subject: [PATCH 06/14] test(calendar-web): update snapshot --- .../src/__tests__/Calendar.spec.tsx | 16 +- .../__snapshots__/Calendar.spec.tsx.snap | 1410 +++-------------- 2 files changed, 239 insertions(+), 1187 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index 913348ed60..1229a8d7cc 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -4,7 +4,7 @@ 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, @@ -37,6 +37,11 @@ const defaultProps: CalendarContainerProps = { showAllEvents: true }; +const standardViewProps: CalendarContainerProps = { + ...customViewProps, + view: "standard" +}; + beforeAll(() => { jest.useFakeTimers(); jest.setSystemTime(new Date("2025-04-28T12:00:00Z")); @@ -48,13 +53,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" + />
-
From 5d317ddf43fad5f207be590e21b48e7f15954abe Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 24 Jun 2025 17:28:48 +0200 Subject: [PATCH 07/14] fix: small text changes --- .../calendar-web/src/Calendar.editorConfig.ts | 2 +- .../pluggableWidgets/calendar-web/src/Calendar.xml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 91f702aa14..229dc2961f 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -32,7 +32,7 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P hidePropertiesIn(defaultProperties, values, ["maxHeight", "overflowY"]); } - // Hide custom week range properties when view is set to 'standard' + // Hide custom week range properties when the view is set to 'standard' if (values.view === "standard") { hidePropertiesIn(defaultProperties, values, [ "defaultViewCustom", diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index 09de16f079..8759359961 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -55,7 +55,7 @@ Color attribute - Attribute containing a valid html color eg: red #FF0000 rgb(250,10,20) rgba(10,10,10, 0.5) + Attribute containing a valid HTML color eg: red #FF0000 rgb(250,10,20) rgba(10,10,10, 0.5) @@ -89,7 +89,7 @@ Initial selected view - The default view to show when the calendar is loaded + The default view showed when the calendar is loaded Day Week @@ -100,7 +100,7 @@ Initial selected view - The default view to show when the calendar is loaded + The default view showed when the calendar is loaded Day Week @@ -116,11 +116,11 @@ Day start hour - The hour at which the day view starts (0-23) + The hour at which the day view starts (0–23) Day end hour - The hour at which the day view ends (1-24) + The hour at which the day view ends (1–24) From dfb582e2cae3d5accfebd2e937c7cf2b14959d73 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 24 Jun 2025 17:31:00 +0200 Subject: [PATCH 08/14] feat(calendar-web): create CustomWeekController to handle CustomWeek component --- .../src/helpers/CustomWeekController.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts new file mode 100644 index 0000000000..749cf26013 --- /dev/null +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts @@ -0,0 +1,77 @@ +import * as dateFns from "date-fns"; +import { createElement, ReactElement } from "react"; +import { CalendarProps, NavigateAction } from "react-big-calendar"; +// @ts-expect-error - TimeGrid is not part of public typings +import TimeGrid from "react-big-calendar/lib/TimeGrid"; +import { getRange } from "../utils/calendar-utils"; + +type CustomWeekComponent = ((viewProps: CalendarProps) => ReactElement) & { + navigate: (date: Date, action: NavigateAction) => Date; + title: (date: Date, options: any) => string; +}; + +export class CustomWeekController { + constructor( + private date: Date, + private viewProps: CalendarProps, + private visibleDays: Set + ) {} + + render(): ReactElement { + const range = this.customRange(); + return createElement(TimeGrid as any, { + ...this.viewProps, + range, + eventOffset: 15 + }); + } + + private customRange(): Date[] { + return getRange(this.date, this.visibleDays); + } + + static 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; + } + } + + static title(date: Date, options: any, visibleDays: Set): string { + const range = getRange(date, visibleDays); + + const loc = options?.localizer ?? { + format: (d: Date, _fmt: string) => d.toLocaleDateString(undefined, { month: "short", day: "2-digit" }) + }; + + const isContiguous = range.every( + (curr, idx, arr) => idx === 0 || dateFns.differenceInCalendarDays(curr, arr[idx - 1]) === 1 + ); + + if (isContiguous) { + const first = range[0]; + const last = range[range.length - 1]; + return `${loc.format(first, "MMM dd")} – ${loc.format(last, "MMM dd")}`; + } + + return range.map(d => loc.format(d, "EEE")).join(", "); + } + + // Main factory method that injects visibleDays + static getComponent(visibleDays: Set): CustomWeekComponent { + const Component = (viewProps: CalendarProps): ReactElement => { + const controller = new CustomWeekController(viewProps.date as Date, viewProps, visibleDays); + return controller.render(); + }; + + Component.navigate = CustomWeekController.navigate; + + Component.title = (date: Date, options: any): string => CustomWeekController.title(date, options, visibleDays); + + return Component; + } +} From dce96e198eff9250f1bbbe4f740a9387dac8e4b3 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 24 Jun 2025 17:31:44 +0200 Subject: [PATCH 09/14] feat(calendar-web): create CalendarPropsBuilder --- .../src/helpers/CalendarPropsBuilder.ts | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts new file mode 100644 index 0000000000..70b1122597 --- /dev/null +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -0,0 +1,179 @@ +import { ObjectItem } from "mendix"; +import { CalendarProps as ReactCalendarProps, NavigateAction, ViewsProps } from "react-big-calendar"; +import { withDragAndDropProps } from "react-big-calendar/lib/addons/dragAndDrop"; + +import { CalendarContainerProps } from "../../typings/CalendarProps"; +import { CustomToolbar } from "../components/Toolbar"; +import { eventPropGetter, getViewRange, localizer } from "../utils/calendar-utils"; +import { CustomWeekController } from "./CustomWeekController"; + +export interface CalendarEvent { + title: string; + start: Date; + end: Date; + allDay: boolean; + color?: string; + item: ObjectItem; +} + +type EventDropOrResize = { + event: CalendarEvent; + start: Date; + end: Date; +}; + +interface DragAndDropCalendarProps + extends ReactCalendarProps, + withDragAndDropProps {} + +export class CalendarPropsBuilder { + private readonly visibleDays: Set; + private readonly defaultView: "month" | "week" | "work_week" | "day" | "agenda"; + private readonly customCaption: string; + private readonly isCustomView: boolean; + private readonly events: CalendarEvent[]; + private readonly minTime: Date; + private readonly maxTime: Date; + + constructor(private props: CalendarContainerProps) { + this.isCustomView = props.view === "custom"; + this.defaultView = this.isCustomView ? props.defaultViewCustom : props.defaultViewStandard; + this.customCaption = props.customViewCaption?.value ?? "Custom"; + this.visibleDays = this.buildVisibleDays(); + this.events = this.buildEvents(props.databaseDataSource?.items ?? []); + this.minTime = this.buildTime(props.minHour ?? 0); + this.maxTime = this.buildTime(props.maxHour ?? 24); + } + + build(): DragAndDropCalendarProps { + const CustomWeek = CustomWeekController.getComponent(this.visibleDays); + const views: ViewsProps = this.isCustomView + ? { day: true, week: true, month: true, work_week: CustomWeek, agenda: true } + : { day: true, week: true, month: true }; + + return { + components: { + toolbar: CustomToolbar + }, + defaultView: this.defaultView, + messages: { + work_week: this.customCaption + }, + events: this.events, + localizer, + resizable: this.props.editable !== "never", + selectable: this.props.enableCreate, + views, + allDayAccessor: (event: CalendarEvent) => event.allDay, + endAccessor: (event: CalendarEvent) => event.end, + eventPropGetter, + onEventDrop: this.handleEventDropOrResize, + onEventResize: this.handleEventDropOrResize, + onNavigate: this.handleRangeChange, + onSelectEvent: this.handleSelectEvent, + onSelectSlot: this.handleSelectSlot, + startAccessor: (event: CalendarEvent) => event.start, + titleAccessor: (event: CalendarEvent) => event.title, + showAllEvents: this.props.showAllEvents, + min: this.minTime, + max: this.maxTime + }; + } + + private buildEvents(items: ObjectItem[]): CalendarEvent[] { + return items.map(item => { + return this.buildEventItem(item); + }); + } + + private buildEventItem(item: ObjectItem): CalendarEvent { + const title = this.buildEventTitle(item); + const start = this.props.startAttribute?.get(item).value ?? new Date(); + const end = this.props.endAttribute?.get(item).value ?? start; + const allDay = this.props.allDayAttribute?.get(item).value ?? false; + const color = this.props.eventColor?.get(item).value; + + return { title, start, end, allDay, color, item }; + } + + private buildEventTitle(item: ObjectItem): string { + if (this.props.titleType === "attribute" && this.props.titleAttribute) { + return this.props.titleAttribute.get(item).value ?? ""; + } else if (this.props.titleType === "expression" && this.props.titleExpression) { + return String(this.props.titleExpression.get(item) ?? ""); + } else { + return "Untitled Event"; + } + } + + private buildTime(hour: number): Date { + const time = new Date(); + time.setHours(hour, 0, 0, 0); + return time; + } + + private buildVisibleDays(): Set { + const visibleDays = [ + this.props.showSunday, + this.props.showMonday, + this.props.showTuesday, + this.props.showWednesday, + this.props.showThursday, + this.props.showFriday, + this.props.showSaturday + ].flatMap((isVisible, dayIndex) => (isVisible ? [dayIndex] : [])); + + return new Set(visibleDays); + } + + private handleEventDropOrResize({ event, start, end }: EventDropOrResize): void { + const action = this.props.onChange?.get(event.item); + + if (action?.canExecute) { + action.execute({ + oldStart: event.start, + oldEnd: event.end, + newStart: start, + newEnd: end + }); + } + } + + private handleRangeChange(date: Date, view: string, _action: NavigateAction): void { + const action = this.props.onRangeChange; + + if (action?.canExecute) { + const { start, end } = getViewRange(view, date); + action.execute({ + rangeStart: start, + rangeEnd: end, + currentView: view + }); + } + } + + private handleSelectEvent(event: CalendarEvent): void { + const action = this.props.onClickEvent?.get(event.item); + + if (action?.canExecute) { + action.execute({ + startDate: event.start, + endDate: event.end, + allDay: event.allDay, + title: event.title + }); + } + } + + private handleSelectSlot(slotInfo: { start: Date; end: Date; action: string }): void { + const action = this.props.onCreateEvent; + + if (action?.canExecute && this.props.enableCreate) { + action?.execute({ + startDate: slotInfo.start, + endDate: slotInfo.end, + allDay: slotInfo.action === "select" + }); + } + } +} From 6cab12c49c42c10026e646dfe199dc97188df62b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 24 Jun 2025 17:32:45 +0200 Subject: [PATCH 10/14] chore(calendar-web): update calendar-utils to have only functions needed on calendar --- .../calendar-web/src/utils/calendar-utils.ts | 253 +++--------------- 1 file changed, 32 insertions(+), 221 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 82d59462f7..967b9c9607 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -1,37 +1,35 @@ -import * as dateFns from "date-fns"; -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 { Calendar, dateFnsLocalizer } from "react-big-calendar"; +import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop"; +import { CalendarEvent } from "../helpers/CalendarPropsBuilder"; import "react-big-calendar/lib/addons/dragAndDrop/styles.css"; import "react-big-calendar/lib/css/react-big-calendar.css"; +import * as dateFns from "date-fns"; + +export const DnDCalendar = withDragAndDrop(Calendar); + +type EventPropGetterReturnType = { + style: + | { + backgroundColor: string; + } + | undefined; +}; -export interface CalEvent { - title: string; - start: Date; - end: Date; - allDay: boolean; - color?: string; - item: ObjectItem; +export function eventPropGetter(event: CalendarEvent): EventPropGetterReturnType { + return { + style: event.color ? { backgroundColor: event.color } : undefined + }; } -// Configure date-fns localizer -const localizer = dateFnsLocalizer({ - format: dateFns.format, - parse: dateFns.parse, - startOfWeek: dateFns.startOfWeek, - getDay: dateFns.getDay, - locales: {} -}); +export function getRange(date: Date, visibleDays: Set): Date[] { + const startOfWeekDate = dateFns.startOfWeek(date, { weekStartsOn: 0 }); -export const DnDCalendar = withDragAndDrop(Calendar); + return Array.from({ length: 7 }, (_, i) => dateFns.addDays(startOfWeekDate, i)).flatMap(current => + visibleDays.has(current.getDay()) ? [current] : [] + ); +} -function getViewRange(view: string, date: Date): { start: Date; end: Date } { +export function getViewRange(view: string, date: Date): { start: Date; end: Date } { switch (view) { case "month": return { start: dateFns.startOfMonth(date), end: dateFns.endOfMonth(date) }; @@ -48,197 +46,10 @@ function getViewRange(view: string, date: Date): { start: Date; end: Date } { } } -type EventPropGetterReturnType = { - style: - | { - backgroundColor: string; - } - | undefined; -}; - -export function eventPropGetter(event: CalEvent): EventPropGetterReturnType { - return { - style: event.color ? { backgroundColor: event.color } : undefined - }; -} - -interface DragAndDropCalendarProps - extends CalendarProps, - withDragAndDropProps {} - -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 = - props.titleType === "attribute" && props.titleAttribute - ? (props.titleAttribute.get(item).value ?? "") - : props.titleType === "expression" && props.titleExpression - ? String(props.titleExpression.get(item) ?? "") - : "Untitled Event"; - const start = props.startAttribute?.get(item).value ?? new Date(); - 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, item }; - }); - - 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?.get(event.item).canExecute) { - props.onClickEvent.get(event.item).execute({ - startDate: event.start, - endDate: event.end, - allDay: event.allDay, - title: event.title - }); - } - }; - - const handleSelectSlot = (slotInfo: { start: Date; end: Date; action: string }): void => { - if (props.enableCreate && props.onCreateEvent?.canExecute) { - props.onCreateEvent.execute({ - startDate: slotInfo.start, - endDate: slotInfo.end, - allDay: slotInfo.action === "select" - }); - } - }; - - const handleEventDropOrResize = ({ event, start, end }: { event: CalEvent; start: Date; end: Date }): void => { - if (props.onChange?.get(event.item).canExecute) { - props.onChange.get(event.item).execute({ - oldStart: event.start, - oldEnd: event.end, - newStart: start, - newEnd: end - }); - } - }; - - const handleRangeChange = (date: Date, view: string, _action: NavigateAction): void => { - if (props.onRangeChange?.canExecute) { - const { start, end } = getViewRange(view, date); - props.onRangeChange.execute({ - rangeStart: start, - rangeEnd: end, - currentView: view - }); - } - }; - - const formats = props.showEventDate ? {} : { eventTimeRangeFormat: () => "" }; - - return { - components: { - toolbar: CustomToolbar - }, - defaultView, - messages: { - work_week: customCaption - }, - events, - formats, - localizer, - resizable: props.editable !== "never", - selectable: props.enableCreate, - views: viewsOption, - allDayAccessor: (event: CalEvent) => event.allDay, - endAccessor: (event: CalEvent) => event.end, - eventPropGetter, - onEventDrop: handleEventDropOrResize, - onEventResize: handleEventDropOrResize, - onNavigate: handleRangeChange, - onSelectEvent: handleSelectEvent, - onSelectSlot: handleSelectSlot, - startAccessor: (event: CalEvent) => event.start, - titleAccessor: (event: CalEvent) => event.title, - showAllEvents: props.showAllEvents, - min: minTime, - max: maxTime - }; -} +export const localizer = dateFnsLocalizer({ + format: dateFns.format, + parse: dateFns.parse, + startOfWeek: dateFns.startOfWeek, + getDay: dateFns.getDay, + locales: {} +}); From ea1fe4cc014ae9fac62e88393655e04bba228fc6 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Tue, 24 Jun 2025 17:33:07 +0200 Subject: [PATCH 11/14] feat(calendar-web): use the new CalendarPropsBuilder --- packages/pluggableWidgets/calendar-web/src/Calendar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx index 3983e9d64e..03f60bc04d 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx @@ -1,14 +1,15 @@ import classnames from "classnames"; import { ReactElement, createElement } from "react"; -import { DnDCalendar, extractCalendarProps } from "./utils/calendar-utils"; import { CalendarContainerProps } from "../typings/CalendarProps"; +import { CalendarPropsBuilder } from "./helpers/CalendarPropsBuilder"; +import { DnDCalendar } from "./utils/calendar-utils"; import { constructWrapperStyle } from "./utils/style-utils"; import "./ui/Calendar.scss"; export default function MxCalendar(props: CalendarContainerProps): ReactElement { const { class: className } = props; const wrapperStyle = constructWrapperStyle(props); - const calendarProps = extractCalendarProps(props); + const calendarProps = new CalendarPropsBuilder(props).build(); return (
From 33311c8c6d6dd31ec6f8a9f552ac104246731c4e Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Wed, 25 Jun 2025 14:07:01 +0200 Subject: [PATCH 12/14] refactor(calendar-web): convert event handlers to arrow functions and adjust time building logic --- .../src/helpers/CalendarPropsBuilder.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 70b1122597..326d72e489 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -108,7 +108,14 @@ export class CalendarPropsBuilder { private buildTime(hour: number): Date { const time = new Date(); - time.setHours(hour, 0, 0, 0); + time.setMinutes(0, 0, 0); + + if (hour >= 24) { + time.setHours(23, 59, 59); + } else { + time.setHours(hour); + } + return time; } @@ -126,7 +133,7 @@ export class CalendarPropsBuilder { return new Set(visibleDays); } - private handleEventDropOrResize({ event, start, end }: EventDropOrResize): void { + private handleEventDropOrResize = ({ event, start, end }: EventDropOrResize): void => { const action = this.props.onChange?.get(event.item); if (action?.canExecute) { @@ -137,9 +144,9 @@ export class CalendarPropsBuilder { newEnd: end }); } - } + }; - private handleRangeChange(date: Date, view: string, _action: NavigateAction): void { + private handleRangeChange = (date: Date, view: string, _action: NavigateAction): void => { const action = this.props.onRangeChange; if (action?.canExecute) { @@ -150,9 +157,9 @@ export class CalendarPropsBuilder { currentView: view }); } - } + }; - private handleSelectEvent(event: CalendarEvent): void { + private handleSelectEvent = (event: CalendarEvent): void => { const action = this.props.onClickEvent?.get(event.item); if (action?.canExecute) { @@ -163,9 +170,9 @@ export class CalendarPropsBuilder { title: event.title }); } - } + }; - private handleSelectSlot(slotInfo: { start: Date; end: Date; action: string }): void { + private handleSelectSlot = (slotInfo: { start: Date; end: Date; action: string }): void => { const action = this.props.onCreateEvent; if (action?.canExecute && this.props.enableCreate) { @@ -175,5 +182,5 @@ export class CalendarPropsBuilder { allDay: slotInfo.action === "select" }); } - } + }; } From 89ee43078201dc56dbd523107565923670932ac6 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Wed, 25 Jun 2025 14:20:20 +0200 Subject: [PATCH 13/14] test(calendar-web): update Calendar snapshot to reflect recent changes --- .../__snapshots__/Calendar.spec.tsx.snap | 3640 +++++++++++++++-- 1 file changed, 3409 insertions(+), 231 deletions(-) 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 82e0a6328f..d661e1424a 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 @@ -224,309 +224,3247 @@ exports[`Calendar renders correctly with basic props 1`] = ` >
-
-
+
-
-
-
-
+ class="rbc-time-slot" + > + + 12:00 AM + +
-
-
-
+
-
-
-
-
+ class="rbc-time-slot" + > + + 1:00 AM + +
-
-
-
+
+ + 2:00 AM + +
+
-
-
-
-
-
-
- , - "container":
-
-
-
-
- - - -
-
- - Apr 28 – May 02 - -
-
- - - - - -
-
-
-
-
-
-
- +
+
- +
+
- +
+
- +
+
- +
+
-
-
-
+ class="rbc-time-slot" + > + + 8:00 AM + +
+
+
+ class="rbc-time-slot" + > + + 9:00 AM + +
-
-
+ + 10:00 AM + +
+
+
+
+
+ + 11:00 AM + +
+
+
+
+
+ + 12:00 PM + +
+
+
+
+
+ + 1:00 PM + +
+
+
+
+
+ + 2:00 PM + +
+
+
+
+
+ + 3:00 PM + +
+
+
+
+
+ + 4:00 PM + +
+
+
+
+
+ + 5:00 PM + +
+
+
+
+
+ + 6:00 PM + +
+
+
+
+
+ + 7:00 PM + +
+
+
+
+
+ + 8:00 PM + +
+
+
+
+
+ + 9:00 PM + +
+
+
+
+
+ + 10:00 PM + +
+
+
+
+
+ + 11:00 PM +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ , + "container":
+
+
+
+
+ + + +
+
+ + Apr 28 – May 02 + +
+
+ + + + + +
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 12:00 AM + +
+
+
+
+
+ + 1:00 AM + +
+
+
+
+
+ + 2:00 AM + +
+
+
+
+
+ + 3:00 AM + +
+
+
+
+
+ + 4:00 AM + +
+
+
+
+
+ + 5:00 AM + +
+
+
+
+
+ + 6:00 AM + +
+
+
+
+
+ + 7:00 AM + +
+
+
+
+
+ + 8:00 AM + +
+
+
+
+
+ + 9:00 AM + +
+
+
+
+
+ + 10:00 AM + +
+
+
+
+
+ + 11:00 AM + +
+
+
+
+
+ + 12:00 PM + +
+
+
+
+
+ + 1:00 PM + +
+
+
+
+
+ + 2:00 PM + +
+
+
+
+
+ + 3:00 PM + +
+
+
+
+
+ + 4:00 PM + +
+
+
+
+
+ + 5:00 PM + +
+
+
+
+
+ + 6:00 PM + +
+
+
+
+
+ + 7:00 PM + +
+
+
+
+
+ + 8:00 PM + +
+
+
+
+
+ + 9:00 PM + +
+
+
+
+
+ + 10:00 PM + +
+
+
+
+
+ + 11:00 PM + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Date: Fri, 27 Jun 2025 13:58:19 +0200 Subject: [PATCH 14/14] feat(calendar-web): add buildFormats method to customize event display formats --- .../src/helpers/CalendarPropsBuilder.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 326d72e489..9aa4dced4f 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -1,5 +1,5 @@ import { ObjectItem } from "mendix"; -import { CalendarProps as ReactCalendarProps, NavigateAction, ViewsProps } from "react-big-calendar"; +import { CalendarProps as ReactCalendarProps, Formats, NavigateAction, ViewsProps } from "react-big-calendar"; import { withDragAndDropProps } from "react-big-calendar/lib/addons/dragAndDrop"; import { CalendarContainerProps } from "../../typings/CalendarProps"; @@ -47,6 +47,7 @@ export class CalendarPropsBuilder { build(): DragAndDropCalendarProps { const CustomWeek = CustomWeekController.getComponent(this.visibleDays); + const formats = this.buildFormats(); const views: ViewsProps = this.isCustomView ? { day: true, week: true, month: true, work_week: CustomWeek, agenda: true } : { day: true, week: true, month: true }; @@ -60,6 +61,7 @@ export class CalendarPropsBuilder { work_week: this.customCaption }, events: this.events, + formats, localizer, resizable: this.props.editable !== "never", selectable: this.props.enableCreate, @@ -106,6 +108,16 @@ export class CalendarPropsBuilder { } } + private buildFormats(): Formats { + const formats: Formats = {}; + + if (this.props.showEventDate === false) { + formats.eventTimeRangeFormat = () => ""; + } + + return formats; + } + private buildTime(hour: number): Date { const time = new Date(); time.setMinutes(0, 0, 0);