diff --git a/packages/pluggableWidgets/calendar-web/CHANGELOG.md b/packages/pluggableWidgets/calendar-web/CHANGELOG.md index 3d7b78ae98..9e222df503 100644 --- a/packages/pluggableWidgets/calendar-web/CHANGELOG.md +++ b/packages/pluggableWidgets/calendar-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue in Custom view where the "Header day format" was only applied to the toolbar title and not to the day, week, and month column headers. + ## [2.4.0] - 2026-03-20 ### Fixed diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index 6174878704..59b646d065 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -1,8 +1,8 @@ import { render, screen } from "@testing-library/react"; import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils"; -import MxCalendar from "../Calendar"; import { CalendarContainerProps } from "../../typings/CalendarProps"; +import MxCalendar from "../Calendar"; import { CalendarPropsBuilder } from "../helpers/CalendarPropsBuilder"; // Mock react-big-calendar to avoid View.title issues @@ -256,3 +256,79 @@ describe("CalendarPropsBuilder validation", () => { expect(result.timeslots).toBe(2); }); }); + +describe("CalendarPropsBuilder column header formats", () => { + const buildFormats = (toolbarItems: CalendarContainerProps["toolbarItems"]): any => { + const localizer = { + format: jest.fn((_date: Date, pattern: string) => pattern), + parse: jest.fn(), + startOfWeek: jest.fn(), + getDay: jest.fn(), + messages: {} + } as any; + const builder = new CalendarPropsBuilder({ ...customViewProps, toolbarItems }); + return { formats: builder.build(localizer, "en").formats, localizer }; + }; + + const toolbarItem = ( + itemType: string, + overrides: Partial = {} + ): CalendarContainerProps["toolbarItems"][number] => + ({ + itemType, + position: "left", + renderMode: "button", + buttonStyle: "default", + ...overrides + }) as any; + + it("wires 'Header day format' on a day item into RBC dayFormat (the column header)", () => { + const { formats, localizer } = buildFormats([ + toolbarItem("day", { customViewHeaderDayFormat: dynamic("EE dd-MM") }) + ]); + + expect(typeof formats.dayFormat).toBe("function"); + formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", localizer); + expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "EE dd-MM", "en"); + }); + + it("wires 'Header day format' on a month item into RBC weekdayFormat", () => { + const { formats, localizer } = buildFormats([ + toolbarItem("month", { customViewHeaderDayFormat: dynamic("EEEE") }) + ]); + + expect(typeof formats.weekdayFormat).toBe("function"); + formats.weekdayFormat(new Date("2025-04-28T12:00:00Z"), "en", localizer); + expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "EEEE", "en"); + }); + + it("prefers the week pattern for the shared dayFormat when day and week both set it", () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined); + const { formats, localizer } = buildFormats([ + toolbarItem("day", { customViewHeaderDayFormat: dynamic("dd") }), + toolbarItem("week", { customViewHeaderDayFormat: dynamic("EE dd-MM") }) + ]); + + formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", localizer); + expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "EE dd-MM", "en"); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("shares a single")); + warn.mockRestore(); + }); + + it("leaves dayFormat/weekdayFormat unset when no header format is configured (RBC defaults preserved)", () => { + const { formats } = buildFormats([toolbarItem("day"), toolbarItem("month")]); + + expect(formats.dayFormat).toBeUndefined(); + expect(formats.weekdayFormat).toBeUndefined(); + }); + + it("wires 'Time gutter format' on a day item into RBC timeGutterFormat", () => { + const { formats, localizer } = buildFormats([ + toolbarItem("day", { customViewGutterTimeFormat: dynamic("HH:mm") }) + ]); + + expect(typeof formats.timeGutterFormat).toBe("function"); + formats.timeGutterFormat(new Date("2025-04-28T14:00:00Z"), "en", localizer); + expect(localizer.format).toHaveBeenCalledWith(expect.any(Date), "HH:mm", "en"); + }); +}); diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index d6176e11a6..d7cbaeeaed 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -1,10 +1,10 @@ import { ObjectItem } from "mendix"; import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar"; +import { CustomWeekController } from "./CustomWeekController"; import { CalendarContainerProps } from "../../typings/CalendarProps"; import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar"; import { eventPropGetter, getTextValue } from "../utils/calendar-utils"; import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings"; -import { CustomWeekController } from "./CustomWeekController"; export class CalendarPropsBuilder { private visibleDays: Set; @@ -225,6 +225,34 @@ export class CalendarPropsBuilder { loc.format(date, monthHeaderPattern, culture); } + // Per-column headers — distinct from the toolbar title above. + // RBC renders the "07 Tue" day-column headers via `dayFormat` (week/day time-grid, + // TimeGridHeader.js) and the month weekday headers via `weekdayFormat` (Month.js). + // These are separate from dayHeaderFormat/monthHeaderFormat (the toolbar title), so we + // must set them explicitly or RBC's date-fns defaults ("dd eee") always win. + if (monthHeaderPattern) { + formats.weekdayFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, monthHeaderPattern, culture); + } + + // RBC exposes a single global `dayFormat` shared by the day AND week column headers, so + // when a day item and a week item each carry a different "Header day format" we can only + // honor one. Precedence matches `chosenTimeGutter` below (week → day → work_week) for + // consistency across the shared-key formats. weekHeaderPattern already folds in + // week/work_week; dayHeaderPattern is the "day" item. + const columnDayPattern: string | undefined = weekHeaderPattern || dayHeaderPattern; + if (weekHeaderPattern && dayHeaderPattern && weekHeaderPattern !== dayHeaderPattern) { + console.warn( + `[Calendar] Both week and day "Header day format" are set to different patterns ` + + `("${weekHeaderPattern}" vs "${dayHeaderPattern}"). react-big-calendar shares a single ` + + `column-header format, so "${columnDayPattern}" will be used for both views.` + ); + } + if (columnDayPattern) { + formats.dayFormat = (date: Date, culture: string, loc: DateLocalizer) => + loc.format(date, columnDayPattern, culture); + } + const agendaHeaderPattern = getPattern(byType.get("agenda")?.customViewHeaderDayFormat); if (agendaHeaderPattern) { formats.agendaHeaderFormat = (range: { start: Date; end: Date }, culture: string, loc: DateLocalizer) =>