Skip to content

Commit 2a80103

Browse files
committed
feat(calendar-web): add custom week day selection
1 parent db86ac1 commit 2a80103

File tree

6 files changed

+195
-22
lines changed

6 files changed

+195
-22
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P
3232
hidePropertiesIn(defaultProperties, values, ["maxHeight", "overflowY"]);
3333
}
3434

35+
// Hide custom week range properties when view is set to 'standard'
36+
if (values.view === "standard") {
37+
hidePropertiesIn(defaultProperties, values, [
38+
"showSunday",
39+
"showMonday",
40+
"showTuesday",
41+
"showWednesday",
42+
"showThursday",
43+
"showFriday",
44+
"showSaturday"
45+
]);
46+
}
47+
3548
// Show/hide title properties based on selection
3649
if (values.titleType === "attribute") {
3750
hidePropertyIn(defaultProperties, values, "titleExpression");

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import classnames from "classnames";
22
import * as dateFns from "date-fns";
33
import { ReactElement, createElement } from "react";
4-
import { Calendar, dateFnsLocalizer } from "react-big-calendar";
4+
import { Calendar, dateFnsLocalizer, EventPropGetter } from "react-big-calendar";
55
import { CalendarPreviewProps } from "../typings/CalendarProps";
66
import { CustomToolbar } from "./components/Toolbar";
77
import { constructWrapperStyle, WrapperStyleProps } from "./utils/style-utils";
@@ -73,15 +73,19 @@ export function preview(props: CalendarPreviewProps): ReactElement {
7373
const { class: className } = props;
7474
const wrapperStyle = constructWrapperStyle(props as WrapperStyleProps);
7575

76+
// Cast eventPropGetter to satisfy preview Calendar generic
77+
const previewEventPropGetter = eventPropGetter as unknown as EventPropGetter<(typeof events)[0]>;
78+
7679
return (
7780
<div className={classnames("widget-events-preview", "widget-calendar", className)} style={wrapperStyle}>
7881
<Calendar
7982
components={{ toolbar: CustomToolbar }}
8083
defaultView={props.defaultView}
8184
events={events}
8285
localizer={localizer}
83-
views={["day", "week", "month"]}
84-
eventPropGetter={eventPropGetter}
86+
messages={{ ...localizer.messages, work_week: "Custom" }}
87+
views={["day", "week", "month", "work_week"]}
88+
eventPropGetter={previewEventPropGetter}
8589
/>
8690
</div>
8791
);

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@
9090
<enumerationValue key="day">Day</enumerationValue>
9191
<enumerationValue key="week">Week</enumerationValue>
9292
<enumerationValue key="month">Month</enumerationValue>
93-
<enumerationValue key="work_week">(Work week)</enumerationValue>
94-
<enumerationValue key="agenda">(Agenda)</enumerationValue>
93+
<enumerationValue key="work_week">Custom</enumerationValue>
94+
<enumerationValue key="agenda">Agenda</enumerationValue>
9595
</enumerationValues>
9696
</property>
9797
<property key="startDateAttribute" type="attribute" required="false">
@@ -109,6 +109,45 @@
109109
<caption>Day end hour</caption>
110110
<description>The hour at which the day view ends (1-24)</description>
111111
</property>
112+
<!-- Custom week caption -->
113+
<property key="customViewCaption" type="translatableString" required="false">
114+
<caption>Custom view caption</caption>
115+
<category>View</category>
116+
<description>Label used for the custom work-week button and title. Defaults to "Custom".</description>
117+
<translations>
118+
<translation lang="en_US">Custom</translation>
119+
</translations>
120+
</property>
121+
</propertyGroup>
122+
<propertyGroup caption="Visible days">
123+
<property key="showMonday" type="boolean" defaultValue="true">
124+
<caption>Monday</caption>
125+
<description>Show Monday in the custom work-week view</description>
126+
</property>
127+
<property key="showTuesday" type="boolean" defaultValue="true">
128+
<caption>Tuesday</caption>
129+
<description>Show Tuesday in the custom work-week view</description>
130+
</property>
131+
<property key="showWednesday" type="boolean" defaultValue="true">
132+
<caption>Wednesday</caption>
133+
<description>Show Wednesday in the custom work-week view</description>
134+
</property>
135+
<property key="showThursday" type="boolean" defaultValue="true">
136+
<caption>Thursday</caption>
137+
<description>Show Thursday in the custom work-week view</description>
138+
</property>
139+
<property key="showFriday" type="boolean" defaultValue="true">
140+
<caption>Friday</caption>
141+
<description>Show Friday in the custom work-week view</description>
142+
</property>
143+
<property key="showSunday" type="boolean" defaultValue="false">
144+
<caption>Sunday</caption>
145+
<description>Show Sunday in the custom work-week view</description>
146+
</property>
147+
<property key="showSaturday" type="boolean" defaultValue="false">
148+
<caption>Saturday</caption>
149+
<description>Show Saturday in the custom work-week view</description>
150+
</property>
112151
</propertyGroup>
113152
</propertyGroup>
114153
<propertyGroup caption="Events">
@@ -119,7 +158,7 @@
119158
<attributeType name="String" />
120159
</attributeTypes>
121160
</property>
122-
<property key="onClickEvent" type="action" required="false">
161+
<property key="onClickEvent" type="action" required="false" dataSource="databaseDataSource">
123162
<caption>On click action</caption>
124163
<description />
125164
<actionVariables>
@@ -138,7 +177,7 @@
138177
<actionVariable key="allDay" type="Boolean" caption="All day flag" />
139178
</actionVariables>
140179
</property>
141-
<property key="onChange" type="action" required="false">
180+
<property key="onChange" type="action" required="false" dataSource="databaseDataSource">
142181
<caption>On change action</caption>
143182
<description>The change event is triggered on moving/dragging an item or changing the start or end time of by resizing an item</description>
144183
<actionVariables>

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,29 @@ const defaultProps: CalendarContainerProps = {
1010
tabIndex: 0,
1111
databaseDataSource: new ListValueBuilder().withItems([]).build(),
1212
titleType: "attribute",
13-
view: "standard",
14-
defaultView: "month",
13+
view: "custom",
14+
defaultView: "work_week",
1515
editable: "default",
1616
enableCreate: true,
1717
widthUnit: "percentage",
1818
width: 100,
1919
heightUnit: "pixels",
2020
height: 400,
21+
minHour: 0,
22+
maxHour: 24,
2123
minHeightUnit: "pixels",
2224
minHeight: 400,
2325
maxHeightUnit: "none",
2426
maxHeight: 400,
25-
overflowY: "auto"
27+
overflowY: "auto",
28+
showSunday: false,
29+
showMonday: true,
30+
showTuesday: true,
31+
showWednesday: true,
32+
showThursday: true,
33+
showFriday: true,
34+
showSaturday: false,
35+
showAllEvents: true
2636
};
2737

2838
beforeAll(() => {

packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import * as dateFns from "date-fns";
2-
import { Calendar, CalendarProps, dateFnsLocalizer, ViewsProps } from "react-big-calendar";
2+
import { ObjectItem } from "mendix";
3+
import { Calendar, CalendarProps, dateFnsLocalizer, NavigateAction, ViewsProps } from "react-big-calendar";
34
import withDragAndDrop, { withDragAndDropProps } from "react-big-calendar/lib/addons/dragAndDrop";
45
import { CalendarContainerProps } from "../../typings/CalendarProps";
56
import { CustomToolbar } from "../components/Toolbar";
7+
import { createElement, ReactElement } from "react";
8+
// @ts-expect-error - TimeGrid is not part of public typings
9+
import TimeGrid from "react-big-calendar/lib/TimeGrid";
610

711
import "react-big-calendar/lib/addons/dragAndDrop/styles.css";
812
import "react-big-calendar/lib/css/react-big-calendar.css";
913

10-
// Define the event shape
1114
export interface CalEvent {
1215
title: string;
1316
start: Date;
1417
end: Date;
1518
allDay: boolean;
1619
color?: string;
20+
item: ObjectItem;
1721
}
1822

1923
// Configure date-fns localizer
@@ -63,6 +67,82 @@ interface DragAndDropCalendarProps<TEvent extends object = Event, TResource exte
6367
withDragAndDropProps<TEvent, TResource> {}
6468

6569
export function extractCalendarProps(props: CalendarContainerProps): DragAndDropCalendarProps<CalEvent, object> {
70+
const visibleSet = new Set<number>();
71+
// Caption for custom work week button / title
72+
73+
const customCaption: string = props.customViewCaption ?? "Custom";
74+
const dayProps = [
75+
{ prop: props.showSunday, day: 0 },
76+
{ prop: props.showMonday, day: 1 },
77+
{ prop: props.showTuesday, day: 2 },
78+
{ prop: props.showWednesday, day: 3 },
79+
{ prop: props.showThursday, day: 4 },
80+
{ prop: props.showFriday, day: 5 },
81+
{ prop: props.showSaturday, day: 6 }
82+
];
83+
84+
dayProps.forEach(({ prop, day }) => {
85+
if (prop) visibleSet.add(day);
86+
});
87+
88+
function customRange(date: Date): Date[] {
89+
const startOfWeekDate = dateFns.startOfWeek(date, { weekStartsOn: 0 });
90+
const range: Date[] = [];
91+
for (let i = 0; i < 7; i++) {
92+
const current = dateFns.addDays(startOfWeekDate, i);
93+
if (visibleSet.has(current.getDay())) {
94+
range.push(current);
95+
}
96+
}
97+
return range;
98+
}
99+
100+
// Custom work-week view component based on TimeGrid
101+
const CustomWeek = (viewProps: CalendarProps): ReactElement => {
102+
const { date } = viewProps;
103+
const range = customRange(date as Date);
104+
105+
return createElement(TimeGrid as any, { ...viewProps, range, eventOffset: 15 });
106+
};
107+
108+
CustomWeek.range = customRange;
109+
CustomWeek.navigate = (date: Date, action: NavigateAction): Date => {
110+
switch (action) {
111+
case "PREV":
112+
return dateFns.addWeeks(date, -1);
113+
case "NEXT":
114+
return dateFns.addWeeks(date, 1);
115+
default:
116+
return date;
117+
}
118+
};
119+
120+
CustomWeek.title = (date: Date, options: any): string => {
121+
const loc = options?.localizer ?? {
122+
// Fallback localizer (EN)
123+
format: (d: Date, _fmt: string) => d.toLocaleDateString(undefined, { month: "short", day: "2-digit" })
124+
};
125+
126+
const range = customRange(date);
127+
128+
// Determine if the dates are contiguous (difference of 1 day between successive dates)
129+
const isContiguous = range.every(
130+
(curr, idx, arr) => idx === 0 || dateFns.differenceInCalendarDays(curr, arr[idx - 1]) === 1
131+
);
132+
133+
if (isContiguous) {
134+
// Keep default first–last representation (e.g. "Mar 11 – Mar 15")
135+
const first = range[0];
136+
const last = range[range.length - 1];
137+
return `${loc.format(first, "MMM dd")}${loc.format(last, "MMM dd")}`;
138+
}
139+
140+
// Non-contiguous selection → list individual weekday names (Mon, Wed, Fri)
141+
const weekdayList = range.map(d => loc.format(d, "EEE")).join(", ");
142+
143+
return weekdayList;
144+
};
145+
66146
const items = props.databaseDataSource?.items ?? [];
67147
const events: CalEvent[] = items.map(item => {
68148
const title =
@@ -75,11 +155,19 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop
75155
const end = props.endAttribute?.get(item).value ?? start;
76156
const allDay = props.allDayAttribute?.get(item).value ?? false;
77157
const color = props.eventColor?.get(item).value;
78-
return { title, start, end, allDay, color };
158+
return { title, start, end, allDay, color, item };
79159
});
80160

161+
// Update button label inside localizer messages
162+
(localizer as any).messages = {
163+
...localizer.messages,
164+
work_week: customCaption
165+
};
166+
81167
const viewsOption: ViewsProps<CalEvent, object> =
82-
props.view === "standard" ? ["day", "week", "month"] : ["month", "week", "work_week", "day", "agenda"];
168+
props.view === "standard"
169+
? { day: true, week: true, month: true }
170+
: { day: true, week: true, month: true, work_week: CustomWeek, agenda: true };
83171

84172
// Compute minimum and maximum times for the day based on configured hours
85173
const minTime = new Date();
@@ -88,8 +176,8 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop
88176
maxTime.setHours(props.maxHour ?? 24, 0, 0, 0);
89177

90178
const handleSelectEvent = (event: CalEvent): void => {
91-
if (props.onClickEvent?.canExecute) {
92-
props.onClickEvent.execute({
179+
if (props.onClickEvent?.get(event.item).canExecute) {
180+
props.onClickEvent.get(event.item).execute({
93181
startDate: event.start,
94182
endDate: event.end,
95183
allDay: event.allDay,
@@ -109,8 +197,8 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop
109197
};
110198

111199
const handleEventDropOrResize = ({ event, start, end }: { event: CalEvent; start: Date; end: Date }): void => {
112-
if (props.onChange?.canExecute) {
113-
props.onChange.execute({
200+
if (props.onChange?.get(event.item).canExecute) {
201+
props.onChange.get(event.item).execute({
114202
oldStart: event.start,
115203
oldEnd: event.end,
116204
newStart: start,
@@ -119,7 +207,7 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop
119207
}
120208
};
121209

122-
const handleRangeChange = (date: Date, view: string): void => {
210+
const handleRangeChange = (date: Date, view: string, _action: NavigateAction): void => {
123211
if (props.onRangeChange?.canExecute) {
124212
const { start, end } = getViewRange(view, date);
125213
props.onRangeChange.execute({
@@ -135,6 +223,9 @@ export function extractCalendarProps(props: CalendarContainerProps): DragAndDrop
135223
toolbar: CustomToolbar
136224
},
137225
defaultView: props.defaultView,
226+
messages: {
227+
work_week: customCaption
228+
},
138229
events,
139230
localizer,
140231
resizable: props.editable !== "never",

packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @author Mendix Widgets Framework Team
55
*/
66
import { CSSProperties } from "react";
7-
import { ActionValue, EditableValue, ListValue, Option, ListAttributeValue, ListExpressionValue } from "mendix";
7+
import { ActionValue, EditableValue, ListValue, Option, ListActionValue, ListAttributeValue, ListExpressionValue } from "mendix";
88

99
export type TitleTypeEnum = "attribute" | "expression";
1010

@@ -44,10 +44,18 @@ export interface CalendarContainerProps {
4444
startDateAttribute?: EditableValue<Date>;
4545
minHour: number;
4646
maxHour: number;
47+
customViewCaption?: any;
48+
showMonday: boolean;
49+
showTuesday: boolean;
50+
showWednesday: boolean;
51+
showThursday: boolean;
52+
showFriday: boolean;
53+
showSunday: boolean;
54+
showSaturday: boolean;
4755
eventDataAttribute?: EditableValue<string>;
48-
onClickEvent?: ActionValue<{ startDate: Option<Date>; endDate: Option<Date>; allDay: Option<boolean>; title: Option<string> }>;
56+
onClickEvent?: ListActionValue<{ startDate: Option<Date>; endDate: Option<Date>; allDay: Option<boolean>; title: Option<string> }>;
4957
onCreateEvent?: ActionValue<{ startDate: Option<Date>; endDate: Option<Date>; allDay: Option<boolean> }>;
50-
onChange?: ActionValue<{ oldStart: Option<Date>; oldEnd: Option<Date>; newStart: Option<Date>; newEnd: Option<Date> }>;
58+
onChange?: ListActionValue<{ oldStart: Option<Date>; oldEnd: Option<Date>; newStart: Option<Date>; newEnd: Option<Date> }>;
5159
onRangeChange?: ActionValue<{ rangeStart: Option<Date>; rangeEnd: Option<Date>; currentView: Option<string> }>;
5260
widthUnit: WidthUnitEnum;
5361
width: number;
@@ -87,6 +95,14 @@ export interface CalendarPreviewProps {
8795
startDateAttribute: string;
8896
minHour: number | null;
8997
maxHour: number | null;
98+
customViewCaption: any;
99+
showMonday: boolean;
100+
showTuesday: boolean;
101+
showWednesday: boolean;
102+
showThursday: boolean;
103+
showFriday: boolean;
104+
showSunday: boolean;
105+
showSaturday: boolean;
90106
eventDataAttribute: string;
91107
onClickEvent: {} | null;
92108
onCreateEvent: {} | null;

0 commit comments

Comments
 (0)