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
Month
+
+ Custom
+
+
+ Agenda
+
-
+ class="rbc-label rbc-time-header-gutter"
+ />
- 30
+
+ 28 Mon
+
- 31
+
+ 29 Tue
+
- 01
+
+ 30 Wed
+
- 02
+
+ 01 Thu
+
- 03
+
+ 02 Fri
+
+
+
-
+ class="rbc-time-gutter rbc-time-column"
+ />
-
-
-
- 06
-
-
-
-
- 07
-
-
-
-
- 08
-
-
-
-
- 09
-
-
-
-
- 10
-
-
-
-
- 11
-
-
+
-
- 12
-
-
+ class="rbc-events-container"
+ />
-
-
-
-
-
-
- 13
-
-
-
-
- 14
-
-
-
-
- 15
-
-
-
-
- 16
-
-
-
-
- 17
-
-
-
-
- 18
-
-
+
-
- 19
-
-
+ class="rbc-events-container"
+ />
-
-
-
-
+
-
- 20
-
-
-
-
- 21
-
-
-
-
- 22
-
-
-
-
- 23
-
-
-
-
- 24
-
-
-
-
- 25
-
-
-
-
- 26
-
-
-
-
-
-
-
-
-
-
-
-
- 27
-
-
-
-
- 28
-
-
-
-
- 29
-
-
-
-
- 30
-
-
-
-
- 01
-
-
-
-
- 02
-
-
-
-
- 03
-
-
+ class="rbc-events-container"
+ />
-
@@ -800,7 +321,7 @@ exports[`Calendar renders correctly with basic props 1`] = `
- April 2025
+ Apr 28 – May 02
Month
+
+ Custom
+
+
+ Agenda
+
-
+ class="rbc-label rbc-time-header-gutter"
+ />
- 30
+
+ 28 Mon
+
- 31
+
+ 29 Tue
+
- 01
+
+ 30 Wed
+
- 02
+
+ 01 Thu
+
- 03
+
+ 02 Fri
+
+
+
-
+ class="rbc-time-gutter rbc-time-column"
+ />
-
-
-
- 06
-
-
-
-
- 07
-
-
-
-
- 08
-
-
+
-
- 09
-
-
-
-
- 10
-
-
-
-
- 11
-
-
-
-
- 12
-
-
+ class="rbc-events-container"
+ />
-
-
-
-
-
-
-
-
- 13
-
-
-
-
- 14
-
-
+
-
- 15
-
-
-
-
- 16
-
-
-
-
- 17
-
-
-
-
- 18
-
-
-
-
- 19
-
-
+ class="rbc-events-container"
+ />
-
-
-
-
-
-
-
-
- 20
-
-
-
-
- 21
-
-
+
-
- 22
-
-
-
-
- 23
-
-
-
-
- 24
-
-
-
-
- 25
-
-
-
-
- 26
-
-
+ class="rbc-events-container"
+ />
-
-
-
-
-
-
- 27
-
-
+
-
- 28
-
-
-
-
- 29
-
-
-
-
- 30
-
-
-
-
- 01
-
-
-
-
- 02
-
-
-
-
- 03
-
-
+ 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;
}