Skip to content

Commit 476c3f4

Browse files
committed
feat(calendar-web): add custom toolbar config, fix time format config, remove previous custom config
1 parent b3e8015 commit 476c3f4

File tree

11 files changed

+335
-116
lines changed

11 files changed

+335
-116
lines changed

packages/pluggableWidgets/calendar-web/CHANGELOG.md

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

77
## [Unreleased]
88

9+
### Added
10+
11+
- You can now customize which controls appear in the calendar’s top bar and how they are arranged, with optional captions, tooltips, and a link-style appearance.
12+
13+
- The calendar title can be formatted consistently across views, including custom work week.
14+
15+
- Time formatting is applied consistently to the time gutter and to event/agenda time ranges, with robust fallbacks for invalid patterns.
16+
17+
### Changed
18+
19+
- When the event time range is disabled, events no longer display start/end time text.
20+
21+
### Breaking changes
22+
23+
- Custom view buttons and their captions are now set inside the Custom top bar views configuration.
24+
925
## [2.0.0] - 2025-08-12
1026

1127
### Breaking changes

packages/pluggableWidgets/calendar-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mendix/calendar-web",
33
"widgetName": "Calendar",
4-
"version": "2.0.0",
4+
"version": "2.2.0",
55
"description": "Calendar",
66
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
77
"license": "Apache-2.0",

packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,6 @@ import { CalendarPreviewProps } from "../typings/CalendarProps";
1010
import IconSVGDark from "./assets/StructureCalendarDark.svg";
1111
import IconSVG from "./assets/StructureCalendarLight.svg";
1212

13-
const CUSTOM_VIEW_CONFIG: Array<keyof CalendarPreviewProps> = [
14-
"customViewShowDay",
15-
"customViewShowWeek",
16-
"customViewShowMonth",
17-
"customViewShowAgenda",
18-
"customViewShowCustomWeek",
19-
"customViewCaption",
20-
"defaultViewCustom"
21-
];
22-
23-
const CUSTOM_VIEW_DAYS_CONFIG: Array<keyof CalendarPreviewProps> = [
24-
"customViewShowMonday",
25-
"customViewShowTuesday",
26-
"customViewShowWednesday",
27-
"customViewShowThursday",
28-
"customViewShowFriday",
29-
"customViewShowSaturday",
30-
"customViewShowSunday"
31-
];
32-
3313
export function getProperties(values: CalendarPreviewProps, defaultProperties: Properties): Properties {
3414
if (values.heightUnit === "percentageOfWidth") {
3515
hidePropertyIn(defaultProperties, values, "height");
@@ -51,15 +31,11 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P
5131
hidePropertiesIn(defaultProperties, values, ["maxHeight", "overflowY"]);
5232
}
5333

54-
// Hide custom week range properties when the view is set to 'standard'
34+
// In custom view, legacy visible-day toggles are removed; only keep default view selection.
5535
if (values.view === "standard") {
56-
hidePropertiesIn(defaultProperties, values, [...CUSTOM_VIEW_CONFIG, ...CUSTOM_VIEW_DAYS_CONFIG]);
36+
hidePropertiesIn(defaultProperties, values, ["defaultViewCustom"]);
5737
} else {
5838
hidePropertyIn(defaultProperties, values, "defaultViewStandard");
59-
60-
if (values.customViewShowCustomWeek === false) {
61-
hidePropertiesIn(defaultProperties, values, ["customViewCaption", ...CUSTOM_VIEW_DAYS_CONFIG]);
62-
}
6339
}
6440

6541
// Show/hide title properties based on selection

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import classnames from "classnames";
22
import { createElement, ReactElement } from "react";
33
import { Calendar, dateFnsLocalizer, EventPropGetter } from "react-big-calendar";
44
import { CalendarPreviewProps } from "../typings/CalendarProps";
5-
import { CustomToolbar } from "./components/Toolbar";
5+
import { CustomToolbar, createConfigurableToolbar } from "./components/Toolbar";
66
import { constructWrapperStyle, WrapperStyleProps } from "./utils/style-utils";
77
import { eventPropGetter, format, getDay, parse, startOfWeek } from "./utils/calendar-utils";
88

@@ -75,10 +75,23 @@ export function preview(props: CalendarPreviewProps): ReactElement {
7575
// Cast eventPropGetter to satisfy preview Calendar generic
7676
const previewEventPropGetter = eventPropGetter as unknown as EventPropGetter<(typeof events)[0]>;
7777

78+
const toolbar =
79+
props.view === "custom" && props.toolbarItems?.length
80+
? createConfigurableToolbar(
81+
props.toolbarItems.map(i => ({
82+
itemType: i.itemType,
83+
position: i.position,
84+
caption: i.caption,
85+
tooltip: i.tooltip,
86+
renderMode: i.renderMode
87+
})) as any
88+
)
89+
: CustomToolbar;
90+
7891
return (
7992
<div className={classnames("widget-events-preview", "widget-calendar", className)} style={wrapperStyle}>
8093
<Calendar
81-
components={{ toolbar: CustomToolbar }}
94+
components={{ toolbar }}
8295
defaultView={props.defaultViewStandard}
8396
events={events}
8497
localizer={localizer}

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@ import "./ui/Calendar.scss";
88
import { useCalendarEvents } from "./helpers/useCalendarEvents";
99

1010
export default function MxCalendar(props: CalendarContainerProps): ReactElement {
11-
// useMemo with empty dependency array is used
12-
// because style and calendar controller needs to be created only once
13-
// and not on every re-render
14-
// eslint-disable-next-line react-hooks/exhaustive-deps
1511
const wrapperStyle = useMemo(() => constructWrapperStyle(props), []);
16-
// eslint-disable-next-line react-hooks/exhaustive-deps
1712
const calendarController = useMemo(() => new CalendarPropsBuilder(props), []);
1813
const calendarProps = useMemo(() => {
1914
calendarController.updateProps(props);

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

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@
115115
<caption>Time format</caption>
116116
<description>Default time format is "hh:mm a"</description>
117117
</property>
118+
<property key="topBarDateFormat" type="textTemplate" required="false">
119+
<caption>Top bar date format</caption>
120+
<description>Format used for the title in the toolbar across views. Defaults to a locale-aware format.</description>
121+
</property>
118122
<property key="minHour" type="integer" defaultValue="0">
119123
<caption>Day start hour</caption>
120124
<description>The hour at which the day view starts (0–23)</description>
@@ -130,34 +134,57 @@
130134
</propertyGroup>
131135
</propertyGroup>
132136
<propertyGroup caption="Custom view">
133-
<propertyGroup caption="Visible views">
134-
<property key="customViewShowDay" type="boolean" defaultValue="true">
135-
<caption>Day</caption>
136-
<description>Show day view in the toolbar</description>
137-
</property>
138-
<property key="customViewShowWeek" type="boolean" defaultValue="true">
139-
<caption>Week</caption>
140-
<description>Show week view in the toolbar</description>
141-
</property>
142-
<property key="customViewShowCustomWeek" type="boolean" defaultValue="false">
143-
<caption>Custom Work Week</caption>
144-
<description>Show custom week view in the toolbar</description>
145-
</property>
146-
<!-- Custom week caption -->
147-
<property key="customViewCaption" type="textTemplate" required="false">
148-
<caption>Custom view caption</caption>
149-
<description>Label used for the custom work-week button and title. Defaults to "Custom".</description>
150-
<translations>
151-
<translation lang="en_US">Custom</translation>
152-
</translations>
153-
</property>
154-
<property key="customViewShowMonth" type="boolean" defaultValue="true">
155-
<caption>Month</caption>
156-
<description>Show month view in the toolbar</description>
157-
</property>
158-
<property key="customViewShowAgenda" type="boolean" defaultValue="false">
159-
<caption>Agenda</caption>
160-
<description>Show agenda view in the toolbar</description>
137+
<propertyGroup caption="Toolbar">
138+
<property key="toolbarItems" type="object" isList="true" required="false">
139+
<caption>Custom top bar views</caption>
140+
<description>Configure items displayed in the calendar toolbar.</description>
141+
<properties>
142+
<property key="itemType" type="enumeration" defaultValue="month">
143+
<caption>Item</caption>
144+
<category>Toolbar</category>
145+
<description>Select which item to render on the toolbar.</description>
146+
<enumerationValues>
147+
<enumerationValue key="previous">Previous button</enumerationValue>
148+
<enumerationValue key="today">Today button</enumerationValue>
149+
<enumerationValue key="next">Next button</enumerationValue>
150+
<enumerationValue key="title">Title date text</enumerationValue>
151+
<enumerationValue key="month">Month button</enumerationValue>
152+
<enumerationValue key="week">Week button</enumerationValue>
153+
<enumerationValue key="work_week">Work week button</enumerationValue>
154+
<enumerationValue key="day">Day button</enumerationValue>
155+
<enumerationValue key="agenda">Agenda button</enumerationValue>
156+
</enumerationValues>
157+
</property>
158+
<property key="position" type="enumeration" defaultValue="left">
159+
<caption>Position</caption>
160+
<category>Toolbar</category>
161+
<description>Align the item within the toolbar.</description>
162+
<enumerationValues>
163+
<enumerationValue key="left">Left</enumerationValue>
164+
<enumerationValue key="center">Center</enumerationValue>
165+
<enumerationValue key="right">Right</enumerationValue>
166+
</enumerationValues>
167+
</property>
168+
<property key="caption" type="textTemplate" required="false">
169+
<caption>Caption</caption>
170+
<category>Toolbar</category>
171+
<description>Optional text for the button or title. If empty, a default localized label is used.</description>
172+
</property>
173+
<property key="tooltip" type="textTemplate" required="false">
174+
<caption>Tooltip</caption>
175+
<category>Toolbar</category>
176+
<description>Optional hover text for the button.</description>
177+
</property>
178+
<property key="renderMode" type="enumeration" defaultValue="button">
179+
<caption>Render mode</caption>
180+
<category>Toolbar</category>
181+
<description>Choose how the item is rendered.</description>
182+
<enumerationValues>
183+
<enumerationValue key="button">Button</enumerationValue>
184+
<enumerationValue key="link">Link</enumerationValue>
185+
</enumerationValues>
186+
</property>
187+
</properties>
161188
</property>
162189
</propertyGroup>
163190
<propertyGroup caption="Custom view visible days">

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,8 @@ const customViewProps: CalendarContainerProps = {
6565
customViewShowFriday: true,
6666
customViewShowSaturday: false,
6767
showAllEvents: true,
68-
customViewShowDay: true,
69-
customViewShowWeek: true,
70-
customViewShowCustomWeek: false,
71-
customViewShowMonth: true,
72-
customViewShowAgenda: false
68+
toolbarItems: [],
69+
topBarDateFormat: undefined
7370
};
7471

7572
const standardViewProps: CalendarContainerProps = {

packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,113 @@ export function CustomToolbar({ label, localizer, onNavigate, onView, view, view
4646
</div>
4747
);
4848
}
49+
50+
export type ResolvedToolbarItem = {
51+
itemType: "previous" | "today" | "next" | "title" | "month" | "week" | "work_week" | "day" | "agenda";
52+
position: "left" | "center" | "right";
53+
caption?: string;
54+
tooltip?: string;
55+
renderMode: "button" | "link";
56+
};
57+
58+
export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: ToolbarProps) => ReactElement {
59+
return function ConfigurableToolbar({ label, localizer, onNavigate, onView, view, views }: ToolbarProps) {
60+
const renderButton = (
61+
key: string,
62+
content: ReactElement | string,
63+
onClick: () => void,
64+
active = false,
65+
renderMode: "button" | "link" = "button",
66+
tooltip?: string
67+
): ReactElement => (
68+
<Button
69+
key={key}
70+
className={classNames("btn", renderMode === "link" ? "btn-link" : "btn-default", { active })}
71+
onClick={onClick}
72+
title={tooltip}
73+
>
74+
{content}
75+
</Button>
76+
);
77+
78+
const isViewEnabled = (name: View): boolean => {
79+
return Array.isArray(views) ? (views as View[]).includes(name) : true;
80+
};
81+
82+
const groups: Record<"left" | "center" | "right", ResolvedToolbarItem[]> = {
83+
left: [],
84+
center: [],
85+
right: []
86+
};
87+
items.forEach(item => {
88+
groups[item.position].push(item);
89+
});
90+
91+
const renderItem = (item: ResolvedToolbarItem): ReactElement | null => {
92+
switch (item.itemType) {
93+
case "previous":
94+
return renderButton(
95+
"prev",
96+
<IconInternal icon={{ type: "glyph", iconClass: "glyphicon-backward" }} />,
97+
() => onNavigate(Navigate.PREVIOUS),
98+
false,
99+
item.renderMode,
100+
item.tooltip
101+
);
102+
case "today":
103+
return renderButton(
104+
"today",
105+
(item.caption ?? localizer.messages.today) as unknown as ReactElement,
106+
() => onNavigate(Navigate.TODAY),
107+
false,
108+
item.renderMode,
109+
item.tooltip
110+
);
111+
case "next":
112+
return renderButton(
113+
"next",
114+
<IconInternal icon={{ type: "glyph", iconClass: "glyphicon-forward" }} />,
115+
() => onNavigate(Navigate.NEXT),
116+
false,
117+
item.renderMode,
118+
item.tooltip
119+
);
120+
case "title":
121+
return (
122+
<span key="title" className="calendar-label" title={item.tooltip}>
123+
{label}
124+
</span>
125+
);
126+
case "month":
127+
case "week":
128+
case "work_week":
129+
case "day":
130+
case "agenda": {
131+
const name = item.itemType as View;
132+
if (!isViewEnabled(name)) {
133+
return null;
134+
}
135+
const caption = item.caption ?? localizer.messages[name];
136+
return renderButton(
137+
name,
138+
caption as unknown as ReactElement,
139+
() => onView(name),
140+
view === name,
141+
item.renderMode,
142+
item.tooltip
143+
);
144+
}
145+
default:
146+
return null;
147+
}
148+
};
149+
150+
return (
151+
<div className="calendar-toolbar">
152+
<div className="btn-group">{groups.left.map(renderItem)}</div>
153+
<div className="btn-group">{groups.center.map(renderItem)}</div>
154+
<div className="btn-group">{groups.right.map(renderItem)}</div>
155+
</div>
156+
);
157+
};
158+
}

0 commit comments

Comments
 (0)