From 69bef59e6153e6d41fcc5c35d0e4e5ace9c5985d Mon Sep 17 00:00:00 2001 From: Rahman Date: Thu, 11 Sep 2025 16:44:48 +0200 Subject: [PATCH 01/14] feat(calendar-web): add custom toolbar config, fix time format config, remove previous custom config --- .../calendar-web/CHANGELOG.md | 16 +++ .../calendar-web/package.json | 2 +- .../calendar-web/src/Calendar.editorConfig.ts | 28 +--- .../src/Calendar.editorPreview.tsx | 17 ++- .../calendar-web/src/Calendar.tsx | 5 - .../calendar-web/src/Calendar.xml | 83 +++++++---- .../src/__tests__/Calendar.spec.tsx | 7 +- .../calendar-web/src/components/Toolbar.tsx | 110 +++++++++++++++ .../src/helpers/CalendarPropsBuilder.ts | 131 +++++++++++++----- .../src/helpers/CustomWeekController.ts | 14 +- .../calendar-web/typings/CalendarProps.d.ts | 38 +++-- 11 files changed, 335 insertions(+), 116 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/CHANGELOG.md b/packages/pluggableWidgets/calendar-web/CHANGELOG.md index 90f317da03..0b8b191fac 100644 --- a/packages/pluggableWidgets/calendar-web/CHANGELOG.md +++ b/packages/pluggableWidgets/calendar-web/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- 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. + +- The calendar title can be formatted consistently across views, including custom work week. + +- Time formatting is applied consistently to the time gutter and to event/agenda time ranges, with robust fallbacks for invalid patterns. + +### Changed + +- When the event time range is disabled, events no longer display start/end time text. + +### Breaking changes + +- Custom view buttons and their captions are now set inside the Custom top bar views configuration. + ## [2.0.0] - 2025-08-12 ### Breaking changes diff --git a/packages/pluggableWidgets/calendar-web/package.json b/packages/pluggableWidgets/calendar-web/package.json index 50dc75af0f..9de2e55bef 100644 --- a/packages/pluggableWidgets/calendar-web/package.json +++ b/packages/pluggableWidgets/calendar-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/calendar-web", "widgetName": "Calendar", - "version": "2.0.0", + "version": "2.2.0", "description": "Calendar", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 411cb34a80..9aece6d047 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -10,26 +10,6 @@ import { CalendarPreviewProps } from "../typings/CalendarProps"; import IconSVGDark from "./assets/StructureCalendarDark.svg"; import IconSVG from "./assets/StructureCalendarLight.svg"; -const CUSTOM_VIEW_CONFIG: Array = [ - "customViewShowDay", - "customViewShowWeek", - "customViewShowMonth", - "customViewShowAgenda", - "customViewShowCustomWeek", - "customViewCaption", - "defaultViewCustom" -]; - -const CUSTOM_VIEW_DAYS_CONFIG: Array = [ - "customViewShowMonday", - "customViewShowTuesday", - "customViewShowWednesday", - "customViewShowThursday", - "customViewShowFriday", - "customViewShowSaturday", - "customViewShowSunday" -]; - export function getProperties(values: CalendarPreviewProps, defaultProperties: Properties): Properties { if (values.heightUnit === "percentageOfWidth") { hidePropertyIn(defaultProperties, values, "height"); @@ -51,15 +31,11 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P hidePropertiesIn(defaultProperties, values, ["maxHeight", "overflowY"]); } - // Hide custom week range properties when the view is set to 'standard' + // In custom view, legacy visible-day toggles are removed; only keep default view selection. if (values.view === "standard") { - hidePropertiesIn(defaultProperties, values, [...CUSTOM_VIEW_CONFIG, ...CUSTOM_VIEW_DAYS_CONFIG]); + hidePropertiesIn(defaultProperties, values, ["defaultViewCustom"]); } else { hidePropertyIn(defaultProperties, values, "defaultViewStandard"); - - if (values.customViewShowCustomWeek === false) { - hidePropertiesIn(defaultProperties, values, ["customViewCaption", ...CUSTOM_VIEW_DAYS_CONFIG]); - } } // Show/hide title properties based on selection diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx index 7d7ec6fab3..d22a603bda 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx @@ -2,7 +2,7 @@ import classnames from "classnames"; import { ReactElement } from "react"; import { Calendar, dateFnsLocalizer, EventPropGetter } from "react-big-calendar"; import { CalendarPreviewProps } from "../typings/CalendarProps"; -import { CustomToolbar } from "./components/Toolbar"; +import { CustomToolbar, createConfigurableToolbar } from "./components/Toolbar"; import { constructWrapperStyle, WrapperStyleProps } from "./utils/style-utils"; import { eventPropGetter, format, getDay, parse, startOfWeek } from "./utils/calendar-utils"; @@ -75,10 +75,23 @@ export function preview(props: CalendarPreviewProps): ReactElement { // Cast eventPropGetter to satisfy preview Calendar generic const previewEventPropGetter = eventPropGetter as unknown as EventPropGetter<(typeof events)[0]>; + const toolbar = + props.view === "custom" && props.toolbarItems?.length + ? createConfigurableToolbar( + props.toolbarItems.map(i => ({ + itemType: i.itemType, + position: i.position, + caption: i.caption, + tooltip: i.tooltip, + renderMode: i.renderMode + })) as any + ) + : CustomToolbar; + return (
constructWrapperStyle(props), []); - // eslint-disable-next-line react-hooks/exhaustive-deps const calendarController = useMemo(() => new CalendarPropsBuilder(props), []); const calendarProps = useMemo(() => { calendarController.updateProps(props); diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index cbd5b84c7b..fdf782525a 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -115,6 +115,10 @@ Time format Default time format is "hh:mm a" + + Top bar date format + Format used for the title in the toolbar across views. Defaults to a locale-aware format. + Day start hour The hour at which the day view starts (0–23) @@ -130,34 +134,57 @@ - - - Day - Show day view in the toolbar - - - Week - Show week view in the toolbar - - - Custom Work Week - Show custom week view in the toolbar - - - - Custom view caption - Label used for the custom work-week button and title. Defaults to "Custom". - - Custom - - - - Month - Show month view in the toolbar - - - Agenda - Show agenda view in the toolbar + + + Custom top bar views + Configure items displayed in the calendar toolbar. + + + Item + Toolbar + Select which item to render on the toolbar. + + Previous button + Today button + Next button + Title date text + Month button + Week button + Work week button + Day button + Agenda button + + + + Position + Toolbar + Align the item within the toolbar. + + Left + Center + Right + + + + Caption + Toolbar + Optional text for the button or title. If empty, a default localized label is used. + + + Tooltip + Toolbar + Optional hover text for the button. + + + Render mode + Toolbar + Choose how the item is rendered. + + Button + Link + + + diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index b8d6b54a70..6725bc192d 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -64,11 +64,8 @@ const customViewProps: CalendarContainerProps = { customViewShowFriday: true, customViewShowSaturday: false, showAllEvents: true, - customViewShowDay: true, - customViewShowWeek: true, - customViewShowCustomWeek: false, - customViewShowMonth: true, - customViewShowAgenda: false + toolbarItems: [], + topBarDateFormat: undefined }; const standardViewProps: CalendarContainerProps = { diff --git a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx index b648425329..d3784ee347 100644 --- a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx @@ -46,3 +46,113 @@ export function CustomToolbar({ label, localizer, onNavigate, onView, view, view
); } + +export type ResolvedToolbarItem = { + itemType: "previous" | "today" | "next" | "title" | "month" | "week" | "work_week" | "day" | "agenda"; + position: "left" | "center" | "right"; + caption?: string; + tooltip?: string; + renderMode: "button" | "link"; +}; + +export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: ToolbarProps) => ReactElement { + return function ConfigurableToolbar({ label, localizer, onNavigate, onView, view, views }: ToolbarProps) { + const renderButton = ( + key: string, + content: ReactElement | string, + onClick: () => void, + active = false, + renderMode: "button" | "link" = "button", + tooltip?: string + ): ReactElement => ( + + ); + + const isViewEnabled = (name: View): boolean => { + return Array.isArray(views) ? (views as View[]).includes(name) : true; + }; + + const groups: Record<"left" | "center" | "right", ResolvedToolbarItem[]> = { + left: [], + center: [], + right: [] + }; + items.forEach(item => { + groups[item.position].push(item); + }); + + const renderItem = (item: ResolvedToolbarItem): ReactElement | null => { + switch (item.itemType) { + case "previous": + return renderButton( + "prev", + , + () => onNavigate(Navigate.PREVIOUS), + false, + item.renderMode, + item.tooltip + ); + case "today": + return renderButton( + "today", + (item.caption ?? localizer.messages.today) as unknown as ReactElement, + () => onNavigate(Navigate.TODAY), + false, + item.renderMode, + item.tooltip + ); + case "next": + return renderButton( + "next", + , + () => onNavigate(Navigate.NEXT), + false, + item.renderMode, + item.tooltip + ); + case "title": + return ( + + {label} + + ); + case "month": + case "week": + case "work_week": + case "day": + case "agenda": { + const name = item.itemType as View; + if (!isViewEnabled(name)) { + return null; + } + const caption = item.caption ?? localizer.messages[name]; + return renderButton( + name, + caption as unknown as ReactElement, + () => onView(name), + view === name, + item.renderMode, + item.tooltip + ); + } + default: + return null; + } + }; + + return ( +
+
{groups.left.map(renderItem)}
+
{groups.center.map(renderItem)}
+
{groups.right.map(renderItem)}
+
+ ); + }; +} diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 88093355d2..48d34129fc 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -1,7 +1,7 @@ import { ObjectItem } from "mendix"; import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar"; import { CalendarContainerProps } from "../../typings/CalendarProps"; -import { CustomToolbar } from "../components/Toolbar"; +import { CustomToolbar, ResolvedToolbarItem, createConfigurableToolbar } from "../components/Toolbar"; import { eventPropGetter, localizer } from "../utils/calendar-utils"; import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings"; import { CustomWeekController } from "./CustomWeekController"; @@ -9,40 +9,45 @@ import { CustomWeekController } from "./CustomWeekController"; export class CalendarPropsBuilder { private visibleDays: Set; private defaultView: "month" | "week" | "work_week" | "day" | "agenda"; - private customCaption: string; private isCustomView: boolean; private events: CalendarEvent[]; private minTime: Date; private maxTime: Date; + private toolbarItems?: ResolvedToolbarItem[]; 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); + this.toolbarItems = this.buildToolbarItems(); } updateProps(props: CalendarContainerProps): void { // Update the props object, skipping props that are static (on construction only) this.props = props; - this.customCaption = props.customViewCaption?.value ?? "Custom"; this.events = this.buildEvents(props.databaseDataSource?.items ?? []); + this.toolbarItems = this.buildToolbarItems(); } build(): DragAndDropCalendarProps { const formats = this.buildFormats(); const views = this.buildVisibleViews(); + const toolbar = + this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0 + ? createConfigurableToolbar(this.toolbarItems) + : CustomToolbar; + return { components: { - toolbar: CustomToolbar + toolbar }, defaultView: this.defaultView, messages: { - work_week: this.customCaption + work_week: "Custom" }, events: this.events, formats, @@ -92,33 +97,75 @@ export class CalendarPropsBuilder { private buildFormats(): Formats { const formats: Formats = {}; - if (this.props.showEventDate?.value === false) { - formats.eventTimeRangeFormat = () => ""; - } - - if (this.props.timeFormat?.status === "available") { - const timeFormat = this.props.timeFormat.value?.trim() || "p"; - - formats.timeGutterFormat = (date: Date, _culture: string, localizer: DateLocalizer) => { - // Some versions of date-fns (used internally by react-big-calendar) throw - // when the supplied format string does not contain any long-format tokens - // ("P" or "p"). This try/catch ensures that we gracefully fall back to a - // locale-aware default ("p") instead of crashing the whole widget. + const timePattern = this.getSafeTimePattern(); + if (timePattern) { + const formatWith = (date: Date, localizer: DateLocalizer, fallback = "p"): string => { try { - return localizer.format(date, timeFormat); + return localizer.format(date, timePattern); } catch (e) { console.warn( - `[Calendar] Failed to format time using pattern "${timeFormat}" – falling back to default pattern "p".`, + `[Calendar] Failed to format time using pattern "${timePattern}" – falling back to default pattern "${fallback}".`, e ); - return localizer.format(date, "p"); + return localizer.format(date, fallback); } }; + + formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) => formatWith(date, loc); + formats.eventTimeRangeFormat = ( + { start, end }: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => `${formatWith(start, loc)} – ${formatWith(end, loc)}`; + formats.agendaTimeRangeFormat = ( + { start, end }: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => `${formatWith(start, loc)} – ${formatWith(end, loc)}`; + } + + // Ensure showEventDate=false always hides event time ranges + if (this.props.showEventDate?.value === false) { + formats.eventTimeRangeFormat = () => ""; + } + + const titlePattern = this.props.topBarDateFormat?.value?.trim(); + if (titlePattern) { + formats.dayHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) => + loc.format(date, titlePattern); + formats.monthHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) => + loc.format(date, titlePattern); + formats.dayRangeHeaderFormat = ( + { start, end }: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => `${loc.format(start, titlePattern)} – ${loc.format(end, titlePattern)}`; + formats.agendaHeaderFormat = ( + { start, end }: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => `${loc.format(start, titlePattern)} – ${loc.format(end, titlePattern)}`; } return formats; } + private getSafeTimePattern(): string | undefined { + console.log("this.props.timeFormat?.status", this.props.timeFormat?.status); + if (this.props.timeFormat?.status === "available") { + console.log("this.props.timeFormat.value", this.props.timeFormat.value); + const trimmed = this.props.timeFormat.value?.trim(); + if (trimmed && trimmed.length > 0) { + console.log("trimmed", trimmed); + return trimmed; + } + console.log("returning p"); + return "p"; + } + console.log("returning undefined"); + return undefined; + } + private buildTime(hour: number): Date { const time = new Date(); time.setMinutes(0, 0, 0); @@ -148,22 +195,40 @@ export class CalendarPropsBuilder { private buildVisibleViews(): ViewsProps { if (this.isCustomView) { - const { - customViewShowDay, - customViewShowWeek, - customViewShowMonth, - customViewShowAgenda, - customViewShowCustomWeek - } = this.props; + // In custom view, visible views are now fully controlled by configured toolbar items. + // We derive which views are enabled by inspecting toolbar items; if none provided, fall back to all. + const itemViews = new Set( + (this.toolbarItems ?? []) + .map(i => i.itemType) + .filter(t => ["day", "week", "work_week", "month", "agenda"].includes(t)) + ); + const hasAny = itemViews.size > 0; return { - day: customViewShowDay, - week: customViewShowWeek, - work_week: customViewShowCustomWeek ? CustomWeekController.getComponent(this.visibleDays) : false, - month: customViewShowMonth, - agenda: customViewShowAgenda + day: hasAny ? itemViews.has("day") : true, + week: hasAny ? itemViews.has("week") : true, + work_week: + hasAny && itemViews.has("work_week") + ? CustomWeekController.getComponent(this.visibleDays, this.props.topBarDateFormat?.value) + : false, + month: hasAny ? itemViews.has("month") : true, + agenda: hasAny ? itemViews.has("agenda") : false }; } else { return { day: true, week: true, month: true }; } } + + private buildToolbarItems(): ResolvedToolbarItem[] | undefined { + const items = this.props.toolbarItems; + if (!items || items.length === 0) { + return undefined; + } + return items.map(i => ({ + itemType: i.itemType, + position: i.position, + caption: i.caption?.value, + tooltip: i.tooltip?.value, + renderMode: i.renderMode + })); + } } diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts index 3e07c6eaf5..d27406bbf6 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CustomWeekController.ts @@ -40,7 +40,7 @@ export class CustomWeekController { } } - static title(date: Date, options: any, visibleDays: Set): string { + static title(date: Date, options: any, visibleDays: Set, titlePattern?: string): string { const range = getRange(date, visibleDays); const loc = options?.localizer ?? { @@ -54,22 +54,28 @@ export class CustomWeekController { if (isContiguous) { const first = range[0]; const last = range[range.length - 1]; + if (titlePattern) { + return `${loc.format(first, titlePattern)} – ${loc.format(last, titlePattern)}`; + } return `${loc.format(first, "MMM dd")} – ${loc.format(last, "MMM dd")}`; } + if (titlePattern) { + return range.map(d => loc.format(d, titlePattern)).join(", "); + } return range.map(d => loc.format(d, "EEE")).join(", "); } // Main factory method that injects visibleDays - static getComponent(visibleDays: Set): CustomWeekComponent { + static getComponent(visibleDays: Set, titlePattern?: string): 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); + Component.title = (date: Date, options: any): string => + CustomWeekController.title(date, options, visibleDays, titlePattern); return Component; } diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index db1de17bd0..be83b3a22e 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -14,6 +14,20 @@ export type DefaultViewStandardEnum = "day" | "week" | "month"; export type DefaultViewCustomEnum = "day" | "week" | "month" | "work_week" | "agenda"; +export type ItemTypeEnum = "previous" | "today" | "next" | "title" | "month" | "week" | "work_week" | "day" | "agenda"; + +export type PositionEnum = "left" | "center" | "right"; + +export type RenderModeEnum = "button" | "link"; + +export interface ToolbarItemsType { + itemType: ItemTypeEnum; + position: PositionEnum; + caption?: DynamicValue; + tooltip?: DynamicValue; + renderMode: RenderModeEnum; +} + export type WidthUnitEnum = "pixels" | "percentage"; export type HeightUnitEnum = "percentageOfWidth" | "pixels" | "percentageOfParent" | "percentageOfView"; @@ -24,6 +38,14 @@ export type MaxHeightUnitEnum = "none" | "pixels" | "percentageOfParent" | "perc export type OverflowYEnum = "auto" | "scroll" | "hidden"; +export interface ToolbarItemsPreviewType { + itemType: ItemTypeEnum; + position: PositionEnum; + caption: string; + tooltip: string; + renderMode: RenderModeEnum; +} + export interface CalendarContainerProps { name: string; class: string; @@ -44,15 +66,11 @@ export interface CalendarContainerProps { defaultViewCustom: DefaultViewCustomEnum; showEventDate: DynamicValue; timeFormat?: DynamicValue; + topBarDateFormat?: DynamicValue; minHour: number; maxHour: number; showAllEvents: boolean; - customViewShowDay: boolean; - customViewShowWeek: boolean; - customViewShowCustomWeek: boolean; - customViewCaption?: DynamicValue; - customViewShowMonth: boolean; - customViewShowAgenda: boolean; + toolbarItems: ToolbarItemsType[]; customViewShowMonday: boolean; customViewShowTuesday: boolean; customViewShowWednesday: boolean; @@ -101,15 +119,11 @@ export interface CalendarPreviewProps { defaultViewCustom: DefaultViewCustomEnum; showEventDate: string; timeFormat: string; + topBarDateFormat: string; minHour: number | null; maxHour: number | null; showAllEvents: boolean; - customViewShowDay: boolean; - customViewShowWeek: boolean; - customViewShowCustomWeek: boolean; - customViewCaption: string; - customViewShowMonth: boolean; - customViewShowAgenda: boolean; + toolbarItems: ToolbarItemsPreviewType[]; customViewShowMonday: boolean; customViewShowTuesday: boolean; customViewShowWednesday: boolean; From 079f06526cf9417e1224af3696ae11b485399c3b Mon Sep 17 00:00:00 2001 From: Rahman Date: Thu, 11 Sep 2025 16:50:40 +0200 Subject: [PATCH 02/14] chore(calendar-web): remove comment about legacy functionality --- .../pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 9aece6d047..c6f8ba89e2 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -31,7 +31,6 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P hidePropertiesIn(defaultProperties, values, ["maxHeight", "overflowY"]); } - // In custom view, legacy visible-day toggles are removed; only keep default view selection. if (values.view === "standard") { hidePropertiesIn(defaultProperties, values, ["defaultViewCustom"]); } else { From 00b8814613ed3e551818db0e5ae28c38d11d3e38 Mon Sep 17 00:00:00 2001 From: Rahman Date: Thu, 11 Sep 2025 16:58:48 +0200 Subject: [PATCH 03/14] chore(calendar-web): fix lint warnings --- .../calendar-web/src/Calendar.editorPreview.tsx | 4 ++-- packages/pluggableWidgets/calendar-web/src/Calendar.tsx | 5 +++++ .../calendar-web/src/helpers/CalendarPropsBuilder.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx index d22a603bda..e649a30b76 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx @@ -2,9 +2,9 @@ import classnames from "classnames"; import { ReactElement } from "react"; import { Calendar, dateFnsLocalizer, EventPropGetter } from "react-big-calendar"; import { CalendarPreviewProps } from "../typings/CalendarProps"; -import { CustomToolbar, createConfigurableToolbar } from "./components/Toolbar"; -import { constructWrapperStyle, WrapperStyleProps } from "./utils/style-utils"; +import { createConfigurableToolbar, CustomToolbar } from "./components/Toolbar"; import { eventPropGetter, format, getDay, parse, startOfWeek } from "./utils/calendar-utils"; +import { constructWrapperStyle, WrapperStyleProps } from "./utils/style-utils"; import "react-big-calendar/lib/css/react-big-calendar.css"; import "./ui/Calendar.scss"; diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx index afb4d70300..5642178fef 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx @@ -8,7 +8,12 @@ import "./ui/Calendar.scss"; import { useCalendarEvents } from "./helpers/useCalendarEvents"; export default function MxCalendar(props: CalendarContainerProps): ReactElement { + // useMemo with empty dependency array is used + // because style and calendar controller needs to be created only once + // and not on every re-render + // eslint-disable-next-line react-hooks/exhaustive-deps const wrapperStyle = useMemo(() => constructWrapperStyle(props), []); + // eslint-disable-next-line react-hooks/exhaustive-deps const calendarController = useMemo(() => new CalendarPropsBuilder(props), []); const calendarProps = useMemo(() => { calendarController.updateProps(props); diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 48d34129fc..571b5d5aa6 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -1,7 +1,7 @@ import { ObjectItem } from "mendix"; import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar"; import { CalendarContainerProps } from "../../typings/CalendarProps"; -import { CustomToolbar, ResolvedToolbarItem, createConfigurableToolbar } from "../components/Toolbar"; +import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar"; import { eventPropGetter, localizer } from "../utils/calendar-utils"; import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings"; import { CustomWeekController } from "./CustomWeekController"; From 70b38a9afcc3186a038de653ce6dd54eac6152f8 Mon Sep 17 00:00:00 2001 From: Rahman Date: Fri, 12 Sep 2025 14:54:16 +0200 Subject: [PATCH 04/14] chore(calendar-web): bump package.xml --- packages/pluggableWidgets/calendar-web/src/package.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/calendar-web/src/package.xml b/packages/pluggableWidgets/calendar-web/src/package.xml index 617f12f361..1db632fc32 100644 --- a/packages/pluggableWidgets/calendar-web/src/package.xml +++ b/packages/pluggableWidgets/calendar-web/src/package.xml @@ -1,6 +1,6 @@ - + From 7929fbc3bd3575875e1813c7cee1a70fc4889dec Mon Sep 17 00:00:00 2001 From: Rahman Date: Mon, 13 Oct 2025 09:57:21 +0200 Subject: [PATCH 05/14] chore(calendar-web): clean logs --- .../calendar-web/src/helpers/CalendarPropsBuilder.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 571b5d5aa6..f65a720291 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -151,18 +151,13 @@ export class CalendarPropsBuilder { } private getSafeTimePattern(): string | undefined { - console.log("this.props.timeFormat?.status", this.props.timeFormat?.status); if (this.props.timeFormat?.status === "available") { - console.log("this.props.timeFormat.value", this.props.timeFormat.value); const trimmed = this.props.timeFormat.value?.trim(); if (trimmed && trimmed.length > 0) { - console.log("trimmed", trimmed); return trimmed; } - console.log("returning p"); return "p"; } - console.log("returning undefined"); return undefined; } From 519f8694dfb98534a6ef05585c2569b163967870 Mon Sep 17 00:00:00 2001 From: Rahman Date: Fri, 17 Oct 2025 15:18:58 +0200 Subject: [PATCH 06/14] feat(calendar-web): default captions for empty custom toolbar items, custom view tab visibility --- .../calendar-web/src/Calendar.editorConfig.ts | 12 +++++++++++- .../calendar-web/src/components/Toolbar.tsx | 16 ++++++---------- .../src/helpers/CalendarPropsBuilder.ts | 5 ++++- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index c6f8ba89e2..fa4567e743 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -32,7 +32,17 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P } if (values.view === "standard") { - hidePropertiesIn(defaultProperties, values, ["defaultViewCustom"]); + hidePropertiesIn(defaultProperties, values, [ + "defaultViewCustom", + "toolbarItems", + "customViewShowMonday", + "customViewShowTuesday", + "customViewShowWednesday", + "customViewShowThursday", + "customViewShowFriday", + "customViewShowSaturday", + "customViewShowSunday" + ]); } else { hidePropertyIn(defaultProperties, values, "defaultViewStandard"); } diff --git a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx index d3784ee347..7ea26b794f 100644 --- a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx @@ -56,7 +56,7 @@ export type ResolvedToolbarItem = { }; export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: ToolbarProps) => ReactElement { - return function ConfigurableToolbar({ label, localizer, onNavigate, onView, view, views }: ToolbarProps) { + return function ConfigurableToolbar({ label, localizer, onNavigate, onView, view }: ToolbarProps) { const renderButton = ( key: string, content: ReactElement | string, @@ -75,10 +75,6 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: ); - const isViewEnabled = (name: View): boolean => { - return Array.isArray(views) ? (views as View[]).includes(name) : true; - }; - const groups: Record<"left" | "center" | "right", ResolvedToolbarItem[]> = { left: [], center: [], @@ -100,9 +96,10 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: item.tooltip ); case "today": + // Always provide a default caption for 'today' button return renderButton( "today", - (item.caption ?? localizer.messages.today) as unknown as ReactElement, + (item.caption || localizer.messages.today) as unknown as ReactElement, () => onNavigate(Navigate.TODAY), false, item.renderMode, @@ -118,6 +115,7 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: item.tooltip ); case "title": + // Title always shows the formatted label, regardless of caption return ( {label} @@ -129,10 +127,8 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: case "day": case "agenda": { const name = item.itemType as View; - if (!isViewEnabled(name)) { - return null; - } - const caption = item.caption ?? localizer.messages[name]; + // Provide default caption from localizer messages if not specified + const caption = item.caption || localizer.messages[name]; return renderButton( name, caption as unknown as ReactElement, diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index f65a720291..8e15613db4 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -41,13 +41,16 @@ export class CalendarPropsBuilder { ? createConfigurableToolbar(this.toolbarItems) : CustomToolbar; + // Use custom caption for work_week if provided in toolbar items, else default to "Custom" + const workWeekCaption = this.toolbarItems?.find(item => item.itemType === "work_week")?.caption || "Custom"; + return { components: { toolbar }, defaultView: this.defaultView, messages: { - work_week: "Custom" + work_week: workWeekCaption }, events: this.events, formats, From e1ebe7312b0b2b13d6d88eb4683703d900a2cb11 Mon Sep 17 00:00:00 2001 From: Rahman Date: Fri, 17 Oct 2025 18:37:07 +0200 Subject: [PATCH 07/14] feat(calendar-web): add per item format and text config --- .../calendar-web/src/Calendar.editorConfig.ts | 51 +++++++- .../calendar-web/src/Calendar.xml | 64 ++++++++-- .../calendar-web/src/components/Toolbar.tsx | 9 ++ .../src/helpers/CalendarPropsBuilder.ts | 114 ++++++++++++++++-- .../calendar-web/typings/CalendarProps.d.ts | 18 ++- 5 files changed, 233 insertions(+), 23 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index fa4567e743..a27078c42d 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -5,7 +5,7 @@ import { StructurePreviewProps, text } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; -import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; +import { hideNestedPropertiesIn, hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { CalendarPreviewProps } from "../typings/CalendarProps"; import IconSVGDark from "./assets/StructureCalendarDark.svg"; import IconSVG from "./assets/StructureCalendarLight.svg"; @@ -47,6 +47,55 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P hidePropertyIn(defaultProperties, values, "defaultViewStandard"); } + values.toolbarItems?.forEach((item, index) => { + // Hide all format properties for non-view items (navigation buttons, title) + if (!["day", "month", "agenda", "week", "work_week"].includes(item.itemType)) { + hideNestedPropertiesIn(defaultProperties, values, "toolbarItems", index, [ + "customViewHeaderDayFormat", + "customViewCellDateFormat", + "customViewGutterDateFormat", + "customViewGutterTimeFormat", + "customViewAllDayText", + "customViewTextHeaderDate", + "customViewTextHeaderTime", + "customViewTextHeaderEvent" + ]); + } else { + switch (item.itemType) { + case "day": + case "week": + case "work_week": + // Day/Week/Custom Week: show headerDayFormat, hide all others + hideNestedPropertiesIn(defaultProperties, values, "toolbarItems", index, [ + "customViewCellDateFormat", + "customViewGutterDateFormat", + "customViewAllDayText", + "customViewTextHeaderDate", + "customViewTextHeaderTime", + "customViewTextHeaderEvent" + ]); + break; + case "month": + // Month: show headerDayFormat and cellDateFormat, hide gutter/agenda-specific + hideNestedPropertiesIn(defaultProperties, values, "toolbarItems", index, [ + "customViewGutterDateFormat", + "customViewGutterTimeFormat", + "customViewAllDayText", + "customViewTextHeaderDate", + "customViewTextHeaderTime", + "customViewTextHeaderEvent" + ]); + break; + case "agenda": + // Agenda: show gutter and text headers, hide headerDayFormat and cellDateFormat + hideNestedPropertiesIn(defaultProperties, values, "toolbarItems", index, [ + "customViewCellDateFormat" + ]); + break; + } + } + }); + // Show/hide title properties based on selection if (values.titleType === "attribute") { hidePropertyIn(defaultProperties, values, "titleExpression"); diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index fdf782525a..c9ec9f88b5 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -141,23 +141,23 @@ Item - Toolbar + Appearance Select which item to render on the toolbar. + Day view button + Month view button + Agenda view button + Week view button + Custom week view button + Title date text Previous button - Today button Next button - Title date text - Month button - Week button - Work week button - Day button - Agenda button + Today button Position - Toolbar + Appearance Align the item within the toolbar. Left @@ -167,23 +167,63 @@ Caption - Toolbar + Appearance Optional text for the button or title. If empty, a default localized label is used. Tooltip - Toolbar + Appearance Optional hover text for the button. Render mode - Toolbar + Appearance Choose how the item is rendered. Button Link + + Header day format + Custom formats + Format of date(s) in the header above the day, week, month, custom or Agenda view. + + + Cell date format + Custom formats + Date shown in the month cells. + + + Time gutter format + Custom formats + Time shown as the first column in the week, day and agenda view. + + + Date gutter format + Custom formats + Date shown as the first column in the agenda view. + + + All day text + Text + Text shown in the all day column. + + + Text header date + Text + Text showing in the agenda view to header in the column date. + + + Text header time + Text + Text showing in the agenda view to header in the column time. + + + Text header event + Text + Text showing in the agenda view to header in the column event. + diff --git a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx index 7ea26b794f..915c8b45eb 100644 --- a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx @@ -53,6 +53,15 @@ export type ResolvedToolbarItem = { caption?: string; tooltip?: string; renderMode: "button" | "link"; + // Custom formatting/text options for Custom view + customViewHeaderDayFormat?: string; + customViewCellDateFormat?: string; + customViewGutterTimeFormat?: string; + customViewGutterDateFormat?: string; + customViewAllDayText?: string; + customViewTextHeaderDate?: string; + customViewTextHeaderTime?: string; + customViewTextHeaderEvent?: string; }; export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: ToolbarProps) => ReactElement { diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 8e15613db4..fc8acf8dff 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -49,9 +49,7 @@ export class CalendarPropsBuilder { toolbar }, defaultView: this.defaultView, - messages: { - work_week: workWeekCaption - }, + messages: this.buildMessages(workWeekCaption), events: this.events, formats, localizer, @@ -71,6 +69,27 @@ export class CalendarPropsBuilder { }; } + private buildMessages(workWeekCaption: string): { + work_week: string; + allDay?: string; + date?: string; + time?: string; + event?: string; + } { + if (this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0) { + const byType = new Map(this.toolbarItems.map(i => [i.itemType, i])); + const agenda = byType.get("agenda"); + return { + work_week: workWeekCaption, + ...(agenda?.customViewAllDayText ? { allDay: agenda.customViewAllDayText } : {}), + ...(agenda?.customViewTextHeaderDate ? { date: agenda.customViewTextHeaderDate } : {}), + ...(agenda?.customViewTextHeaderTime ? { time: agenda.customViewTextHeaderTime } : {}), + ...(agenda?.customViewTextHeaderEvent ? { event: agenda.customViewTextHeaderEvent } : {}) + } as const; + } + return { work_week: workWeekCaption } as const; + } + private buildEvents(items: ObjectItem[]): CalendarEvent[] { return items.map(item => { return this.buildEventItem(item); @@ -127,11 +146,6 @@ export class CalendarPropsBuilder { ) => `${formatWith(start, loc)} – ${formatWith(end, loc)}`; } - // Ensure showEventDate=false always hides event time ranges - if (this.props.showEventDate?.value === false) { - formats.eventTimeRangeFormat = () => ""; - } - const titlePattern = this.props.topBarDateFormat?.value?.trim(); if (titlePattern) { formats.dayHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) => @@ -150,6 +164,80 @@ export class CalendarPropsBuilder { ) => `${loc.format(start, titlePattern)} – ${loc.format(end, titlePattern)}`; } + // Apply per-view custom formats only in custom view mode + if (this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0) { + const byType = new Map(this.toolbarItems.map(i => [i.itemType, i])); + + type HeaderFormat = Formats["dayHeaderFormat"]; + const applyHeader = (pattern?: string, existing?: HeaderFormat): HeaderFormat | undefined => { + if (!pattern) return existing; + return (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, pattern); + }; + + const dayHeaderPattern = byType.get("day")?.customViewHeaderDayFormat || this.props.topBarDateFormat?.value; + const weekHeaderPattern = + byType.get("week")?.customViewHeaderDayFormat || + byType.get("work_week")?.customViewHeaderDayFormat || + this.props.topBarDateFormat?.value; + const monthHeaderPattern = + byType.get("month")?.customViewHeaderDayFormat || this.props.topBarDateFormat?.value; + const agendaHeaderPattern = + byType.get("agenda")?.customViewHeaderDayFormat || this.props.topBarDateFormat?.value; + + formats.dayHeaderFormat = applyHeader(dayHeaderPattern, formats.dayHeaderFormat); + formats.dayRangeHeaderFormat = ( + range: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => { + const pattern = weekHeaderPattern; + if (pattern) { + return `${loc.format(range.start, pattern)} – ${loc.format(range.end, pattern)}`; + } + return `${loc.format(range.start, "P")} – ${loc.format(range.end, "P")}`; + }; + formats.monthHeaderFormat = applyHeader(monthHeaderPattern, formats.monthHeaderFormat); + formats.agendaHeaderFormat = (range: { start: Date; end: Date }, _culture: string, loc: DateLocalizer) => { + const pattern = agendaHeaderPattern; + if (pattern) { + return `${loc.format(range.start, pattern)} – ${loc.format(range.end, pattern)}`; + } + return `${loc.format(range.start, "P")} – ${loc.format(range.end, "P")}`; + }; + + // Month day numbers + const monthCellDate = byType.get("month")?.customViewCellDateFormat; + if (monthCellDate) { + formats.dateFormat = (date: Date, _culture: string, loc: DateLocalizer) => + loc.format(date, monthCellDate); + } + + // Time gutters + const weekTimeGutter = byType.get("week")?.customViewGutterTimeFormat; + const dayTimeGutter = byType.get("day")?.customViewGutterTimeFormat; + const workWeekTimeGutter = byType.get("work_week")?.customViewGutterTimeFormat; + const chosenTimeGutter = weekTimeGutter || dayTimeGutter || workWeekTimeGutter; + if (chosenTimeGutter) { + formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) => + loc.format(date, chosenTimeGutter); + } + const agendaTime = byType.get("agenda")?.customViewGutterTimeFormat; + if (agendaTime) { + formats.agendaTimeFormat = (date: Date, _culture: string, loc: DateLocalizer) => + loc.format(date, agendaTime); + } + const agendaDate = byType.get("agenda")?.customViewGutterDateFormat; + if (agendaDate) { + formats.agendaDateFormat = (date: Date, _culture: string, loc: DateLocalizer) => + loc.format(date, agendaDate); + } + } + + // Ensure showEventDate=false always hides event time ranges + if (this.props.showEventDate?.value === false) { + formats.eventTimeRangeFormat = () => ""; + } + return formats; } @@ -226,7 +314,15 @@ export class CalendarPropsBuilder { position: i.position, caption: i.caption?.value, tooltip: i.tooltip?.value, - renderMode: i.renderMode + renderMode: i.renderMode, + customViewHeaderDayFormat: i.customViewHeaderDayFormat?.value, + customViewCellDateFormat: i.customViewCellDateFormat?.value, + customViewGutterTimeFormat: i.customViewGutterTimeFormat?.value, + customViewGutterDateFormat: i.customViewGutterDateFormat?.value, + customViewAllDayText: i.customViewAllDayText?.value, + customViewTextHeaderDate: i.customViewTextHeaderDate?.value, + customViewTextHeaderTime: i.customViewTextHeaderTime?.value, + customViewTextHeaderEvent: i.customViewTextHeaderEvent?.value })); } } diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index be83b3a22e..d1d515c76c 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -14,7 +14,7 @@ export type DefaultViewStandardEnum = "day" | "week" | "month"; export type DefaultViewCustomEnum = "day" | "week" | "month" | "work_week" | "agenda"; -export type ItemTypeEnum = "previous" | "today" | "next" | "title" | "month" | "week" | "work_week" | "day" | "agenda"; +export type ItemTypeEnum = "day" | "month" | "agenda" | "week" | "work_week" | "title" | "previous" | "next" | "today"; export type PositionEnum = "left" | "center" | "right"; @@ -26,6 +26,14 @@ export interface ToolbarItemsType { caption?: DynamicValue; tooltip?: DynamicValue; renderMode: RenderModeEnum; + customViewHeaderDayFormat?: DynamicValue; + customViewCellDateFormat?: DynamicValue; + customViewGutterTimeFormat?: DynamicValue; + customViewGutterDateFormat?: DynamicValue; + customViewAllDayText?: DynamicValue; + customViewTextHeaderDate?: DynamicValue; + customViewTextHeaderTime?: DynamicValue; + customViewTextHeaderEvent?: DynamicValue; } export type WidthUnitEnum = "pixels" | "percentage"; @@ -44,6 +52,14 @@ export interface ToolbarItemsPreviewType { caption: string; tooltip: string; renderMode: RenderModeEnum; + customViewHeaderDayFormat: string; + customViewCellDateFormat: string; + customViewGutterTimeFormat: string; + customViewGutterDateFormat: string; + customViewAllDayText: string; + customViewTextHeaderDate: string; + customViewTextHeaderTime: string; + customViewTextHeaderEvent: string; } export interface CalendarContainerProps { From 1e2b6fe61351de6b521a86b77396f06bb0fb7ef0 Mon Sep 17 00:00:00 2001 From: Rahman Date: Fri, 17 Oct 2025 19:01:52 +0200 Subject: [PATCH 08/14] feat(calendar-web): custom button tooltip and styling per toolbar item --- .../calendar-web/src/Calendar.editorConfig.ts | 3 ++ .../calendar-web/src/Calendar.xml | 23 ++++++++++--- .../calendar-web/src/components/Toolbar.tsx | 32 ++++++++++++++----- .../src/helpers/CalendarPropsBuilder.ts | 3 +- .../calendar-web/typings/CalendarProps.d.ts | 8 +++-- 5 files changed, 53 insertions(+), 16 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index a27078c42d..1556c8c4ea 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -48,6 +48,9 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P } values.toolbarItems?.forEach((item, index) => { + if (item.itemType === "title") { + hideNestedPropertiesIn(defaultProperties, values, "toolbarItems", index, ["buttonTooltip", "buttonStyle"]); + } // Hide all format properties for non-view items (navigation buttons, title) if (!["day", "month", "agenda", "week", "work_week"].includes(item.itemType)) { hideNestedPropertiesIn(defaultProperties, values, "toolbarItems", index, [ diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index c9ec9f88b5..a31259b0a9 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -170,11 +170,6 @@ Appearance Optional text for the button or title. If empty, a default localized label is used. - - Tooltip - Appearance - Optional hover text for the button. - Render mode Appearance @@ -184,6 +179,24 @@ Link + + Button tooltip + Appearance + Tooltip for the button. + + + Button style + Appearance + Style of the button. + + Default + Primary + Success + Info + Warning + Danger + + Header day format Custom formats diff --git a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx index 915c8b45eb..f2d14ee295 100644 --- a/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/components/Toolbar.tsx @@ -51,7 +51,6 @@ export type ResolvedToolbarItem = { itemType: "previous" | "today" | "next" | "title" | "month" | "week" | "work_week" | "day" | "agenda"; position: "left" | "center" | "right"; caption?: string; - tooltip?: string; renderMode: "button" | "link"; // Custom formatting/text options for Custom view customViewHeaderDayFormat?: string; @@ -62,6 +61,9 @@ export type ResolvedToolbarItem = { customViewTextHeaderDate?: string; customViewTextHeaderTime?: string; customViewTextHeaderEvent?: string; + // Custom button presentation + customButtonTooltip?: string; + customButtonStyle?: "default" | "primary" | "success" | "info" | "warning" | "danger"; }; export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: ToolbarProps) => ReactElement { @@ -72,11 +74,14 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: onClick: () => void, active = false, renderMode: "button" | "link" = "button", - tooltip?: string + tooltip?: string, + styleClass?: string ): ReactElement => ( ); + const resolveStyleClass = (item: ResolvedToolbarItem): string => { + if (item.renderMode === "link") { + return "btn-link"; + } + return `btn-${item.customButtonStyle ?? "default"}`; + }; + const groups: Record<"left" | "center" | "right", ResolvedToolbarItem[]> = { left: [], center: [], @@ -102,7 +114,8 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: () => onNavigate(Navigate.PREVIOUS), false, item.renderMode, - item.tooltip + item.customButtonTooltip, + resolveStyleClass(item) ); case "today": // Always provide a default caption for 'today' button @@ -112,7 +125,8 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: () => onNavigate(Navigate.TODAY), false, item.renderMode, - item.tooltip + item.customButtonTooltip, + resolveStyleClass(item) ); case "next": return renderButton( @@ -121,12 +135,13 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: () => onNavigate(Navigate.NEXT), false, item.renderMode, - item.tooltip + item.customButtonTooltip, + resolveStyleClass(item) ); case "title": // Title always shows the formatted label, regardless of caption return ( - + {label} ); @@ -144,7 +159,8 @@ export function createConfigurableToolbar(items: ResolvedToolbarItem[]): (props: () => onView(name), view === name, item.renderMode, - item.tooltip + item.customButtonTooltip, + resolveStyleClass(item) ); } default: diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index fc8acf8dff..935493c17f 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -313,8 +313,9 @@ export class CalendarPropsBuilder { itemType: i.itemType, position: i.position, caption: i.caption?.value, - tooltip: i.tooltip?.value, renderMode: i.renderMode, + customButtonTooltip: i.buttonTooltip?.value, + customButtonStyle: i.buttonStyle, customViewHeaderDayFormat: i.customViewHeaderDayFormat?.value, customViewCellDateFormat: i.customViewCellDateFormat?.value, customViewGutterTimeFormat: i.customViewGutterTimeFormat?.value, diff --git a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts index d1d515c76c..519f41a392 100644 --- a/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts +++ b/packages/pluggableWidgets/calendar-web/typings/CalendarProps.d.ts @@ -20,12 +20,15 @@ export type PositionEnum = "left" | "center" | "right"; export type RenderModeEnum = "button" | "link"; +export type ButtonStyleEnum = "default" | "primary" | "success" | "info" | "warning" | "danger"; + export interface ToolbarItemsType { itemType: ItemTypeEnum; position: PositionEnum; caption?: DynamicValue; - tooltip?: DynamicValue; renderMode: RenderModeEnum; + buttonTooltip?: DynamicValue; + buttonStyle: ButtonStyleEnum; customViewHeaderDayFormat?: DynamicValue; customViewCellDateFormat?: DynamicValue; customViewGutterTimeFormat?: DynamicValue; @@ -50,8 +53,9 @@ export interface ToolbarItemsPreviewType { itemType: ItemTypeEnum; position: PositionEnum; caption: string; - tooltip: string; renderMode: RenderModeEnum; + buttonTooltip: string; + buttonStyle: ButtonStyleEnum; customViewHeaderDayFormat: string; customViewCellDateFormat: string; customViewGutterTimeFormat: string; From be80744a18be649323d9f09e67de8f2aa902d1c7 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 21 Oct 2025 11:49:42 +0200 Subject: [PATCH 09/14] chore(calendar-web): lint issue --- .../pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx index e649a30b76..849be6af89 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx @@ -82,7 +82,6 @@ export function preview(props: CalendarPreviewProps): ReactElement { itemType: i.itemType, position: i.position, caption: i.caption, - tooltip: i.tooltip, renderMode: i.renderMode })) as any ) From 3954ada5b9b4df3598b2ed8932ffd607d5d607d5 Mon Sep 17 00:00:00 2001 From: Rahman Date: Wed, 22 Oct 2025 16:39:34 +0200 Subject: [PATCH 10/14] fix(calendar-web): format render issue --- .../src/helpers/CalendarPropsBuilder.ts | 95 +++++++++++-------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 935493c17f..c6e308c22f 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -35,7 +35,6 @@ export class CalendarPropsBuilder { build(): DragAndDropCalendarProps { const formats = this.buildFormats(); const views = this.buildVisibleViews(); - const toolbar = this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0 ? createConfigurableToolbar(this.toolbarItems) @@ -44,11 +43,17 @@ export class CalendarPropsBuilder { // Use custom caption for work_week if provided in toolbar items, else default to "Custom" const workWeekCaption = this.toolbarItems?.find(item => item.itemType === "work_week")?.caption || "Custom"; + // Ensure defaultView is actually enabled in views, otherwise pick the first enabled view + const enabledViews = Object.entries(views) + .filter(([_, enabled]) => enabled !== false) + .map(([view]) => view as "day" | "week" | "work_week" | "month" | "agenda"); + const safeDefaultView = enabledViews.includes(this.defaultView) ? this.defaultView : enabledViews[0]; + return { components: { toolbar }, - defaultView: this.defaultView, + defaultView: safeDefaultView, messages: this.buildMessages(workWeekCaption), events: this.events, formats, @@ -170,63 +175,77 @@ export class CalendarPropsBuilder { type HeaderFormat = Formats["dayHeaderFormat"]; const applyHeader = (pattern?: string, existing?: HeaderFormat): HeaderFormat | undefined => { - if (!pattern) return existing; + if (!pattern || pattern.trim() === "") return existing; return (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, pattern); }; - const dayHeaderPattern = byType.get("day")?.customViewHeaderDayFormat || this.props.topBarDateFormat?.value; - const weekHeaderPattern = - byType.get("week")?.customViewHeaderDayFormat || - byType.get("work_week")?.customViewHeaderDayFormat || - this.props.topBarDateFormat?.value; - const monthHeaderPattern = - byType.get("month")?.customViewHeaderDayFormat || this.props.topBarDateFormat?.value; - const agendaHeaderPattern = - byType.get("agenda")?.customViewHeaderDayFormat || this.props.topBarDateFormat?.value; - - formats.dayHeaderFormat = applyHeader(dayHeaderPattern, formats.dayHeaderFormat); - formats.dayRangeHeaderFormat = ( - range: { start: Date; end: Date }, - _culture: string, - loc: DateLocalizer - ) => { - const pattern = weekHeaderPattern; - if (pattern) { - return `${loc.format(range.start, pattern)} – ${loc.format(range.end, pattern)}`; - } - return `${loc.format(range.start, "P")} – ${loc.format(range.end, "P")}`; - }; - formats.monthHeaderFormat = applyHeader(monthHeaderPattern, formats.monthHeaderFormat); - formats.agendaHeaderFormat = (range: { start: Date; end: Date }, _culture: string, loc: DateLocalizer) => { - const pattern = agendaHeaderPattern; - if (pattern) { - return `${loc.format(range.start, pattern)} – ${loc.format(range.end, pattern)}`; - } - return `${loc.format(range.start, "P")} – ${loc.format(range.end, "P")}`; + // Helper to get non-empty pattern or fallback + const getPattern = (pattern?: string, fallback?: string): string | undefined => { + const trimmed = pattern?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : fallback; }; + const dayHeaderPattern = getPattern( + byType.get("day")?.customViewHeaderDayFormat, + this.props.topBarDateFormat?.value + ); + const weekHeaderPattern = getPattern( + byType.get("week")?.customViewHeaderDayFormat || byType.get("work_week")?.customViewHeaderDayFormat, + this.props.topBarDateFormat?.value + ); + const monthHeaderPattern = getPattern( + byType.get("month")?.customViewHeaderDayFormat, + this.props.topBarDateFormat?.value + ); + const agendaHeaderPattern = getPattern( + byType.get("agenda")?.customViewHeaderDayFormat, + this.props.topBarDateFormat?.value + ); + + // Only apply if we have a valid pattern + if (dayHeaderPattern) { + formats.dayHeaderFormat = applyHeader(dayHeaderPattern, formats.dayHeaderFormat); + } + if (weekHeaderPattern) { + formats.dayRangeHeaderFormat = ( + range: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => `${loc.format(range.start, weekHeaderPattern)} – ${loc.format(range.end, weekHeaderPattern)}`; + } + if (monthHeaderPattern) { + formats.monthHeaderFormat = applyHeader(monthHeaderPattern, formats.monthHeaderFormat); + } + if (agendaHeaderPattern) { + formats.agendaHeaderFormat = ( + range: { start: Date; end: Date }, + _culture: string, + loc: DateLocalizer + ) => `${loc.format(range.start, agendaHeaderPattern)} – ${loc.format(range.end, agendaHeaderPattern)}`; + } + // Month day numbers - const monthCellDate = byType.get("month")?.customViewCellDateFormat; + const monthCellDate = getPattern(byType.get("month")?.customViewCellDateFormat); if (monthCellDate) { formats.dateFormat = (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, monthCellDate); } // Time gutters - const weekTimeGutter = byType.get("week")?.customViewGutterTimeFormat; - const dayTimeGutter = byType.get("day")?.customViewGutterTimeFormat; - const workWeekTimeGutter = byType.get("work_week")?.customViewGutterTimeFormat; + const weekTimeGutter = getPattern(byType.get("week")?.customViewGutterTimeFormat); + const dayTimeGutter = getPattern(byType.get("day")?.customViewGutterTimeFormat); + const workWeekTimeGutter = getPattern(byType.get("work_week")?.customViewGutterTimeFormat); const chosenTimeGutter = weekTimeGutter || dayTimeGutter || workWeekTimeGutter; if (chosenTimeGutter) { formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, chosenTimeGutter); } - const agendaTime = byType.get("agenda")?.customViewGutterTimeFormat; + const agendaTime = getPattern(byType.get("agenda")?.customViewGutterTimeFormat); if (agendaTime) { formats.agendaTimeFormat = (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, agendaTime); } - const agendaDate = byType.get("agenda")?.customViewGutterDateFormat; + const agendaDate = getPattern(byType.get("agenda")?.customViewGutterDateFormat); if (agendaDate) { formats.agendaDateFormat = (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, agendaDate); From 934caf0a68e3fce65d64698d6f67e8dafca376d0 Mon Sep 17 00:00:00 2001 From: Rahman Date: Wed, 22 Oct 2025 16:50:47 +0200 Subject: [PATCH 11/14] fix(calendar-web): standart view preview, rename custom week to work week --- .../src/Calendar.editorPreview.tsx | 18 +++++++++++++----- .../calendar-web/src/Calendar.xml | 4 ++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx index 849be6af89..006f106d0c 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorPreview.tsx @@ -1,6 +1,6 @@ import classnames from "classnames"; import { ReactElement } from "react"; -import { Calendar, dateFnsLocalizer, EventPropGetter } from "react-big-calendar"; +import { Calendar, dateFnsLocalizer, EventPropGetter, View } from "react-big-calendar"; import { CalendarPreviewProps } from "../typings/CalendarProps"; import { createConfigurableToolbar, CustomToolbar } from "./components/Toolbar"; import { eventPropGetter, format, getDay, parse, startOfWeek } from "./utils/calendar-utils"; @@ -75,27 +75,35 @@ export function preview(props: CalendarPreviewProps): ReactElement { // Cast eventPropGetter to satisfy preview Calendar generic const previewEventPropGetter = eventPropGetter as unknown as EventPropGetter<(typeof events)[0]>; + const isCustomView = props.view === "custom"; const toolbar = - props.view === "custom" && props.toolbarItems?.length + isCustomView && props.toolbarItems?.length ? createConfigurableToolbar( props.toolbarItems.map(i => ({ itemType: i.itemType, position: i.position, caption: i.caption, - renderMode: i.renderMode + renderMode: i.renderMode, + customButtonTooltip: undefined, + customButtonStyle: i.buttonStyle })) as any ) : CustomToolbar; + const defaultView = isCustomView ? props.defaultViewCustom : props.defaultViewStandard; + const views: View[] = isCustomView + ? (["day", "week", "month", "work_week"] as View[]) + : (["day", "week", "month"] as View[]); + return (
diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.xml b/packages/pluggableWidgets/calendar-web/src/Calendar.xml index a31259b0a9..f9850a7a66 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.xml +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.xml @@ -102,7 +102,7 @@ Day Week Month - Custom + Work week Agenda
@@ -148,7 +148,7 @@ Month view button Agenda view button Week view button - Custom week view button + Work week view button Title date text Previous button Next button From 72ac3f3cb5c46a6f6218df198a140c8e1fd697ed Mon Sep 17 00:00:00 2001 From: Rahman Date: Fri, 24 Oct 2025 16:16:58 +0200 Subject: [PATCH 12/14] feat(calendar-web): add localization --- .../calendar-web/src/Calendar.editorConfig.ts | 2 +- .../calendar-web/src/Calendar.tsx | 9 +- .../src/__tests__/Calendar.spec.tsx | 26 +++- .../__snapshots__/Calendar.spec.tsx.snap | 12 +- .../src/helpers/CalendarPropsBuilder.ts | 120 ++++++++---------- .../calendar-web/src/helpers/useLocalizer.ts | 110 ++++++++++++++++ .../calendar-web/src/utils/calendar-utils.ts | 32 ++++- .../calendar-web/typings/global.d.ts | 51 ++++++++ 8 files changed, 287 insertions(+), 75 deletions(-) create mode 100644 packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts create mode 100644 packages/pluggableWidgets/calendar-web/typings/global.d.ts diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts index 1556c8c4ea..b703170b28 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.editorConfig.ts @@ -44,7 +44,7 @@ export function getProperties(values: CalendarPreviewProps, defaultProperties: P "customViewShowSunday" ]); } else { - hidePropertyIn(defaultProperties, values, "defaultViewStandard"); + hidePropertiesIn(defaultProperties, values, ["defaultViewStandard", "topBarDateFormat"]); } values.toolbarItems?.forEach((item, index) => { diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx index 5642178fef..a7d8d31d52 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx @@ -6,6 +6,7 @@ import { DnDCalendar } from "./utils/calendar-utils"; import { constructWrapperStyle } from "./utils/style-utils"; import "./ui/Calendar.scss"; import { useCalendarEvents } from "./helpers/useCalendarEvents"; +import { useLocalizer } from "./helpers/useLocalizer"; export default function MxCalendar(props: CalendarContainerProps): ReactElement { // useMemo with empty dependency array is used @@ -15,10 +16,14 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement const wrapperStyle = useMemo(() => constructWrapperStyle(props), []); // eslint-disable-next-line react-hooks/exhaustive-deps const calendarController = useMemo(() => new CalendarPropsBuilder(props), []); + + // Get locale-aware localizer + const { localizer, culture } = useLocalizer(); + const calendarProps = useMemo(() => { calendarController.updateProps(props); - return calendarController.build(); - }, [props, calendarController]); + return calendarController.build(localizer, culture); + }, [props, calendarController, localizer, culture]); const calendarEvents = useCalendarEvents(props); return ( diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index 6725bc192d..2bd2baeee0 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -9,8 +9,30 @@ jest.mock("react-big-calendar", () => { const originalModule = jest.requireActual("react-big-calendar"); return { ...originalModule, - Calendar: ({ children, ...props }: any) => ( -
+ Calendar: ({ + children, + defaultView, + culture, + resizable, + selectable, + showAllEvents, + min, + max, + events, + ...domProps + }: any) => ( +
{children}
), 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 c99cf60c98..b7d86299ff 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 @@ -7,14 +7,18 @@ exports[`Calendar renders correctly with basic props 1`] = ` >
diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index c6e308c22f..3f078edd77 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -2,7 +2,7 @@ import { ObjectItem } from "mendix"; import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar"; import { CalendarContainerProps } from "../../typings/CalendarProps"; import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar"; -import { eventPropGetter, localizer } from "../utils/calendar-utils"; +import { eventPropGetter } from "../utils/calendar-utils"; import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings"; import { CustomWeekController } from "./CustomWeekController"; @@ -32,8 +32,8 @@ export class CalendarPropsBuilder { this.toolbarItems = this.buildToolbarItems(); } - build(): DragAndDropCalendarProps { - const formats = this.buildFormats(); + build(localizer: DateLocalizer, culture: string): DragAndDropCalendarProps { + const formats = this.buildFormats(localizer); const views = this.buildVisibleViews(); const toolbar = this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0 @@ -50,6 +50,8 @@ export class CalendarPropsBuilder { const safeDefaultView = enabledViews.includes(this.defaultView) ? this.defaultView : enabledViews[0]; return { + localizer, + culture, components: { toolbar }, @@ -57,7 +59,6 @@ export class CalendarPropsBuilder { messages: this.buildMessages(workWeekCaption), events: this.events, formats, - localizer, resizable: this.props.editable.value ?? true, selectable: this.props.editable.value ?? true, views, @@ -121,114 +122,101 @@ export class CalendarPropsBuilder { } } - private buildFormats(): Formats { + private buildFormats(_localizer: DateLocalizer): Formats { const formats: Formats = {}; const timePattern = this.getSafeTimePattern(); if (timePattern) { - const formatWith = (date: Date, localizer: DateLocalizer, fallback = "p"): string => { + const formatWith = (date: Date, culture: string, loc: DateLocalizer, fallback = "p"): string => { try { - return localizer.format(date, timePattern); + return loc.format(date, timePattern, culture); } catch (e) { console.warn( `[Calendar] Failed to format time using pattern "${timePattern}" – falling back to default pattern "${fallback}".`, e ); - return localizer.format(date, fallback); + return loc.format(date, fallback, culture); } }; - formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) => formatWith(date, loc); + formats.timeGutterFormat = (date: Date, culture: string, loc: DateLocalizer) => + formatWith(date, culture, loc); formats.eventTimeRangeFormat = ( { start, end }: { start: Date; end: Date }, - _culture: string, + culture: string, loc: DateLocalizer - ) => `${formatWith(start, loc)} – ${formatWith(end, loc)}`; + ) => `${formatWith(start, culture, loc)} – ${formatWith(end, culture, loc)}`; formats.agendaTimeRangeFormat = ( { start, end }: { start: Date; end: Date }, - _culture: string, + culture: string, loc: DateLocalizer - ) => `${formatWith(start, loc)} – ${formatWith(end, loc)}`; + ) => `${formatWith(start, culture, loc)} – ${formatWith(end, culture, loc)}`; } const titlePattern = this.props.topBarDateFormat?.value?.trim(); if (titlePattern) { - formats.dayHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) => - loc.format(date, titlePattern); - formats.monthHeaderFormat = (date: Date, _culture: string, loc: DateLocalizer) => - loc.format(date, titlePattern); + formats.dayHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, titlePattern, culture); + formats.monthHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, titlePattern, culture); formats.dayRangeHeaderFormat = ( { start, end }: { start: Date; end: Date }, - _culture: string, + culture: string, loc: DateLocalizer - ) => `${loc.format(start, titlePattern)} – ${loc.format(end, titlePattern)}`; + ) => `${loc.format(start, titlePattern, culture)} – ${loc.format(end, titlePattern, culture)}`; formats.agendaHeaderFormat = ( { start, end }: { start: Date; end: Date }, - _culture: string, + culture: string, loc: DateLocalizer - ) => `${loc.format(start, titlePattern)} – ${loc.format(end, titlePattern)}`; + ) => `${loc.format(start, titlePattern, culture)} – ${loc.format(end, titlePattern, culture)}`; } // Apply per-view custom formats only in custom view mode if (this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0) { const byType = new Map(this.toolbarItems.map(i => [i.itemType, i])); - type HeaderFormat = Formats["dayHeaderFormat"]; - const applyHeader = (pattern?: string, existing?: HeaderFormat): HeaderFormat | undefined => { - if (!pattern || pattern.trim() === "") return existing; - return (date: Date, _culture: string, loc: DateLocalizer) => loc.format(date, pattern); - }; - - // Helper to get non-empty pattern or fallback - const getPattern = (pattern?: string, fallback?: string): string | undefined => { + // Helper to get non-empty pattern + const getPattern = (pattern?: string): string | undefined => { const trimmed = pattern?.trim(); - return trimmed && trimmed.length > 0 ? trimmed : fallback; + return trimmed && trimmed.length > 0 ? trimmed : undefined; }; - const dayHeaderPattern = getPattern( - byType.get("day")?.customViewHeaderDayFormat, - this.props.topBarDateFormat?.value - ); - const weekHeaderPattern = getPattern( - byType.get("week")?.customViewHeaderDayFormat || byType.get("work_week")?.customViewHeaderDayFormat, - this.props.topBarDateFormat?.value - ); - const monthHeaderPattern = getPattern( - byType.get("month")?.customViewHeaderDayFormat, - this.props.topBarDateFormat?.value - ); - const agendaHeaderPattern = getPattern( - byType.get("agenda")?.customViewHeaderDayFormat, - this.props.topBarDateFormat?.value - ); - - // Only apply if we have a valid pattern + // Header formats + const dayHeaderPattern = getPattern(byType.get("day")?.customViewHeaderDayFormat); if (dayHeaderPattern) { - formats.dayHeaderFormat = applyHeader(dayHeaderPattern, formats.dayHeaderFormat); + formats.dayHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, dayHeaderPattern, culture); } + + const weekHeaderPattern = getPattern( + byType.get("week")?.customViewHeaderDayFormat || byType.get("work_week")?.customViewHeaderDayFormat + ); if (weekHeaderPattern) { formats.dayRangeHeaderFormat = ( range: { start: Date; end: Date }, - _culture: string, + culture: string, loc: DateLocalizer - ) => `${loc.format(range.start, weekHeaderPattern)} – ${loc.format(range.end, weekHeaderPattern)}`; + ) => + `${loc.format(range.start, weekHeaderPattern, culture)} – ${loc.format(range.end, weekHeaderPattern, culture)}`; } + + const monthHeaderPattern = getPattern(byType.get("month")?.customViewHeaderDayFormat); if (monthHeaderPattern) { - formats.monthHeaderFormat = applyHeader(monthHeaderPattern, formats.monthHeaderFormat); + formats.monthHeaderFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, monthHeaderPattern, culture); } + + const agendaHeaderPattern = getPattern(byType.get("agenda")?.customViewHeaderDayFormat); if (agendaHeaderPattern) { - formats.agendaHeaderFormat = ( - range: { start: Date; end: Date }, - _culture: string, - loc: DateLocalizer - ) => `${loc.format(range.start, agendaHeaderPattern)} – ${loc.format(range.end, agendaHeaderPattern)}`; + formats.agendaHeaderFormat = (range: { start: Date; end: Date }, culture: string, loc: DateLocalizer) => + `${loc.format(range.start, agendaHeaderPattern, culture)} – ${loc.format(range.end, agendaHeaderPattern, culture)}`; } // Month day numbers const monthCellDate = getPattern(byType.get("month")?.customViewCellDateFormat); if (monthCellDate) { - formats.dateFormat = (date: Date, _culture: string, loc: DateLocalizer) => - loc.format(date, monthCellDate); + formats.dateFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, monthCellDate, culture); } // Time gutters @@ -237,18 +225,20 @@ export class CalendarPropsBuilder { const workWeekTimeGutter = getPattern(byType.get("work_week")?.customViewGutterTimeFormat); const chosenTimeGutter = weekTimeGutter || dayTimeGutter || workWeekTimeGutter; if (chosenTimeGutter) { - formats.timeGutterFormat = (date: Date, _culture: string, loc: DateLocalizer) => - loc.format(date, chosenTimeGutter); + formats.timeGutterFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, chosenTimeGutter, culture); } + const agendaTime = getPattern(byType.get("agenda")?.customViewGutterTimeFormat); if (agendaTime) { - formats.agendaTimeFormat = (date: Date, _culture: string, loc: DateLocalizer) => - loc.format(date, agendaTime); + formats.agendaTimeFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, agendaTime, culture); } + const agendaDate = getPattern(byType.get("agenda")?.customViewGutterDateFormat); if (agendaDate) { - formats.agendaDateFormat = (date: Date, _culture: string, loc: DateLocalizer) => - loc.format(date, agendaDate); + formats.agendaDateFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, agendaDate, culture); } } diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts b/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts new file mode 100644 index 0000000000..1a28924030 --- /dev/null +++ b/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts @@ -0,0 +1,110 @@ +import { useMemo } from "react"; +import { dateFnsLocalizer, DateLocalizer } from "react-big-calendar"; +import { format, FormatLong, getDay, Locale, Localize, Match, parse, startOfWeek } from "date-fns"; +import { getMendixLocale } from "../utils/calendar-utils"; + +/** + * Creates a minimal date-fns compatible Locale object from Mendix locale data. + * This allows us to use Mendix's localized weekday/month names directly without + * importing external date-fns locale files. + */ +function createLocaleFromMendixData(mendixLocale: ReturnType): Locale { + if (!mendixLocale?.dates) { + // Return a minimal default locale structure + return { + code: "en-US", + localize: {} as Localize, + formatLong: {} as FormatLong, + formatDistance: () => "", + formatRelative: () => "", + match: {} as Match, + options: { + weekStartsOn: 0, + firstWeekContainsDate: 1 + } + }; + } + + const { dates, firstDayOfWeek, languageTag } = mendixLocale; + + // Create a minimal locale object that date-fns can use + // We provide localize.month and localize.day functions with support for different widths + // (wide, abbreviated, narrow) since RBC uses these for displaying month/weekday names + const locale: Locale = { + code: languageTag?.replace("_", "-") || "en-US", + localize: { + month: (monthIndex: number, options?: { width?: "wide" | "abbreviated" | "narrow" }) => { + const width = options?.width || "wide"; + if (width === "abbreviated") { + return dates.abbreviatedMonths?.[monthIndex] || dates.months?.[monthIndex] || ""; + } + return dates.months?.[monthIndex] || ""; + }, + day: (dayIndex: number, options?: { width?: "wide" | "abbreviated" | "short" | "narrow" }) => { + const width = options?.width || "wide"; + if (width === "abbreviated" || width === "short") { + return dates.shortWeekdays?.[dayIndex] || dates.weekdays?.[dayIndex] || ""; + } + return dates.weekdays?.[dayIndex] || ""; + }, + // Minimal implementations for other required methods + ordinalNumber: (n: number) => String(n), + era: () => "", + quarter: () => "", + dayPeriod: (dayPeriodEnumValue: string) => { + if (dayPeriodEnumValue === "am") return dates.dayPeriods?.[0] || "AM"; + if (dayPeriodEnumValue === "pm") return dates.dayPeriods?.[1] || "PM"; + return dayPeriodEnumValue; + } + } as Localize, + formatLong: { + date: () => "P", + time: () => "p", + dateTime: () => "Pp" + } as FormatLong, + formatDistance: () => "", + formatRelative: () => "", + match: {} as Match, + options: { + weekStartsOn: firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6, + firstWeekContainsDate: 1 + } + }; + + return locale; +} + +/** + * Creates a locale-aware localizer that updates reactively when Mendix locale changes. + * Uses Mendix's locale data directly to avoid external locale imports. + */ +export function useLocalizer(): { localizer: DateLocalizer; culture: string } { + const mendixLocale = getMendixLocale(); + const culture = mendixLocale?.languageTag?.replace("_", "-") || "en-US"; + const firstDayOfWeek = mendixLocale?.firstDayOfWeek ?? 0; + + const localizer = useMemo(() => { + // Create a custom locale object from Mendix data + const customLocale = createLocaleFromMendixData(mendixLocale); + + const locales: { [key: string]: Locale } = { + [culture]: customLocale + }; + + return dateFnsLocalizer({ + format, + parse, + startOfWeek: (date: Date) => { + // Use Mendix's firstDayOfWeek setting + return startOfWeek(date, { weekStartsOn: firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6 }); + }, + getDay, + locales + }); + }, [culture, firstDayOfWeek, mendixLocale]); + + return { + localizer, + culture + }; +} diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 8db171b8f7..7653293358 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -15,6 +15,7 @@ import { startOfMonth, startOfWeek } from "date-fns"; +import type { MXLocaleDates, MXLocaleNumbers, MXLocalePatterns, MXSessionData } from "../../typings/global"; // Utility to lighten hex colors. Accepts #RGB or #RRGGBB. function lightenColor(color: string, amount = 0.2): string { @@ -106,10 +107,39 @@ export function getViewRange(view: string, date: Date): { start: Date; end: Date } } +// Helper to get Mendix session locale +export interface MendixLocaleData { + code: string; + languageTag: string; + firstDayOfWeek: number; + dates?: MXLocaleDates; + patterns?: MXLocalePatterns; + numbers?: MXLocaleNumbers; +} + +export function getMendixLocale(): MendixLocaleData | null { + try { + const sessionData: MXSessionData | undefined = window.mx?.session?.sessionData; + if (sessionData?.locale) { + return { + code: sessionData.locale.code || "en-US", + languageTag: sessionData.locale.languageTag || sessionData.locale.code || "en-US", + firstDayOfWeek: sessionData.locale.firstDayOfWeek ?? 0, + dates: sessionData.locale.dates, + patterns: sessionData.locale.patterns, + numbers: sessionData.locale.numbers + }; + } + } catch (e) { + console.warn("[Calendar] Failed to get Mendix locale:", e); + } + return null; +} + export const localizer = dateFnsLocalizer({ format, parse, startOfWeek, getDay, - locales: {} + locales: {} // Will be populated dynamically }); diff --git a/packages/pluggableWidgets/calendar-web/typings/global.d.ts b/packages/pluggableWidgets/calendar-web/typings/global.d.ts new file mode 100644 index 0000000000..78777e898e --- /dev/null +++ b/packages/pluggableWidgets/calendar-web/typings/global.d.ts @@ -0,0 +1,51 @@ +// TypeScript interfaces for Mendix global objects + +export interface MXLocalePatterns { + date: string; + datetime: string; + time: string; +} + +export interface MXLocaleDates { + weekdays: string[]; + shortWeekdays: string[]; + months: string[]; + shortMonths: string[]; + abbreviatedMonths: string[]; + abbreviatedShortMonths: string[]; + dayPeriods: string[]; + eras: string[]; +} + +export interface MXLocaleNumbers { + decimalSeparator: string; + groupingSeparator: string; + minusSign: string; +} + +export interface MXSessionLocale { + code: string; + languageTag: string; + firstDayOfWeek: number; + dates: MXLocaleDates; + patterns: MXLocalePatterns; + numbers: MXLocaleNumbers; +} + +export interface MXSessionData { + locale: MXSessionLocale; +} + +export interface MXSession { + sessionData: MXSessionData; +} + +export interface MXGlobalObject { + mx?: { + session?: MXSession; + }; +} + +declare global { + interface Window extends MXGlobalObject {} +} From 230a14921e7a282b72abde360671b84291fe8468 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 28 Oct 2025 09:56:33 +0100 Subject: [PATCH 13/14] fix(calendar-web): custom agenda view fix --- .../src/helpers/CalendarPropsBuilder.ts | 23 ++++++++++--------- .../calendar-web/src/utils/calendar-utils.ts | 10 ++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index 3f078edd77..3f1a49df8a 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -2,7 +2,7 @@ import { ObjectItem } from "mendix"; import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar"; import { CalendarContainerProps } from "../../typings/CalendarProps"; import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar"; -import { eventPropGetter } from "../utils/calendar-utils"; +import { eventPropGetter, getTextValue } from "../utils/calendar-utils"; import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings"; import { CustomWeekController } from "./CustomWeekController"; @@ -318,21 +318,22 @@ export class CalendarPropsBuilder { if (!items || items.length === 0) { return undefined; } + return items.map(i => ({ itemType: i.itemType, position: i.position, - caption: i.caption?.value, + caption: getTextValue(i.caption?.value), renderMode: i.renderMode, - customButtonTooltip: i.buttonTooltip?.value, + customButtonTooltip: getTextValue(i.buttonTooltip?.value), customButtonStyle: i.buttonStyle, - customViewHeaderDayFormat: i.customViewHeaderDayFormat?.value, - customViewCellDateFormat: i.customViewCellDateFormat?.value, - customViewGutterTimeFormat: i.customViewGutterTimeFormat?.value, - customViewGutterDateFormat: i.customViewGutterDateFormat?.value, - customViewAllDayText: i.customViewAllDayText?.value, - customViewTextHeaderDate: i.customViewTextHeaderDate?.value, - customViewTextHeaderTime: i.customViewTextHeaderTime?.value, - customViewTextHeaderEvent: i.customViewTextHeaderEvent?.value + customViewHeaderDayFormat: getTextValue(i.customViewHeaderDayFormat?.value), + customViewCellDateFormat: getTextValue(i.customViewCellDateFormat?.value), + customViewGutterTimeFormat: getTextValue(i.customViewGutterTimeFormat?.value), + customViewGutterDateFormat: getTextValue(i.customViewGutterDateFormat?.value), + customViewAllDayText: getTextValue(i.customViewAllDayText?.value), + customViewTextHeaderDate: getTextValue(i.customViewTextHeaderDate?.value), + customViewTextHeaderTime: getTextValue(i.customViewTextHeaderTime?.value), + customViewTextHeaderEvent: getTextValue(i.customViewTextHeaderEvent?.value) })); } } diff --git a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts index 7653293358..6ef481ce3b 100644 --- a/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts +++ b/packages/pluggableWidgets/calendar-web/src/utils/calendar-utils.ts @@ -107,6 +107,16 @@ export function getViewRange(view: string, date: Date): { start: Date; end: Date } } +/** + * Converts empty or whitespace-only strings to undefined. + * Useful for handling optional textTemplate values from Mendix. + * @param value - The string value to check + * @returns The trimmed value if non-empty, otherwise undefined + */ +export function getTextValue(value?: string): string | undefined { + return value && value.trim().length > 0 ? value : undefined; +} + // Helper to get Mendix session locale export interface MendixLocaleData { code: string; From d08fb41b5504a018e2fb74232ba32aec1fd14b88 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 28 Oct 2025 13:19:42 +0100 Subject: [PATCH 14/14] fix(calendar-web): custom view fix --- .../calendar-web/src/helpers/useLocalizer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts b/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts index 1a28924030..4e304078a6 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/useLocalizer.ts @@ -58,9 +58,11 @@ function createLocaleFromMendixData(mendixLocale: ReturnType "P", - time: () => "p", - dateTime: () => "Pp" + // Use actual format strings from Mendix patterns, not tokens + // These are used when P/p tokens are encountered in format strings + date: () => mendixLocale?.patterns?.date || "MM/dd/yyyy", + time: () => mendixLocale?.patterns?.time || "h:mm a", + dateTime: () => mendixLocale?.patterns?.datetime || "MM/dd/yyyy, h:mm a" } as FormatLong, formatDistance: () => "", formatRelative: () => "",