Skip to content

Commit a318b99

Browse files
[WC-2792]: Calendar actions & events (#1589)
2 parents a97bf0d + 8137626 commit a318b99

File tree

8 files changed

+1486
-1735
lines changed

8 files changed

+1486
-1735
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
]
3939
},
4040
"overrides": {
41-
"@mendix/pluggable-widgets-tools": "10.18.2",
41+
"@mendix/pluggable-widgets-tools": "10.21.0",
4242
"react": "^18.0.0",
4343
"react-dom": "^18.0.0",
4444
"prettier": "3.5.3",
@@ -55,7 +55,8 @@
5555
"[email protected]": ">=1.0.2",
5656
"@codemirror/view": "^6.34.2",
5757
"enzyme>cheerio": "1.0.0-rc.10",
58-
"ts-node": "10.9.2"
58+
"ts-node": "10.9.2",
59+
"react-big-calendar@1>clsx": "2.1.0"
5960
},
6061
"patchedDependencies": {
6162

packages/pluggableWidgets/calendar-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"devDependencies": {
5454
"@mendix/automation-utils": "workspace:*",
5555
"@mendix/eslint-config-web-widgets": "workspace:*",
56-
"@mendix/pluggable-widgets-tools": "10.16.0",
56+
"@mendix/pluggable-widgets-tools": "*",
5757
"@mendix/prettier-config-web-widgets": "workspace:*",
5858
"@mendix/run-e2e": "workspace:^*",
5959
"@mendix/widget-plugin-component-kit": "workspace:*",
Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,17 @@
11
import classnames from "classnames";
2-
import * as dateFns from "date-fns";
32
import { ReactElement, createElement } from "react";
4-
import { Calendar, dateFnsLocalizer, ViewsProps } from "react-big-calendar";
5-
import "react-big-calendar/lib/css/react-big-calendar.css";
3+
import { DnDCalendar, extractCalendarProps } from "./utils/calendar-utils";
64
import { CalendarContainerProps } from "../typings/CalendarProps";
75
import { constructWrapperStyle } from "./utils/utils";
86

9-
const localizer = dateFnsLocalizer({
10-
format: dateFns.format,
11-
parse: dateFns.parse,
12-
startOfWeek: dateFns.startOfWeek,
13-
getDay: dateFns.getDay,
14-
locales: {}
15-
});
16-
17-
interface CalEvent {
18-
title: string;
19-
start: Date;
20-
end: Date;
21-
allDay: boolean;
22-
color?: string;
23-
}
24-
257
export default function MxCalendar(props: CalendarContainerProps): ReactElement {
268
const { class: className } = props;
279
const wrapperStyle = constructWrapperStyle(props);
28-
29-
const items = props.databaseDataSource?.items ?? [];
30-
31-
const events: CalEvent[] = items.map(item => {
32-
const title =
33-
props.titleType === "attribute" && props.titleAttribute
34-
? (props.titleAttribute.get(item).value ?? "")
35-
: props.titleType === "expression" && props.titleExpression
36-
? String(props.titleExpression.get(item) ?? "")
37-
: "Untitled Event";
38-
39-
const start = props.startAttribute?.get(item).value ?? new Date();
40-
const end = props.endAttribute?.get(item).value ?? start;
41-
const allDay = props.allDayAttribute?.get(item).value ?? false;
42-
const color = props.eventColor?.get(item).value;
43-
44-
return { title, start, end, allDay, color };
45-
});
46-
47-
const viewsOption: ViewsProps<CalEvent, object> =
48-
props.view === "standard" ? ["month", "week", "day"] : ["month", "week", "work_week", "day", "agenda"];
49-
50-
const eventPropGetter = (event: CalEvent) => ({
51-
style: event.color ? { backgroundColor: event.color } : undefined
52-
});
10+
const calendarProps = extractCalendarProps(props);
5311

5412
return (
5513
<div className={classnames("widget-calendar", className)} style={wrapperStyle}>
56-
<Calendar<CalEvent>
57-
localizer={localizer}
58-
events={events}
59-
defaultView={props.defaultView}
60-
startAccessor={event => event.start}
61-
endAccessor={event => event.end}
62-
views={viewsOption}
63-
titleAccessor={event => event.title}
64-
allDayAccessor={event => event.allDay}
65-
eventPropGetter={eventPropGetter}
66-
/>
14+
<DnDCalendar {...calendarProps} />
6715
</div>
6816
);
6917
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,40 @@
114114
<property key="onClickEvent" type="action" required="false">
115115
<caption>On click action</caption>
116116
<description />
117+
<actionVariables>
118+
<actionVariable key="startDate" type="DateTime" caption="Event start date" />
119+
<actionVariable key="endDate" type="DateTime" caption="Event end date" />
120+
<actionVariable key="allDay" type="Boolean" caption="Event all day" />
121+
<actionVariable key="title" type="String" caption="Event title" />
122+
</actionVariables>
117123
</property>
118124
<property key="onCreateEvent" type="action" required="false">
119125
<caption>On create action</caption>
120126
<description>The create event is triggered when a time slot is selected, and the 'Enable create' property is set to 'true'</description>
127+
<actionVariables>
128+
<actionVariable key="startDate" type="DateTime" caption="New event start date" />
129+
<actionVariable key="endDate" type="DateTime" caption="New event end date" />
130+
<actionVariable key="allDay" type="Boolean" caption="All day flag" />
131+
</actionVariables>
121132
</property>
122133
<property key="onChange" type="action" required="false">
123134
<caption>On change action</caption>
124135
<description>The change event is triggered on moving/dragging an item or changing the start or end time of by resizing an item</description>
136+
<actionVariables>
137+
<actionVariable key="oldStart" type="DateTime" caption="Old start date" />
138+
<actionVariable key="oldEnd" type="DateTime" caption="Old end date" />
139+
<actionVariable key="newStart" type="DateTime" caption="New start date" />
140+
<actionVariable key="newEnd" type="DateTime" caption="New end date" />
141+
</actionVariables>
142+
</property>
143+
<property key="onRangeChange" type="action" required="false">
144+
<caption>On view range change</caption>
145+
<description>Triggered when the calendar view range (start/end) changes</description>
146+
<actionVariables>
147+
<actionVariable key="rangeStart" type="DateTime" caption="View range start" />
148+
<actionVariable key="rangeEnd" type="DateTime" caption="View range end" />
149+
<actionVariable key="currentView" type="String" caption="Current view" />
150+
</actionVariables>
125151
</property>
126152
</propertyGroup>
127153
<propertyGroup caption="Dimensions">

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ exports[`Calendar renders correctly with basic props 1`] = `
1010
style="width: 100%; height: 400px;"
1111
>
1212
<div
13-
class="rbc-calendar"
13+
class="rbc-addons-dnd rbc-calendar"
1414
>
1515
<div
1616
class="rbc-toolbar"
@@ -256,6 +256,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
256256
</button>
257257
</div>
258258
</div>
259+
<div
260+
class="rbc-addons-dnd-row-body"
261+
/>
259262
</div>
260263
</div>
261264
<div
@@ -372,6 +375,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
372375
</button>
373376
</div>
374377
</div>
378+
<div
379+
class="rbc-addons-dnd-row-body"
380+
/>
375381
</div>
376382
</div>
377383
<div
@@ -488,6 +494,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
488494
</button>
489495
</div>
490496
</div>
497+
<div
498+
class="rbc-addons-dnd-row-body"
499+
/>
491500
</div>
492501
</div>
493502
<div
@@ -604,6 +613,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
604613
</button>
605614
</div>
606615
</div>
616+
<div
617+
class="rbc-addons-dnd-row-body"
618+
/>
607619
</div>
608620
</div>
609621
<div
@@ -720,6 +732,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
720732
</button>
721733
</div>
722734
</div>
735+
<div
736+
class="rbc-addons-dnd-row-body"
737+
/>
723738
</div>
724739
</div>
725740
</div>
@@ -733,7 +748,7 @@ exports[`Calendar renders correctly with basic props 1`] = `
733748
style="width: 100%; height: 400px;"
734749
>
735750
<div
736-
class="rbc-calendar"
751+
class="rbc-addons-dnd rbc-calendar"
737752
>
738753
<div
739754
class="rbc-toolbar"
@@ -979,6 +994,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
979994
</button>
980995
</div>
981996
</div>
997+
<div
998+
class="rbc-addons-dnd-row-body"
999+
/>
9821000
</div>
9831001
</div>
9841002
<div
@@ -1095,6 +1113,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
10951113
</button>
10961114
</div>
10971115
</div>
1116+
<div
1117+
class="rbc-addons-dnd-row-body"
1118+
/>
10981119
</div>
10991120
</div>
11001121
<div
@@ -1211,6 +1232,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
12111232
</button>
12121233
</div>
12131234
</div>
1235+
<div
1236+
class="rbc-addons-dnd-row-body"
1237+
/>
12141238
</div>
12151239
</div>
12161240
<div
@@ -1327,6 +1351,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
13271351
</button>
13281352
</div>
13291353
</div>
1354+
<div
1355+
class="rbc-addons-dnd-row-body"
1356+
/>
13301357
</div>
13311358
</div>
13321359
<div
@@ -1443,6 +1470,9 @@ exports[`Calendar renders correctly with basic props 1`] = `
14431470
</button>
14441471
</div>
14451472
</div>
1473+
<div
1474+
class="rbc-addons-dnd-row-body"
1475+
/>
14461476
</div>
14471477
</div>
14481478
</div>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { Calendar, dateFnsLocalizer, ViewsProps } from "react-big-calendar";
2+
import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop";
3+
import "react-big-calendar/lib/addons/dragAndDrop/styles.css";
4+
import "react-big-calendar/lib/css/react-big-calendar.css";
5+
6+
import * as dateFns from "date-fns";
7+
import { CalendarContainerProps } from "../../typings/CalendarProps";
8+
9+
// Define the event shape
10+
export interface CalEvent {
11+
title: string;
12+
start: Date;
13+
end: Date;
14+
allDay: boolean;
15+
color?: string;
16+
}
17+
18+
// Configure date-fns localizer
19+
const localizer = dateFnsLocalizer({
20+
format: dateFns.format,
21+
parse: dateFns.parse,
22+
startOfWeek: dateFns.startOfWeek,
23+
getDay: dateFns.getDay,
24+
locales: {}
25+
});
26+
27+
export const DnDCalendar = withDragAndDrop(Calendar);
28+
29+
function getViewRange(view: string, date: Date) {
30+
switch (view) {
31+
case "month":
32+
return { start: dateFns.startOfMonth(date), end: dateFns.endOfMonth(date) };
33+
case "week":
34+
return { start: dateFns.startOfWeek(date), end: dateFns.endOfWeek(date) };
35+
case "work_week": {
36+
const start = dateFns.startOfWeek(date);
37+
return { start, end: dateFns.addDays(start, 4) };
38+
}
39+
case "day":
40+
return { start: date, end: date };
41+
default:
42+
return { start: date, end: date };
43+
}
44+
}
45+
46+
export function extractCalendarProps(props: CalendarContainerProps) {
47+
const items = props.databaseDataSource?.items ?? [];
48+
const events: CalEvent[] = items.map(item => {
49+
const title =
50+
props.titleType === "attribute" && props.titleAttribute
51+
? (props.titleAttribute.get(item).value ?? "")
52+
: props.titleType === "expression" && props.titleExpression
53+
? String(props.titleExpression.get(item) ?? "")
54+
: "Untitled Event";
55+
const start = props.startAttribute?.get(item).value ?? new Date();
56+
const end = props.endAttribute?.get(item).value ?? start;
57+
const allDay = props.allDayAttribute?.get(item).value ?? false;
58+
const color = props.eventColor?.get(item).value;
59+
return { title, start, end, allDay, color };
60+
});
61+
62+
const viewsOption: ViewsProps<CalEvent, object> =
63+
props.view === "standard" ? ["month", "week", "day"] : ["month", "week", "work_week", "day", "agenda"];
64+
65+
const eventPropGetter = (event: CalEvent) => ({
66+
style: event.color ? { backgroundColor: event.color } : undefined
67+
});
68+
69+
const handleSelectEvent = (event: CalEvent) => {
70+
if (props.onClickEvent?.canExecute) {
71+
props.onClickEvent.execute({
72+
startDate: event.start,
73+
endDate: event.end,
74+
allDay: event.allDay,
75+
title: event.title
76+
});
77+
}
78+
};
79+
80+
const handleSelectSlot = (slotInfo: { start: Date; end: Date; action: string }) => {
81+
if (props.enableCreate && props.onCreateEvent?.canExecute) {
82+
props.onCreateEvent.execute({
83+
startDate: slotInfo.start,
84+
endDate: slotInfo.end,
85+
allDay: slotInfo.action === "select"
86+
});
87+
}
88+
};
89+
90+
const handleEventDropOrResize = ({ event, start, end }: { event: CalEvent; start: Date; end: Date }) => {
91+
if (props.onChange?.canExecute) {
92+
props.onChange.execute({
93+
oldStart: event.start,
94+
oldEnd: event.end,
95+
newStart: start,
96+
newEnd: end
97+
});
98+
}
99+
};
100+
101+
const handleRangeChange = (date: Date, view: string) => {
102+
if (props.onRangeChange?.canExecute) {
103+
const { start, end } = getViewRange(view, date);
104+
props.onRangeChange.execute({
105+
rangeStart: start,
106+
rangeEnd: end,
107+
currentView: view
108+
});
109+
}
110+
};
111+
112+
return {
113+
localizer,
114+
events,
115+
defaultView: props.defaultView,
116+
startAccessor: (event: unknown) => (event as CalEvent).start,
117+
endAccessor: (event: unknown) => (event as CalEvent).end,
118+
selectable: props.enableCreate,
119+
resizable: props.editable !== "never",
120+
onSelectEvent: handleSelectEvent,
121+
onSelectSlot: handleSelectSlot,
122+
onEventDrop: handleEventDropOrResize,
123+
onEventResize: handleEventDropOrResize,
124+
onNavigate: handleRangeChange,
125+
views: viewsOption,
126+
titleAccessor: (event: unknown) => (event as CalEvent).title,
127+
allDayAccessor: (event: unknown) => (event as CalEvent).allDay,
128+
eventPropGetter
129+
};
130+
}

0 commit comments

Comments
 (0)