Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/calendar-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- We added a `showMultiDayTimes` property to control whether start/end times are displayed for multi-day events in the calendar.

## [2.4.0] - 2026-03-20

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/calendar-web/src/Calendar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
<caption>Show all events</caption>
<description>Auto-adjust calendar height to display all events without "more" links</description>
</property>
<property key="showMultiDayTimes" type="boolean" defaultValue="false">
<caption>Show multi-day times</caption>
<description>Show start and end times for events that span multiple days in the week and day views instead of placing them in the all-day row</description>
</property>
<property key="step" type="integer" defaultValue="30">
<caption>Step</caption>
<description>Determines the selectable time increments in week and day views</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { dynamic, ListValueBuilder } from "@mendix/widget-plugin-test-utils";

import MxCalendar from "../Calendar";
import { CalendarContainerProps } from "../../typings/CalendarProps";

Check warning on line 5 in packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx

View workflow job for this annotation

GitHub Actions / Run code quality check

`../../typings/CalendarProps` import should occur before import of `../Calendar`
import { CalendarPropsBuilder } from "../helpers/CalendarPropsBuilder";

// Mock react-big-calendar to avoid View.title issues
Expand All @@ -19,6 +19,9 @@
resizable,
selectable,
showAllEvents,
showMultiDayTimes,
min,
max,
events,
step,
timeslots,
Expand All @@ -34,6 +37,9 @@
data-resizable={resizable}
data-selectable={selectable}
data-show-all-events={showAllEvents}
data-show-multi-day-times={showMultiDayTimes}
data-min={min?.toISOString()}
data-max={max?.toISOString()}
data-events-count={events?.length ?? 0}
data-step={step}
data-timeslots={timeslots}
Expand Down Expand Up @@ -93,6 +99,7 @@
customViewShowFriday: true,
customViewShowSaturday: false,
showAllEvents: true,
showMultiDayTimes: true,
step: 60,
timeslots: 2,
toolbarItems: [],
Expand Down Expand Up @@ -256,3 +263,179 @@
expect(result.timeslots).toBe(2);
});
});

describe("CalendarPropsBuilder showMultiDayTimes", () => {
const mockLocalizer = {
format: jest.fn(),
parse: jest.fn(),
startOfWeek: jest.fn(),
getDay: jest.fn(),
messages: {}
} as any;

it("passes showMultiDayTimes=true to calendar props", () => {
const props = { ...customViewProps, showMultiDayTimes: true };
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");
expect(result.showMultiDayTimes).toBe(true);
});

it("passes showMultiDayTimes=false to calendar props", () => {
const props = { ...customViewProps, showMultiDayTimes: false };
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");
expect(result.showMultiDayTimes).toBe(false);
});
});

describe("CalendarPropsBuilder multi-day time formats", () => {
const mockLocalizer = {
format: jest.fn((date: Date, pattern: string, _culture: string) => {
// Simulate locale-aware formatting using the pattern
const hours = date.getHours();
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes} (${pattern})`;
}),
parse: jest.fn(),
startOfWeek: jest.fn(),
getDay: jest.fn(),
messages: {}
} as any;

const buildWithTimeFormat = (timeFormatValue: string) => {

Check warning on line 305 in packages/pluggableWidgets/calendar-web/src/__tests__/Calendar.spec.tsx

View workflow job for this annotation

GitHub Actions / Run code quality check

Missing return type on function
const props = {
...customViewProps,
timeFormat: dynamic(timeFormatValue)
};
const builder = new CalendarPropsBuilder(props);
return builder.build(mockLocalizer, "en");
};

it("sets eventTimeRangeStartFormat using the configured time pattern", () => {
const result = buildWithTimeFormat("HH:mm");
const start = new Date("2025-04-28T22:00:00Z");
const end = new Date("2025-04-29T02:00:00Z");

expect(result.formats!.eventTimeRangeStartFormat).toBeDefined();
const label = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
expect(label).toContain("HH:mm");
expect(label).toMatch(/– $/);
});

it("sets eventTimeRangeEndFormat using the configured time pattern", () => {
const result = buildWithTimeFormat("HH:mm");
const start = new Date("2025-04-28T22:00:00Z");
const end = new Date("2025-04-29T02:00:00Z");

expect(result.formats!.eventTimeRangeEndFormat).toBeDefined();
const label = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);
expect(label).toContain("HH:mm");
expect(label).toMatch(/^ – /);
});

it("uses the same pattern for eventTimeRangeFormat, start, and end formats", () => {
const result = buildWithTimeFormat("h:mm a");
const start = new Date("2025-04-28T22:00:00Z");
const end = new Date("2025-04-29T02:00:00Z");

const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);

// All three should use the same "h:mm a" pattern passed to localizer.format
expect(rangeLabel).toContain("h:mm a");
expect(startLabel).toContain("h:mm a");
expect(endLabel).toContain("h:mm a");
});

it("does not set start/end formats when no timeFormat is configured", () => {
const props = { ...customViewProps, timeFormat: undefined };
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");

expect(result.formats!.eventTimeRangeStartFormat).toBeUndefined();
expect(result.formats!.eventTimeRangeEndFormat).toBeUndefined();
});
});

describe("CalendarPropsBuilder showEventDate hides multi-day formats", () => {
const mockLocalizer = {
format: jest.fn((_date: Date, pattern: string) => `formatted(${pattern})`),
parse: jest.fn(),
startOfWeek: jest.fn(),
getDay: jest.fn(),
messages: {}
} as any;

it("blanks eventTimeRangeStartFormat when showEventDate=false", () => {
const props = {
...customViewProps,
showEventDate: dynamic(false),
timeFormat: dynamic("HH:mm")
};
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");

const label = (result.formats!.eventTimeRangeStartFormat as Function)(
{ start: new Date(), end: new Date() },
"en",
mockLocalizer
);
expect(label).toBe("");
});

it("blanks eventTimeRangeEndFormat when showEventDate=false", () => {
const props = {
...customViewProps,
showEventDate: dynamic(false),
timeFormat: dynamic("HH:mm")
};
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");

const label = (result.formats!.eventTimeRangeEndFormat as Function)(
{ start: new Date(), end: new Date() },
"en",
mockLocalizer
);
expect(label).toBe("");
});

it("blanks eventTimeRangeFormat when showEventDate=false", () => {
const props = {
...customViewProps,
showEventDate: dynamic(false),
timeFormat: dynamic("HH:mm")
};
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");

const label = (result.formats!.eventTimeRangeFormat as Function)(
{ start: new Date(), end: new Date() },
"en",
mockLocalizer
);
expect(label).toBe("");
});

it("preserves all time range formats when showEventDate=true", () => {
const props = {
...customViewProps,
showEventDate: dynamic(true),
timeFormat: dynamic("p")
};
const builder = new CalendarPropsBuilder(props);
const result = builder.build(mockLocalizer, "en");

const start = new Date("2025-04-28T22:00:00Z");
const end = new Date("2025-04-29T02:00:00Z");

const rangeLabel = (result.formats!.eventTimeRangeFormat as Function)({ start, end }, "en", mockLocalizer);
const startLabel = (result.formats!.eventTimeRangeStartFormat as Function)({ start, end }, "en", mockLocalizer);
const endLabel = (result.formats!.eventTimeRangeEndFormat as Function)({ start, end }, "en", mockLocalizer);

expect(rangeLabel).not.toBe("");
expect(startLabel).not.toBe("");
expect(endLabel).not.toBe("");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ exports[`Calendar renders correctly with basic props 1`] = `
data-resizable="true"
data-selectable="true"
data-show-all-events="true"
data-show-multi-day-times="true"
data-step="60"
data-testid="mock-calendar"
data-timeslots="2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { createConfigurableToolbar, CustomToolbar, ResolvedToolbarItem } from "../components/Toolbar";
import { eventPropGetter, getTextValue } from "../utils/calendar-utils";
import { CalendarEvent, DragAndDropCalendarProps } from "../utils/typings";
import { CustomWeekController } from "./CustomWeekController";

Check warning on line 7 in packages/pluggableWidgets/calendar-web/src/helpers/CalendarPropsBuilder.ts

View workflow job for this annotation

GitHub Actions / Run code quality check

`./CustomWeekController` import should occur before import of `../../typings/CalendarProps`

export class CalendarPropsBuilder {
private visibleDays: Set<number>;
Expand Down Expand Up @@ -86,6 +86,7 @@
startAccessor: (event: CalendarEvent) => event.start,
titleAccessor: (event: CalendarEvent) => event.title,
showAllEvents: this.props.showAllEvents,
showMultiDayTimes: this.props.showMultiDayTimes,
min: this.minTime,
max: this.maxTime,
step: this.step,
Expand Down Expand Up @@ -165,6 +166,16 @@
culture: string,
loc: DateLocalizer
) => `${formatWith(start, culture, loc)} – ${formatWith(end, culture, loc)}`;
formats.eventTimeRangeStartFormat = (
{ start }: { start: Date; end: Date },
culture: string,
loc: DateLocalizer
) => `${formatWith(start, culture, loc)} – `;
formats.eventTimeRangeEndFormat = (
{ end }: { start: Date; end: Date },
culture: string,
loc: DateLocalizer
) => ` – ${formatWith(end, culture, loc)}`;
formats.agendaTimeRangeFormat = (
{ start, end }: { start: Date; end: Date },
culture: string,
Expand Down Expand Up @@ -264,6 +275,8 @@
// Ensure showEventDate=false always hides event time ranges
if (this.props.showEventDate?.value === false) {
formats.eventTimeRangeFormat = () => "";
formats.eventTimeRangeStartFormat = () => "";
formats.eventTimeRangeEndFormat = () => "";
}

return formats;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface CalendarContainerProps {
minHour: number;
maxHour: number;
showAllEvents: boolean;
showMultiDayTimes: boolean;
step: number;
timeslots: number;
startDateAttribute?: EditableValue<Date>;
Expand Down Expand Up @@ -144,6 +145,7 @@ export interface CalendarPreviewProps {
minHour: number | null;
maxHour: number | null;
showAllEvents: boolean;
showMultiDayTimes: boolean;
step: number | null;
timeslots: number | null;
startDateAttribute: string;
Expand Down
Loading