From 65897c1745e4cc221ab80a07993d9c8f75b48813 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 7 Jul 2026 16:37:58 +0200 Subject: [PATCH 1/3] fix(calendar-web): apply custom Header day format to column headers In Custom view the Header day format was only wired into react-big-calendar's toolbar-title keys (dayHeaderFormat/monthHeaderFormat), so the day/week/month column headers still used RBC's default dayFormat (dd eee, rendering "07 Tue") and the user's pattern was silently dropped. Wire customViewHeaderDayFormat into RBC's per-column keys: dayFormat (day/week columns) and weekdayFormat (month weekday headers). RBC shares a single global dayFormat across day and week, so when both items set different patterns the week pattern wins (matching the existing timeGutterFormat precedence) and a warning is logged. Add unit tests covering both formats, the collision precedence, and the no-format regression. --- .../calendar-web/CHANGELOG.md | 4 + .../src/__tests__/Calendar.spec.tsx | 76 +++++++++++++++++++ .../src/helpers/CalendarPropsBuilder.ts | 28 +++++++ 3 files changed, 108 insertions(+) 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 e31691c5cf..a1ee536033 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -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..e159b3e8da 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -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) => From 8bbcb931211b4e8fdfb7a73c16e594822f398ddc Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 7 Jul 2026 17:09:18 +0200 Subject: [PATCH 2/3] style(calendar-web): fix import order --- .../calendar-web/src/__tests__/Calendar.spec.tsx | 2 +- .../calendar-web/src/helpers/CalendarPropsBuilder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index a1ee536033..265e28404b 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 diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index e159b3e8da..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; From 4daebe8ad2edbf37f5ec0b1e93d3b67f9db47f00 Mon Sep 17 00:00:00 2001 From: Rahman Date: Tue, 21 Jul 2026 15:36:43 +0200 Subject: [PATCH 3/3] fix(calendar-web): resolve dayFormat by active view to prevent week/work_week header leak --- .../calendar-web/src/Calendar.tsx | 16 +++- .../src/__tests__/Calendar.spec.tsx | 58 +++++++++---- .../__snapshots__/Calendar.spec.tsx.snap | 1 + .../src/helpers/CalendarPropsBuilder.ts | 82 +++++++++++-------- 4 files changed, 103 insertions(+), 54 deletions(-) diff --git a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx index 878bb94fb4..a60dbfaae3 100644 --- a/packages/pluggableWidgets/calendar-web/src/Calendar.tsx +++ b/packages/pluggableWidgets/calendar-web/src/Calendar.tsx @@ -1,5 +1,6 @@ -import { Fragment, ReactElement, useMemo } from "react"; +import { Fragment, ReactElement, useCallback, useMemo, useState } from "react"; import classNames from "classnames"; +import { View } from "react-big-calendar"; import { CalendarContainerProps } from "../typings/CalendarProps"; import { CalendarPropsBuilder } from "./helpers/CalendarPropsBuilder"; import { DnDCalendar } from "./utils/calendar-utils"; @@ -20,10 +21,17 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement // Get locale-aware localizer const { localizer, culture } = useLocalizer(); + // The calendar is controlled on `view` so the props builder always knows which view is on + // screen — needed to resolve the shared RBC `dayFormat` key (week/work_week/day column + // headers) to that view's own custom pattern instead of a sibling view's. Undefined until + // RBC reports its first view (via defaultView), at which point the builder picks a safe one. + const [activeView, setActiveView] = useState(undefined); + const handleView = useCallback((view: View) => setActiveView(view), []); + const calendarProps = useMemo(() => { calendarController.updateProps(props); - return calendarController.build(localizer, culture); - }, [props, calendarController, localizer, culture]); + return calendarController.build(localizer, culture, activeView); + }, [props, calendarController, localizer, culture, activeView]); const calendarEvents = useCalendarEvents(props); @@ -33,7 +41,7 @@ export default function MxCalendar(props: CalendarContainerProps): ReactElement ) : (
- +
)} diff --git a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx index 265e28404b..50c72a9ac1 100644 --- a/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx +++ b/packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx @@ -258,7 +258,7 @@ describe("CalendarPropsBuilder validation", () => { }); describe("CalendarPropsBuilder column header formats", () => { - const buildFormats = (toolbarItems: CalendarContainerProps["toolbarItems"]): any => { + const buildFormats = (toolbarItems: CalendarContainerProps["toolbarItems"], activeView?: string): any => { const localizer = { format: jest.fn((_date: Date, pattern: string) => pattern), parse: jest.fn(), @@ -267,7 +267,8 @@ describe("CalendarPropsBuilder column header formats", () => { messages: {} } as any; const builder = new CalendarPropsBuilder({ ...customViewProps, toolbarItems }); - return { formats: builder.build(localizer, "en").formats, localizer }; + const built = builder.build(localizer, "en", activeView as any); + return { formats: built.formats, view: built.view, localizer }; }; const toolbarItem = ( @@ -284,7 +285,7 @@ describe("CalendarPropsBuilder column header formats", () => { 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") }) + toolbarItem("day", { customViewHeaderDayFormat: dynamic.available("EE dd-MM") }) ]); expect(typeof formats.dayFormat).toBe("function"); @@ -294,7 +295,7 @@ describe("CalendarPropsBuilder column header formats", () => { it("wires 'Header day format' on a month item into RBC weekdayFormat", () => { const { formats, localizer } = buildFormats([ - toolbarItem("month", { customViewHeaderDayFormat: dynamic("EEEE") }) + toolbarItem("month", { customViewHeaderDayFormat: dynamic.available("EEEE") }) ]); expect(typeof formats.weekdayFormat).toBe("function"); @@ -302,21 +303,46 @@ describe("CalendarPropsBuilder column header formats", () => { 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") }) - ]); + it("resolves the shared dayFormat from whichever view is active, not a sibling view", () => { + // Week and work_week both configured with their OWN, different "Header day format". + // RBC shares a single `dayFormat` key across both (and day), so the builder must pick + // the pattern that belongs to whatever view is actually on screen (`activeView`). + const items = [ + toolbarItem("day", { customViewHeaderDayFormat: dynamic.available("dd") }), + toolbarItem("week", { customViewHeaderDayFormat: dynamic.available("EE dd-MM") }), + toolbarItem("work_week", { customViewHeaderDayFormat: dynamic.available("MMM dd") }) + ]; + + const week = buildFormats(items, "week"); + week.formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", week.localizer); + expect(week.localizer.format).toHaveBeenCalledWith(expect.any(Date), "EE dd-MM", "en"); + + const workWeek = buildFormats(items, "work_week"); + workWeek.formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", workWeek.localizer); + expect(workWeek.localizer.format).toHaveBeenCalledWith(expect.any(Date), "MMM dd", "en"); + + const day = buildFormats(items, "day"); + day.formats.dayFormat(new Date("2025-04-28T12:00:00Z"), "en", day.localizer); + expect(day.localizer.format).toHaveBeenCalledWith(expect.any(Date), "dd", "en"); + }); - 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("falls back to RBC's default when the active view (work_week) has no pattern of its own", () => { + // Regression: work_week previously inherited week's pattern instead of falling back. + // week HAS a custom pattern, work_week does NOT — while work_week is active, dayFormat + // must stay unset so RBC's own default ("dd eee") is used, not week's pattern. + const { formats } = buildFormats( + [ + toolbarItem("week", { customViewHeaderDayFormat: dynamic.available("EE dd-MM") }), + toolbarItem("work_week") + ], + "work_week" + ); + + expect(formats.dayFormat).toBeUndefined(); }); it("leaves dayFormat/weekdayFormat unset when no header format is configured (RBC defaults preserved)", () => { - const { formats } = buildFormats([toolbarItem("day"), toolbarItem("month")]); + const { formats } = buildFormats([toolbarItem("day"), toolbarItem("month")], "day"); expect(formats.dayFormat).toBeUndefined(); expect(formats.weekdayFormat).toBeUndefined(); @@ -324,7 +350,7 @@ describe("CalendarPropsBuilder column header formats", () => { it("wires 'Time gutter format' on a day item into RBC timeGutterFormat", () => { const { formats, localizer } = buildFormats([ - toolbarItem("day", { customViewGutterTimeFormat: dynamic("HH:mm") }) + toolbarItem("day", { customViewGutterTimeFormat: dynamic.available("HH:mm") }) ]); expect(typeof formats.timeGutterFormat).toBe("function"); 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 63c3b68dfc..422306a487 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 @@ -21,6 +21,7 @@ exports[`Calendar renders correctly with basic props 1`] = ` max="Mon Apr 28 2025 23:59:59 GMT+0000 (Coordinated Universal Time)" messages="[object Object]" min="Mon Apr 28 2025 00:00:00 GMT+0000 (Coordinated Universal Time)" + view="day" views="[object Object]" /> diff --git a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts index d7cbaeeaed..7f33a8b198 100644 --- a/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts +++ b/packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts @@ -1,5 +1,5 @@ import { ObjectItem } from "mendix"; -import { DateLocalizer, Formats, ViewsProps } from "react-big-calendar"; +import { DateLocalizer, Formats, View, ViewsProps } from "react-big-calendar"; import { CustomWeekController } from "./CustomWeekController"; import { CalendarContainerProps } from "../../typings/CalendarProps"; import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar"; @@ -8,7 +8,7 @@ import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings"; export class CalendarPropsBuilder { private visibleDays: Set; - private defaultView: "month" | "week" | "work_week" | "day" | "agenda"; + private defaultView: View; private isCustomView: boolean; private events: CalendarEvent[]; private minTime: Date; @@ -48,8 +48,7 @@ export class CalendarPropsBuilder { this.defaultDate = props.startDateAttribute?.value; } - build(localizer: DateLocalizer, culture: string): DragAndDropCalendarProps { - const formats = this.buildFormats(localizer); + build(localizer: DateLocalizer, culture: string, activeView?: View): DragAndDropCalendarProps { const views = this.buildVisibleViews(); const toolbar = this.isCustomView && this.toolbarItems && this.toolbarItems.length > 0 @@ -62,15 +61,22 @@ export class CalendarPropsBuilder { // 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"); + .map(([view]) => view as View); const safeDefaultView = enabledViews.includes(this.defaultView) ? this.defaultView : enabledViews[0]; + // The view RBC is actually showing right now. Falls back to safeDefaultView when the + // caller hasn't told us yet (first render) or the reported view is no longer enabled. + const effectiveView = activeView && enabledViews.includes(activeView) ? activeView : safeDefaultView; + + const formats = this.buildFormats(localizer, effectiveView); + return { localizer, culture, components: { toolbar }, + view: effectiveView, defaultView: safeDefaultView, messages: this.buildMessages(workWeekCaption), events: this.events, @@ -141,7 +147,10 @@ export class CalendarPropsBuilder { } } - private buildFormats(_localizer: DateLocalizer): Formats { + private buildFormats( + _localizer: DateLocalizer, + activeView: "day" | "week" | "work_week" | "month" | "agenda" + ): Formats { const formats: Formats = {}; const timePattern = this.getSafeTimePattern(); @@ -207,17 +216,8 @@ export class CalendarPropsBuilder { 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, - loc: DateLocalizer - ) => - `${loc.format(range.start, weekHeaderPattern, culture)} – ${loc.format(range.end, weekHeaderPattern, culture)}`; - } + const weekOwnPattern = getPattern(byType.get("week")?.customViewHeaderDayFormat); + const workWeekOwnPattern = getPattern(byType.get("work_week")?.customViewHeaderDayFormat); const monthHeaderPattern = getPattern(byType.get("month")?.customViewHeaderDayFormat); if (monthHeaderPattern) { @@ -226,33 +226,47 @@ export class CalendarPropsBuilder { } // 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. + // RBC renders the "07 Tue" day-column headers via a single, global `dayFormat` key shared + // by Day, Week AND our custom work_week view (all render through 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.` - ); - } + // Because `dayFormat` is shared, we can't set it once for all views — a pattern meant for + // Week would leak into work_week (and vice versa). Instead resolve it from whichever view + // is actually on screen right now, and only for the views that render through TimeGridHeader. + // When that view has no custom pattern of its own, leave `dayFormat` unset so RBC's default + // ("dd eee") applies — no more inheriting a sibling view's pattern. + const columnDayPattern: string | undefined = + activeView === "day" + ? dayHeaderPattern + : activeView === "week" + ? weekOwnPattern + : activeView === "work_week" + ? workWeekOwnPattern + : undefined; if (columnDayPattern) { formats.dayFormat = (date: Date, culture: string, loc: DateLocalizer) => loc.format(date, columnDayPattern, culture); } + // Toolbar title range for week/work_week — same active-view resolution as the column + // header above, so the title and the columns always agree on which pattern is showing. + const weekRangePattern = + activeView === "week" ? weekOwnPattern : activeView === "work_week" ? workWeekOwnPattern : undefined; + if (weekRangePattern) { + formats.dayRangeHeaderFormat = ( + range: { start: Date; end: Date }, + culture: string, + loc: DateLocalizer + ) => + `${loc.format(range.start, weekRangePattern, culture)} – ${loc.format(range.end, weekRangePattern, culture)}`; + } + const agendaHeaderPattern = getPattern(byType.get("agenda")?.customViewHeaderDayFormat); if (agendaHeaderPattern) { formats.agendaHeaderFormat = (range: { start: Date; end: Date }, culture: string, loc: DateLocalizer) =>