diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 0cd2731f37..229dc2961f 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 the 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.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 (
diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index fc7da33603..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) @@ -87,15 +87,24 @@ 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 + The default view showed when the calendar is loaded + + Day + Week + Month + Custom + Agenda + + + + Initial selected view + The default view showed when the calendar is loaded Day Week Month - (Work week) - (Agenda) @@ -105,6 +114,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 + @@ -115,7 +170,7 @@ - + On click action @@ -134,7 +189,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 @@ -218,6 +273,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 f656c5e60a..1229a8d7cc 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -4,26 +4,42 @@ 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", - showEventDate: true + showEventDate: true, + showSunday: false, + showMonday: true, + showTuesday: true, + showWednesday: true, + showThursday: true, + showFriday: true, + showSaturday: false, + showAllEvents: true +}; + +const standardViewProps: CalendarContainerProps = { + ...customViewProps, + view: "standard" }; beforeAll(() => { @@ -37,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..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 @@ -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-row-bg" + > +
+
+
+
+
- +
+
+
-
-
-
-
-
-
-
-
-
-
- + 12:00 AM +
+
+
+
- + 1:00 AM +
+
+
+
- + 2:00 AM +
+
+
+
- + 3:00 AM +
+
+
+
- + 4:00 AM +
+
+
+
- + 5:00 AM +
+
+
+
- + 6:00 AM +
+
-
-
-
-
-
-
-
-
+ class="rbc-timeslot-group" + > +
+ + 7:00 AM + +
+
+
+ class="rbc-timeslot-group" + > +
+ + 8:00 AM + +
+
+
+ class="rbc-timeslot-group" + > +
+ + 9:00 AM + +
+
+
-
-
+ class="rbc-timeslot-group" + > +
+ + 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 +
- -
+ class="rbc-time-slot" + /> +
+
- + 7:00 PM +
+
+
+
- + 8:00 PM +
+
+
+
- + 9:00 PM +
+
+
+
- + 10:00 PM +
+
+
+
- + 11:00 PM +
+
-
-
-
+ class="rbc-timeslot-group" + > +
+
+
+ class="rbc-timeslot-group" + > +
+
+
+ class="rbc-timeslot-group" + > +
+
+
+ class="rbc-timeslot-group" + > +
+
+
+ class="rbc-timeslot-group" + > +
+
+
+ class="rbc-timeslot-group" + > +
+
+
-
-
+ class="rbc-timeslot-group" + > +
+
+
- -
+ class="rbc-time-slot" + />
- -
+ class="rbc-time-slot" + /> +
+
- -
+ class="rbc-time-slot" + />
- -
+ class="rbc-time-slot" + /> +
+
- -
+ class="rbc-time-slot" + />
- -
+ class="rbc-time-slot" + /> +
+
- -
+ class="rbc-time-slot" + /> +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- , - "container":
-
-
-
-
- - - -
-
- - April 2025 - -
-
- - - -
-
-
-
-
- - Sun - -
-
- - Mon - -
-
- - Tue - -
-
- - Wed - -
-
- - Thu - -
-
- - Fri - -
-
- - Sat - -
-
-
-
-
-
-
-
-
-
-
-
-
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
- +
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ , + "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 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..9aa4dced4f --- /dev/null +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -0,0 +1,198 @@ +import { ObjectItem } from "mendix"; +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"; +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 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 }; + + return { + components: { + toolbar: CustomToolbar + }, + defaultView: this.defaultView, + messages: { + work_week: this.customCaption + }, + events: this.events, + formats, + 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 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); + + if (hour >= 24) { + time.setHours(23, 59, 59); + } else { + time.setHours(hour); + } + + 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" + }); + } + }; +} 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; + } +} diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 31a3d7c927..967b9c9607 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -1,33 +1,35 @@ -import * as dateFns from "date-fns"; -import { Calendar, CalendarProps, dateFnsLocalizer, 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 { 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); -// Define the event shape -export interface CalEvent { - title: string; - start: Date; - end: Date; - allDay: boolean; - color?: string; +type EventPropGetterReturnType = { + style: + | { + backgroundColor: string; + } + | undefined; +}; + +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) }; @@ -44,108 +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 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 }; - }); - - const viewsOption: ViewsProps = - props.view === "standard" ? ["day", "week", "month"] : ["month", "week", "work_week", "day", "agenda"]; - - const handleSelectEvent = (event: CalEvent): void => { - if (props.onClickEvent?.canExecute) { - props.onClickEvent.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?.canExecute) { - props.onChange.execute({ - oldStart: event.start, - oldEnd: event.end, - newStart: start, - newEnd: end - }); - } - }; - - const handleRangeChange = (date: Date, view: string): 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: props.defaultView, - 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 - }; -} +export const localizer = dateFnsLocalizer({ + format: dateFns.format, + parse: dateFns.parse, + startOfWeek: dateFns.startOfWeek, + getDay: dateFns.getDay, + locales: {} +}); diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index 3b51c3846d..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, 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"; @@ -41,12 +43,23 @@ export interface CalendarContainerProps { editable: EditableEnum; enableCreate: boolean; showEventDate: 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; @@ -57,6 +70,7 @@ export interface CalendarContainerProps { maxHeightUnit: MaxHeightUnitEnum; maxHeight: number; overflowY: OverflowYEnum; + showAllEvents: boolean; } export interface CalendarPreviewProps { @@ -82,8 +96,19 @@ export interface CalendarPreviewProps { editable: EditableEnum; enableCreate: boolean; showEventDate: 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; @@ -98,4 +123,5 @@ export interface CalendarPreviewProps { maxHeightUnit: MaxHeightUnitEnum; maxHeight: number | null; overflowY: OverflowYEnum; + showAllEvents: boolean; }