From 607ec1c2b3fdfaa892f779a00b4ab6f3861cdb1a Mon Sep 17 00:00:00 2001 From: delangle Date: Thu, 30 Oct 2025 10:56:02 +0100 Subject: [PATCH 01/10] [scheduler] Store events with processed dates in the state --- .../scheduler/agenda-view/BasicAgendaView.tsx | 4 +- .../data/scheduler/datasets/all-day-events.ts | 4 +- docs/data/scheduler/datasets/car-rental.ts | 4 +- docs/data/scheduler/datasets/palette-demo.ts | 4 +- .../scheduler/datasets/personal-agenda.ts | 4 +- .../scheduler/datasets/recurring-events.ts | 4 +- .../scheduler/datasets/timeline-events.ts | 4 +- docs/data/scheduler/day-view/BasicDayView.tsx | 4 +- docs/data/scheduler/event-calendar/AllDay.tsx | 4 +- .../event-calendar/ColorPalettes.tsx | 4 +- .../scheduler/event-calendar/DefaultView.tsx | 4 +- .../event-calendar/DefaultVisibleDate.tsx | 4 +- .../scheduler/event-calendar/DragAndDrop.tsx | 4 +- .../event-calendar/ExternalDragAndDrop.tsx | 2 +- .../event-calendar/FullEventCalendar.tsx | 4 +- .../event-calendar/PreferencesMenu.tsx | 4 +- .../scheduler/event-calendar/RemoveViews.tsx | 4 +- .../scheduler/event-calendar/Translations.tsx | 4 +- .../scheduler/month-view/BasicMonthView.tsx | 4 +- .../RecurringEventsDataset.tsx | 4 +- .../data/scheduler/timeline/BasicTimeline.tsx | 4 +- .../scheduler/week-view/BasicWeekView.tsx | 4 +- packages/x-scheduler-headless/package.json | 1 + .../CalendarGridCurrentTimeIndicator.tsx | 8 +- .../day-cell/useDayCellDropTarget.ts | 4 +- .../CalendarGridDayEventPlaceholder.test.tsx | 9 +- ...CalendarGridDayEventResizeHandler.test.tsx | 9 +- .../day-event/CalendarGridDayEvent.test.tsx | 9 +- .../day-event/CalendarGridDayEvent.tsx | 10 +- .../time-column/useTimeDropTarget.ts | 4 +- .../CalendarGridTimeEventPlaceholder.test.tsx | 7 +- ...alendarGridTimeEventResizeHandler.test.tsx | 7 +- .../time-event/CalendarGridTimeEvent.test.tsx | 7 +- .../time-event/CalendarGridTimeEvent.tsx | 6 +- .../useCalendarGridPlaceholderInDay.ts | 15 +- .../useCalendarGridPlaceholderInRange.ts | 11 +- .../src/models/dragAndDrop.ts | 7 +- .../x-scheduler-headless/src/models/event.ts | 82 +++++++- .../src/process-date/processDate.ts | 1 + .../src/process-event/index.ts | 1 + .../src/process-event/processEvent.ts | 20 ++ .../scheduler-selectors/schedulerSelectors.ts | 26 +-- .../src/timeline/event/TimelineEvent.test.tsx | 7 +- ...gendaEventOccurrencesGroupedByDay.test.tsx | 27 +-- .../selectors.EventCalendarStore.test.ts | 12 +- .../src/use-event-calendar/tests/utils.ts | 6 +- .../useEventOccurrencesGroupedByDay.ts | 4 +- .../useEventOccurencesGroupedByResource.tsx | 4 +- ...ventOccurrencesWithDayGridPosition.test.ts | 19 +- .../useEventOccurrencesWithDayGridPosition.ts | 2 +- ...entOccurrencesWithTimelinePosition.test.ts | 19 +- ...useEventOccurrencesWithTimelinePosition.ts | 4 +- .../utils/SchedulerStore/SchedulerStore.ts | 17 +- .../SchedulerStore/SchedulerStore.types.ts | 9 +- .../SchedulerStore/SchedulerStore.utils.ts | 51 +++-- .../tests/core.SchedulerStore.test.ts | 4 +- .../tests/event.SchedulerStore.test.ts | 3 +- .../recurring-event.SchedulerStore.test.ts | 7 +- .../src/utils/SchedulerStore/tests/utils.ts | 6 +- .../src/utils/event-utils.test.ts | 23 +- .../src/utils/event-utils.ts | 14 +- .../src/utils/recurring-event-utils.test.ts | 196 ++++++++++-------- .../src/utils/recurring-event-utils.ts | 73 ++++--- .../src/utils/useDraggableEvent.ts | 4 +- .../src/utils/useDropTarget.ts | 13 +- .../utils/useElementPositionInCollection.ts | 10 +- .../src/utils/useEvent.ts | 6 +- .../event-popover/EventPopover.test.tsx | 56 ++--- .../components/event-popover/FormContent.tsx | 22 +- .../event-popover/ReadonlyContent.tsx | 11 +- .../internals/components/event/Event.types.ts | 4 +- .../event/day-grid-event/DayGridEvent.tsx | 11 +- .../components/event/event-item/EventItem.tsx | 16 +- .../event/time-grid-event/TimeGridEvent.tsx | 9 +- packages/x-scheduler/src/models/index.ts | 2 +- .../src/month-view/MonthView.test.tsx | 6 +- .../src/timeline/Timeline.test.tsx | 12 +- test/utils/scheduler/event.ts | 21 ++ test/utils/scheduler/index.ts | 1 + 79 files changed, 617 insertions(+), 424 deletions(-) create mode 100644 packages/x-scheduler-headless/src/process-event/index.ts create mode 100644 packages/x-scheduler-headless/src/process-event/processEvent.ts create mode 100644 test/utils/scheduler/event.ts diff --git a/docs/data/scheduler/agenda-view/BasicAgendaView.tsx b/docs/data/scheduler/agenda-view/BasicAgendaView.tsx index 1d04a5f249ee8..2c397264cbab2 100644 --- a/docs/data/scheduler/agenda-view/BasicAgendaView.tsx +++ b/docs/data/scheduler/agenda-view/BasicAgendaView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { StandaloneAgendaView } from '@mui/x-scheduler/agenda-view'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function BasicAgendaView() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/datasets/all-day-events.ts b/docs/data/scheduler/datasets/all-day-events.ts index 953ba6a0e5f2d..f93985440c4ff 100644 --- a/docs/data/scheduler/datasets/all-day-events.ts +++ b/docs/data/scheduler/datasets/all-day-events.ts @@ -2,13 +2,13 @@ // Non-realistic set focused on edge cases of all-day events positioning. import { DateTime } from 'luxon'; -import { CalendarEvent, CalendarResource } from '@mui/x-scheduler/models'; +import { SchedulerEvent, CalendarResource } from '@mui/x-scheduler/models'; export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00'); const START_OF_FIRST_WEEK = defaultVisibleDate.startOf('week'); -export const initialEvents: CalendarEvent[] = [ +export const initialEvents: SchedulerEvent[] = [ { id: '1', start: START_OF_FIRST_WEEK.set({ weekday: 1, hour: 9 }), diff --git a/docs/data/scheduler/datasets/car-rental.ts b/docs/data/scheduler/datasets/car-rental.ts index f83dc0094cef6..58ff3df72995d 100644 --- a/docs/data/scheduler/datasets/car-rental.ts +++ b/docs/data/scheduler/datasets/car-rental.ts @@ -1,12 +1,12 @@ // Fake data of a car rental company import { DateTime } from 'luxon'; -import { CalendarEvent, CalendarResource } from '@mui/x-scheduler/models'; +import { SchedulerEvent, CalendarResource } from '@mui/x-scheduler/models'; export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00'); const START_OF_FIRST_WEEK = defaultVisibleDate.startOf('week'); -export const initialEvents: CalendarEvent[] = [ +export const initialEvents: SchedulerEvent[] = [ { id: 'rental-1', start: START_OF_FIRST_WEEK.set({ hour: 9 }), diff --git a/docs/data/scheduler/datasets/palette-demo.ts b/docs/data/scheduler/datasets/palette-demo.ts index 8ad377e82711f..ac79ba97b4d5f 100644 --- a/docs/data/scheduler/datasets/palette-demo.ts +++ b/docs/data/scheduler/datasets/palette-demo.ts @@ -1,12 +1,12 @@ // Fake data of an agenda with lots of different resources import { DateTime } from 'luxon'; -import { CalendarEvent, CalendarResource } from '@mui/x-scheduler/models'; +import { SchedulerEvent, CalendarResource } from '@mui/x-scheduler/models'; export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00'); const START_OF_FIRST_WEEK = defaultVisibleDate.startOf('week'); -export const initialEvents: CalendarEvent[] = [ +export const initialEvents: SchedulerEvent[] = [ { id: 'violet', start: START_OF_FIRST_WEEK.set({ weekday: 1, hour: 2 }), diff --git a/docs/data/scheduler/datasets/personal-agenda.ts b/docs/data/scheduler/datasets/personal-agenda.ts index 1bad4e2b2e17e..682d175d9afad 100644 --- a/docs/data/scheduler/datasets/personal-agenda.ts +++ b/docs/data/scheduler/datasets/personal-agenda.ts @@ -1,13 +1,13 @@ // Personal Agenda Events Dataset import { DateTime } from 'luxon'; -import { CalendarEvent, CalendarResource } from '@mui/x-scheduler/models'; +import { SchedulerEvent, CalendarResource } from '@mui/x-scheduler/models'; export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00'); const START_OF_FIRST_WEEK = defaultVisibleDate.startOf('week'); -export const initialEvents: CalendarEvent[] = [ +export const initialEvents: SchedulerEvent[] = [ // Work events { id: 'work-daily-standup', diff --git a/docs/data/scheduler/datasets/recurring-events.ts b/docs/data/scheduler/datasets/recurring-events.ts index a130ecdebd167..7db1c299e5dfe 100644 --- a/docs/data/scheduler/datasets/recurring-events.ts +++ b/docs/data/scheduler/datasets/recurring-events.ts @@ -2,12 +2,12 @@ // Non-realistic set focused on edge cases of RRULE handling. import { DateTime } from 'luxon'; -import { CalendarEvent, CalendarResource } from '@mui/x-scheduler/models'; +import { SchedulerEvent, CalendarResource } from '@mui/x-scheduler/models'; export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00'); const START = defaultVisibleDate.startOf('week'); -export const initialEvents: CalendarEvent[] = [ +export const initialEvents: SchedulerEvent[] = [ // WEEKLY PATTERNS { id: 'weekly-weekdays-only', diff --git a/docs/data/scheduler/datasets/timeline-events.ts b/docs/data/scheduler/datasets/timeline-events.ts index 8ead887d5c5de..a898234a78eea 100644 --- a/docs/data/scheduler/datasets/timeline-events.ts +++ b/docs/data/scheduler/datasets/timeline-events.ts @@ -1,11 +1,11 @@ import { DateTime } from 'luxon'; -import { CalendarEvent, CalendarResource } from '@mui/x-scheduler/models'; +import { SchedulerEvent, CalendarResource } from '@mui/x-scheduler/models'; // Timeline starts July 1, 2025 export const defaultVisibleDate = DateTime.fromISO('2025-07-01T00:00:00'); const START = defaultVisibleDate.startOf('week'); -export const initialEvents: CalendarEvent[] = [ +export const initialEvents: SchedulerEvent[] = [ // Project { id: 'meeting-1', diff --git a/docs/data/scheduler/day-view/BasicDayView.tsx b/docs/data/scheduler/day-view/BasicDayView.tsx index 3ff4d0e90bf37..dc314b9697a05 100644 --- a/docs/data/scheduler/day-view/BasicDayView.tsx +++ b/docs/data/scheduler/day-view/BasicDayView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { StandaloneDayView } from '@mui/x-scheduler/day-view'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function BasicDayView() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/AllDay.tsx b/docs/data/scheduler/event-calendar/AllDay.tsx index bdb64bdc62214..7175f9a095bdb 100644 --- a/docs/data/scheduler/event-calendar/AllDay.tsx +++ b/docs/data/scheduler/event-calendar/AllDay.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/all-day-events'; export default function AllDay() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/ColorPalettes.tsx b/docs/data/scheduler/event-calendar/ColorPalettes.tsx index c2e1be7f18425..d47a7b810982d 100644 --- a/docs/data/scheduler/event-calendar/ColorPalettes.tsx +++ b/docs/data/scheduler/event-calendar/ColorPalettes.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -9,7 +9,7 @@ import { } from '../datasets/palette-demo'; export default function ColorPalettes() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/DefaultView.tsx b/docs/data/scheduler/event-calendar/DefaultView.tsx index cc3e79ebbbaea..583ec3b072358 100644 --- a/docs/data/scheduler/event-calendar/DefaultView.tsx +++ b/docs/data/scheduler/event-calendar/DefaultView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function DefaultView() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/DefaultVisibleDate.tsx b/docs/data/scheduler/event-calendar/DefaultVisibleDate.tsx index 63b72d86beeda..2b6ce43707531 100644 --- a/docs/data/scheduler/event-calendar/DefaultVisibleDate.tsx +++ b/docs/data/scheduler/event-calendar/DefaultVisibleDate.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { DateTime } from 'luxon'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, resources } from '../datasets/personal-agenda'; const defaultVisibleDate = DateTime.fromISO('2025-11-01'); export default function DefaultVisibleDate() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/DragAndDrop.tsx b/docs/data/scheduler/event-calendar/DragAndDrop.tsx index 77fbd88da38d8..1fe9ec9ff48f0 100644 --- a/docs/data/scheduler/event-calendar/DragAndDrop.tsx +++ b/docs/data/scheduler/event-calendar/DragAndDrop.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function DragAndDrop() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/ExternalDragAndDrop.tsx b/docs/data/scheduler/event-calendar/ExternalDragAndDrop.tsx index 3a75ce43c695f..b0708134805e7 100644 --- a/docs/data/scheduler/event-calendar/ExternalDragAndDrop.tsx +++ b/docs/data/scheduler/event-calendar/ExternalDragAndDrop.tsx @@ -79,7 +79,7 @@ export default function ExternalDragAndDrop() { setPlaceholder({ ...eventData, - duration: end.diff(start).as('minutes'), + duration: end.value.diff(start.value).as('minutes'), }); }, onDragLeave: () => { diff --git a/docs/data/scheduler/event-calendar/FullEventCalendar.tsx b/docs/data/scheduler/event-calendar/FullEventCalendar.tsx index acb5d2925679d..55371d52f0dee 100644 --- a/docs/data/scheduler/event-calendar/FullEventCalendar.tsx +++ b/docs/data/scheduler/event-calendar/FullEventCalendar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function FullEventCalendar() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/PreferencesMenu.tsx b/docs/data/scheduler/event-calendar/PreferencesMenu.tsx index fdded905e9dcb..7c81a2337aaeb 100644 --- a/docs/data/scheduler/event-calendar/PreferencesMenu.tsx +++ b/docs/data/scheduler/event-calendar/PreferencesMenu.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function PreferencesMenu() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/RemoveViews.tsx b/docs/data/scheduler/event-calendar/RemoveViews.tsx index c2b25b4f0c563..0d4a5cc15eb60 100644 --- a/docs/data/scheduler/event-calendar/RemoveViews.tsx +++ b/docs/data/scheduler/event-calendar/RemoveViews.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function RemoveViews() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/event-calendar/Translations.tsx b/docs/data/scheduler/event-calendar/Translations.tsx index dd8a79bebda29..8fcbd5ee996a5 100644 --- a/docs/data/scheduler/event-calendar/Translations.tsx +++ b/docs/data/scheduler/event-calendar/Translations.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { frFR } from '@mui/x-scheduler/translations'; import { @@ -9,7 +9,7 @@ import { } from '../datasets/palette-demo'; export default function Translations() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/month-view/BasicMonthView.tsx b/docs/data/scheduler/month-view/BasicMonthView.tsx index 43d134b95d344..9c22836839eea 100644 --- a/docs/data/scheduler/month-view/BasicMonthView.tsx +++ b/docs/data/scheduler/month-view/BasicMonthView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { StandaloneMonthView } from '@mui/x-scheduler/month-view'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function BasicMonthView() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/recurring-events/RecurringEventsDataset.tsx b/docs/data/scheduler/recurring-events/RecurringEventsDataset.tsx index d4e522e18df09..207b511ef6954 100644 --- a/docs/data/scheduler/recurring-events/RecurringEventsDataset.tsx +++ b/docs/data/scheduler/recurring-events/RecurringEventsDataset.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { EventCalendar } from '@mui/x-scheduler/event-calendar'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/recurring-events'; export default function RecurringEventsDataset() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/timeline/BasicTimeline.tsx b/docs/data/scheduler/timeline/BasicTimeline.tsx index a4600d2d57edf..22a26b24d94a6 100644 --- a/docs/data/scheduler/timeline/BasicTimeline.tsx +++ b/docs/data/scheduler/timeline/BasicTimeline.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { Timeline } from '@mui/x-scheduler/timeline'; import { defaultVisibleDate, @@ -8,7 +8,7 @@ import { } from '../datasets/timeline-events'; export default function BasicTimeline() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/docs/data/scheduler/week-view/BasicWeekView.tsx b/docs/data/scheduler/week-view/BasicWeekView.tsx index e5e811ef9fd60..9421936cc8c8b 100644 --- a/docs/data/scheduler/week-view/BasicWeekView.tsx +++ b/docs/data/scheduler/week-view/BasicWeekView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CalendarEvent } from '@mui/x-scheduler/models'; +import { SchedulerEvent } from '@mui/x-scheduler/models'; import { StandaloneWeekView } from '@mui/x-scheduler/week-view'; import { initialEvents, @@ -8,7 +8,7 @@ import { } from '../datasets/personal-agenda'; export default function BasicWeekView() { - const [events, setEvents] = React.useState(initialEvents); + const [events, setEvents] = React.useState(initialEvents); return (
diff --git a/packages/x-scheduler-headless/package.json b/packages/x-scheduler-headless/package.json index b3098c89a4a76..b32a1f438fdeb 100644 --- a/packages/x-scheduler-headless/package.json +++ b/packages/x-scheduler-headless/package.json @@ -20,6 +20,7 @@ "./event-calendar-provider": "./src/event-calendar-provider/index.ts", "./models": "./src/models/index.ts", "./process-date": "./src/process-date/index.ts", + "./process-event": "./src/process-event/index.ts", "./scheduler-selectors": "./src/scheduler-selectors/index.ts", "./standalone-event": "./src/standalone-event/index.ts", "./timeline": "./src/timeline/index.ts", diff --git a/packages/x-scheduler-headless/src/calendar-grid/current-time-indicator/CalendarGridCurrentTimeIndicator.tsx b/packages/x-scheduler-headless/src/calendar-grid/current-time-indicator/CalendarGridCurrentTimeIndicator.tsx index cc96e5f6cac64..528c4d4d7599c 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/current-time-indicator/CalendarGridCurrentTimeIndicator.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/current-time-indicator/CalendarGridCurrentTimeIndicator.tsx @@ -10,6 +10,7 @@ import { CalendarGridCurrentTimeIndicatorCssVars } from './CalendarGridCurrentTi import { mergeDateAndTime } from '../../utils/date-utils'; import { useEventCalendarStoreContext } from '../../use-event-calendar-store-context'; import { selectors } from '../../use-event-calendar'; +import { processDate } from '../../process-date'; export const CalendarGridCurrentTimeIndicator = React.forwardRef( function CalendarGridCurrentTimeIndicator( @@ -31,12 +32,12 @@ export const CalendarGridCurrentTimeIndicator = React.forwardRef( const now = useStore(store, selectors.nowUpdatedEveryMinute); const nowForColumn = React.useMemo( - () => mergeDateAndTime(adapter, columnStart, now), + () => processDate(mergeDateAndTime(adapter, columnStart, now), adapter), [adapter, columnStart, now], ); const endForCalc = React.useMemo( - () => adapter.addMinutes(nowForColumn, 1), + () => processDate(adapter.addMinutes(nowForColumn.value, 1), adapter), [adapter, nowForColumn], ); @@ -58,7 +59,8 @@ export const CalendarGridCurrentTimeIndicator = React.forwardRef( const props = React.useMemo(() => ({ style }), [style]); const isOutOfRange = - adapter.isBefore(nowForColumn, columnStart) || adapter.isAfter(nowForColumn, columnEnd); + adapter.isBefore(nowForColumn.value, columnStart) || + adapter.isAfter(nowForColumn.value, columnEnd); return useRenderElement('div', componentProps, { ref: [forwardedRef], diff --git a/packages/x-scheduler-headless/src/calendar-grid/day-cell/useDayCellDropTarget.ts b/packages/x-scheduler-headless/src/calendar-grid/day-cell/useDayCellDropTarget.ts index 4b5ecdb64178a..03247c5c2219e 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/day-cell/useDayCellDropTarget.ts +++ b/packages/x-scheduler-headless/src/calendar-grid/day-cell/useDayCellDropTarget.ts @@ -3,7 +3,7 @@ import * as React from 'react'; import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; import { buildIsValidDropTarget } from '../../build-is-valid-drop-target'; import { useAdapter, diffIn } from '../../use-adapter'; -import { CalendarEvent, SchedulerValidDate } from '../../models'; +import { SchedulerProcessedEvent, SchedulerValidDate } from '../../models'; import { mergeDateAndTime } from '../../utils/date-utils'; import { useDropTarget } from '../../utils/useDropTarget'; import { EVENT_CREATION_DEFAULT_LENGTH_MINUTE } from '../../constants'; @@ -121,6 +121,6 @@ export namespace useDayCellDropTarget { /** * Add properties to the event dropped in the cell before storing it in the store. */ - addPropertiesToDroppedEvent?: () => Partial; + addPropertiesToDroppedEvent?: () => Partial; } } diff --git a/packages/x-scheduler-headless/src/calendar-grid/day-event-placeholder/CalendarGridDayEventPlaceholder.test.tsx b/packages/x-scheduler-headless/src/calendar-grid/day-event-placeholder/CalendarGridDayEventPlaceholder.test.tsx index 314e501355927..c2a765463c1b3 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/day-event-placeholder/CalendarGridDayEventPlaceholder.test.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/day-event-placeholder/CalendarGridDayEventPlaceholder.test.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { CalendarGrid } from '@mui/x-scheduler-headless/calendar-grid'; import { adapter, createSchedulerRenderer, describeConformance } from 'test/utils/scheduler'; import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider'; +import { processDate } from '@mui/x-scheduler-headless/process-date'; describe('', () => { const { render } = createSchedulerRenderer(); - const eventStart = adapter.date(); - const eventEnd = adapter.addHours(eventStart, 1); + const eventStart = processDate(adapter.date(), adapter); + const eventEnd = processDate(adapter.addHours(eventStart.value, 1), adapter); describeConformance( , @@ -17,8 +18,8 @@ describe('', () => { return render( - - {node} + + {node} , diff --git a/packages/x-scheduler-headless/src/calendar-grid/day-event-resize-handler/CalendarGridDayEventResizeHandler.test.tsx b/packages/x-scheduler-headless/src/calendar-grid/day-event-resize-handler/CalendarGridDayEventResizeHandler.test.tsx index 73ef2a6d95a59..2e6fa5e4863e3 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/day-event-resize-handler/CalendarGridDayEventResizeHandler.test.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/day-event-resize-handler/CalendarGridDayEventResizeHandler.test.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { CalendarGrid } from '@mui/x-scheduler-headless/calendar-grid'; import { adapter, createSchedulerRenderer, describeConformance } from 'test/utils/scheduler'; import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider'; +import { processDate } from '@mui/x-scheduler-headless/process-date'; describe('', () => { const { render } = createSchedulerRenderer(); - const eventStart = adapter.date(); - const eventEnd = adapter.addHours(eventStart, 1); + const eventStart = processDate(adapter.date(), adapter); + const eventEnd = processDate(adapter.addHours(eventStart.value, 1), adapter); describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, @@ -15,8 +16,8 @@ describe('', () => { return render( - - + + ', () => { const { render } = createSchedulerRenderer(); - const eventStart = adapter.date(); - const eventEnd = adapter.addHours(eventStart, 1); + const eventStart = processDate(adapter.date(), adapter); + const eventEnd = processDate(adapter.addHours(eventStart.value, 1), adapter); describeConformance( ', () => { return render( - - {node} + + {node} , diff --git a/packages/x-scheduler-headless/src/calendar-grid/day-event/CalendarGridDayEvent.tsx b/packages/x-scheduler-headless/src/calendar-grid/day-event/CalendarGridDayEvent.tsx index 40c1e79a049c2..6380ec811a970 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/day-event/CalendarGridDayEvent.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/day-event/CalendarGridDayEvent.tsx @@ -63,11 +63,11 @@ export const CalendarGridDayEvent = React.forwardRef(function CalendarGridDayEve // Feature hooks const getDraggedDay = useEventCallback((input: { clientX: number }) => { if (!ref.current) { - return start; + return start.value; } - const eventStartInRow = adapter.isBefore(start, rowStart) ? rowStart : start; - const eventEndInRow = adapter.isAfter(end, rowEnd) ? rowEnd : end; + const eventStartInRow = adapter.isBefore(start.value, rowStart) ? rowStart : start.value; + const eventEndInRow = adapter.isAfter(end.value, rowEnd) ? rowEnd : end.value; const eventDayLengthInRow = diffIn(adapter, eventEndInRow, eventStartInRow, 'days') + 1; const clientX = input.clientX; const elementPosition = ref.current.getBoundingClientRect(); @@ -90,8 +90,8 @@ export const CalendarGridDayEvent = React.forwardRef(function CalendarGridDayEve eventId, occurrenceKey, originalOccurrence, - start, - end, + start: start.value, + end: end.value, }), ); diff --git a/packages/x-scheduler-headless/src/calendar-grid/time-column/useTimeDropTarget.ts b/packages/x-scheduler-headless/src/calendar-grid/time-column/useTimeDropTarget.ts index 1034c1a021241..6a663a9c53954 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/time-column/useTimeDropTarget.ts +++ b/packages/x-scheduler-headless/src/calendar-grid/time-column/useTimeDropTarget.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { useEventCallback } from '@base-ui-components/utils/useEventCallback'; import { useAdapter } from '../../use-adapter/useAdapter'; -import { CalendarEvent, SchedulerValidDate } from '../../models'; +import { SchedulerProcessedEvent, SchedulerValidDate } from '../../models'; import { buildIsValidDropTarget } from '../../build-is-valid-drop-target'; import { CalendarGridTimeColumnContext } from './CalendarGridTimeColumnContext'; import { useDropTarget } from '../../utils/useDropTarget'; @@ -161,7 +161,7 @@ export namespace useTimeDropTarget { /** * Add properties to the event dropped in the column before storing it in the store. */ - addPropertiesToDroppedEvent?: () => Partial; + addPropertiesToDroppedEvent?: () => Partial; } export interface ReturnValue diff --git a/packages/x-scheduler-headless/src/calendar-grid/time-event-placeholder/CalendarGridTimeEventPlaceholder.test.tsx b/packages/x-scheduler-headless/src/calendar-grid/time-event-placeholder/CalendarGridTimeEventPlaceholder.test.tsx index c292e662498cd..f5bb24af0f4b0 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/time-event-placeholder/CalendarGridTimeEventPlaceholder.test.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/time-event-placeholder/CalendarGridTimeEventPlaceholder.test.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { CalendarGrid } from '@mui/x-scheduler-headless/calendar-grid'; import { adapter, createSchedulerRenderer, describeConformance } from 'test/utils/scheduler'; import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider'; +import { processDate } from '@mui/x-scheduler-headless/process-date'; describe('', () => { const { render } = createSchedulerRenderer(); - const eventStart = adapter.date(); - const eventEnd = adapter.addHours(eventStart, 1); + const eventStart = processDate(adapter.date(), adapter); + const eventEnd = processDate(adapter.addHours(eventStart.value, 1), adapter); describeConformance( , @@ -17,7 +18,7 @@ describe('', () => { return render( - + {node} diff --git a/packages/x-scheduler-headless/src/calendar-grid/time-event-resize-handler/CalendarGridTimeEventResizeHandler.test.tsx b/packages/x-scheduler-headless/src/calendar-grid/time-event-resize-handler/CalendarGridTimeEventResizeHandler.test.tsx index 0b01536e4a63a..29f1e9d023acc 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/time-event-resize-handler/CalendarGridTimeEventResizeHandler.test.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/time-event-resize-handler/CalendarGridTimeEventResizeHandler.test.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { CalendarGrid } from '@mui/x-scheduler-headless/calendar-grid'; import { adapter, createSchedulerRenderer, describeConformance } from 'test/utils/scheduler'; import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider'; +import { processDate } from '@mui/x-scheduler-headless/process-date'; describe('', () => { const { render } = createSchedulerRenderer(); - const eventStart = adapter.date(); - const eventEnd = adapter.addHours(eventStart, 1); + const eventStart = processDate(adapter.date(), adapter); + const eventEnd = processDate(adapter.addHours(eventStart.value, 1), adapter); describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, @@ -15,7 +16,7 @@ describe('', () => { return render( - + ', () => { const { render } = createSchedulerRenderer(); - const eventStart = adapter.date(); - const eventEnd = adapter.addHours(eventStart, 1); + const eventStart = processDate(adapter.date(), adapter); + const eventEnd = processDate(adapter.addHours(eventStart.value, 1), adapter); describeConformance( ', () => { return render( - + {node} diff --git a/packages/x-scheduler-headless/src/calendar-grid/time-event/CalendarGridTimeEvent.tsx b/packages/x-scheduler-headless/src/calendar-grid/time-event/CalendarGridTimeEvent.tsx index d71f2d3ec6811..ee1cd84b7ccaf 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/time-event/CalendarGridTimeEvent.tsx +++ b/packages/x-scheduler-headless/src/calendar-grid/time-event/CalendarGridTimeEvent.tsx @@ -63,7 +63,7 @@ export const CalendarGridTimeEvent = React.forwardRef(function CalendarGridTimeE const getSharedDragData: CalendarGridTimeEventContext['getSharedDragData'] = useEventCallback( (input) => { const offsetBeforeColumnStart = Math.max( - adapter.toJsDate(columnStart).getTime() - adapter.toJsDate(start).getTime(), + adapter.toJsDate(columnStart).getTime() - start.timestamp, 0, ); @@ -82,8 +82,8 @@ export const CalendarGridTimeEvent = React.forwardRef(function CalendarGridTimeE eventId, occurrenceKey, originalOccurrence, - start, - end, + start: start.value, + end: end.value, initialCursorPositionInEventMs: offsetBeforeColumnStart + offsetInsideColumn, }; }, diff --git a/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-day/useCalendarGridPlaceholderInDay.ts b/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-day/useCalendarGridPlaceholderInDay.ts index 3ba073facde76..7da30bd780da1 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-day/useCalendarGridPlaceholderInDay.ts +++ b/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-day/useCalendarGridPlaceholderInDay.ts @@ -6,6 +6,7 @@ import { useEventCalendarStoreContext } from '../../use-event-calendar-store-con import { useCalendarGridDayRowContext } from '../day-row/CalendarGridDayRowContext'; import type { useEventOccurrencesWithDayGridPosition } from '../../use-event-occurrences-with-day-grid-position'; import { useAdapter, diffIn } from '../../use-adapter/useAdapter'; +import { processDate } from '../../process-date'; export function useCalendarGridPlaceholderInDay( day: SchedulerValidDate, @@ -33,8 +34,7 @@ export function useCalendarGridPlaceholderInDay( const sharedProperties = { key: 'occurrence-placeholder', - start: rawPlaceholder.start, - end: rawPlaceholder.end, + modelInBuiltInFormat: null, }; // Creation mode @@ -44,8 +44,11 @@ export function useCalendarGridPlaceholderInDay( id: 'occurrence-placeholder', title: '', allDay: true, - start: day, - end: adapter.isAfter(rawPlaceholder.end, rowEnd) ? rowEnd : rawPlaceholder.end, + start: processDate(day, adapter), + end: processDate( + adapter.isAfter(rawPlaceholder.end, rowEnd) ? rowEnd : rawPlaceholder.end, + adapter, + ), position: { index: 1, daySpan: diffIn(adapter, rawPlaceholder.end, day, 'days') + 1, @@ -58,6 +61,8 @@ export function useCalendarGridPlaceholderInDay( ...sharedProperties, id: 'occurrence-placeholder', title: rawPlaceholder.eventData.title ?? '', + start: processDate(rawPlaceholder.start, adapter), + end: processDate(rawPlaceholder.end, adapter), position: { index: 1, daySpan: diffIn(adapter, rawPlaceholder.end, day, 'days') + 1, @@ -79,6 +84,8 @@ export function useCalendarGridPlaceholderInDay( return { ...originalEvent!, ...sharedProperties, + start: processDate(rawPlaceholder.start, adapter), + end: processDate(rawPlaceholder.end, adapter), position: { index: positionIndex, daySpan: diffIn(adapter, rawPlaceholder.end, day, 'days') + 1, diff --git a/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-range/useCalendarGridPlaceholderInRange.ts b/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-range/useCalendarGridPlaceholderInRange.ts index 35801ed4b9fa9..94ad24a2e2350 100644 --- a/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-range/useCalendarGridPlaceholderInRange.ts +++ b/packages/x-scheduler-headless/src/calendar-grid/use-placeholder-in-range/useCalendarGridPlaceholderInRange.ts @@ -4,11 +4,15 @@ import { CalendarEventOccurrenceWithTimePosition, SchedulerValidDate } from '../ import { useEventCalendarStoreContext } from '../../use-event-calendar-store-context'; import { selectors } from '../../use-event-calendar'; import { useEventOccurrencesWithTimelinePosition } from '../../use-event-occurrences-with-timeline-position'; +import { processDate } from '../../process-date'; +import { useAdapter } from '../../use-adapter'; export function useCalendarGridPlaceholderInRange( parameters: useCalendarGridPlaceholderInRange.Parameters, ): useCalendarGridPlaceholderInRange.ReturnValue { const { start, end, occurrences, maxIndex } = parameters; + + const adapter = useAdapter(); const store = useEventCalendarStoreContext(); const rawPlaceholder = useStore( @@ -29,8 +33,9 @@ export function useCalendarGridPlaceholderInRange( const sharedProperties = { key: 'occurrence-placeholder', - start: rawPlaceholder.start, - end: rawPlaceholder.end, + start: processDate(rawPlaceholder.start, adapter), + end: processDate(rawPlaceholder.end, adapter), + modelInBuiltInFormat: null, }; if (rawPlaceholder.type === 'creation') { @@ -69,7 +74,7 @@ export function useCalendarGridPlaceholderInRange( ...sharedProperties, position, }; - }, [rawPlaceholder, occurrences, maxIndex, originalEvent]); + }, [adapter, rawPlaceholder, occurrences, maxIndex, originalEvent]); } export namespace useCalendarGridPlaceholderInRange { diff --git a/packages/x-scheduler-headless/src/models/dragAndDrop.ts b/packages/x-scheduler-headless/src/models/dragAndDrop.ts index 53a8d291b1402..dff099598581d 100644 --- a/packages/x-scheduler-headless/src/models/dragAndDrop.ts +++ b/packages/x-scheduler-headless/src/models/dragAndDrop.ts @@ -1,14 +1,15 @@ -import type { CalendarEvent } from './event'; +import type { SchedulerEvent, SchedulerProcessedEvent } from './event'; export type RenderDragPreviewParameters = | { type: 'internal-event'; - data: CalendarEvent; + data: SchedulerProcessedEvent; } | { type: 'standalone-event'; data: CalendarOccurrencePlaceholderExternalDragData }; +// TODO: Add support for eventModelStructure. export interface CalendarOccurrencePlaceholderExternalDragData - extends Omit { + extends Omit { /** * The default duration of the event in minutes. * Will be ignored if the event is dropped on a UI that only handles multi-day events. diff --git a/packages/x-scheduler-headless/src/models/event.ts b/packages/x-scheduler-headless/src/models/event.ts index 844f5433f1d7f..7db21eaca10f8 100644 --- a/packages/x-scheduler-headless/src/models/event.ts +++ b/packages/x-scheduler-headless/src/models/event.ts @@ -3,9 +3,66 @@ import { RecurringEventRecurrenceRule } from './recurringEvent'; import type { CalendarOccurrencePlaceholderExternalDragData } from './dragAndDrop'; import type { CalendarResourceId } from './resource'; -// TODO: Rename SchedulerProcessedEvent and replace the raw SchedulerValidDate with processed dates. -// TODO: Create a new SchedulerDefaultEventModel to replace CalendarEvent on props.events. -export interface CalendarEvent { +export interface SchedulerProcessedEvent { + /** + * The unique identifier of the event. + */ + id: CalendarEventId; + /** + * The title of the event. + */ + title: string; + /** + * The description of the event. + */ + description?: string; + /** + * The start date and time of the event. + */ + start: CalendarProcessedDate; + /** + * The end date and time of the event. + */ + end: CalendarProcessedDate; + /** + * The id of the resource this event is associated with. + */ + resource?: CalendarResourceId; + /** + * The recurrence rule for the event. + * If not defined, the event will have only one occurrence. + */ + rrule?: RecurringEventRecurrenceRule; + /** + * Exception dates for the event. + * These dates will be excluded from the recurrence. + */ + exDates?: SchedulerValidDate[]; + /** + * Whether the event is an all-day event. + * @default false + */ + allDay?: boolean; + /** + * Whether the event is read-only. + * Readonly events cannot be modified using UI features such as popover editing or drag and drop. + * @default false + */ + readOnly?: boolean; + /** + * The id of the original event from which this event was split. + * If provided, it must reference an existing event in the calendar. + * If it does not match any existing event, the value will be ignored + * and no link to an original event will be created. + */ + extractedFromId?: CalendarEventId; + /** + * The event model in the `SchedulerEvent` format. + */ + modelInBuiltInFormat: SchedulerEvent | null; +} + +export interface SchedulerEvent { /** * The unique identifier of the event. */ @@ -42,11 +99,13 @@ export interface CalendarEvent { exDates?: SchedulerValidDate[]; /** * Whether the event is an all-day event. + * @default false */ allDay?: boolean; /** * Whether the event is read-only. * Readonly events cannot be modified using UI features such as popover editing or drag and drop. + * @default false */ readOnly?: boolean; /** @@ -61,7 +120,7 @@ export interface CalendarEvent { /** * A concrete occurrence derived from a `CalendarEvent` (recurring or single). */ -export interface CalendarEventOccurrence extends CalendarEvent { +export interface CalendarEventOccurrence extends SchedulerProcessedEvent { /** * Unique key that can be passed to the React `key` property when looping through events. */ @@ -211,6 +270,10 @@ export interface CalendarProcessedDate { * It only contains date information, two dates representing the same day but with different time will have the same key. */ key: string; + /** + * The timestamp of the date. + */ + timestamp: number; } /** @@ -218,8 +281,9 @@ export interface CalendarProcessedDate { * The `id`, `start` and `end` properties are required in order to identify the event to update and the new dates. * All other properties are optional and can be skipped if not modified. */ -export type CalendarEventUpdatedProperties = Partial & - Required>; +export type CalendarEventUpdatedProperties = Partial & { + id: CalendarEventId; +}; /** * The type of surface the event is being rendered on. @@ -227,15 +291,15 @@ export type CalendarEventUpdatedProperties = Partial & export type EventSurfaceType = 'day-grid' | 'time-grid'; export type SchedulerEventModelStructure = { - [key in keyof CalendarEvent]?: { - getter: (event: TEvent) => CalendarEvent[key]; + [key in keyof SchedulerEvent]?: { + getter: (event: TEvent) => SchedulerEvent[key]; /** * Setter for the event property. * If not provided, the property won't be editable. */ setter?: ( event: TEvent | Partial, - value: CalendarEvent[key], + value: SchedulerEvent[key], ) => TEvent | Partial; }; }; diff --git a/packages/x-scheduler-headless/src/process-date/processDate.ts b/packages/x-scheduler-headless/src/process-date/processDate.ts index 870910d18c95d..687d4ae6b6961 100644 --- a/packages/x-scheduler-headless/src/process-date/processDate.ts +++ b/packages/x-scheduler-headless/src/process-date/processDate.ts @@ -9,5 +9,6 @@ export function processDate(date: SchedulerValidDate, adapter: Adapter): Calenda return { value: date, key: getDateKey(date, adapter), + timestamp: adapter.toJsDate(date).getTime(), }; } diff --git a/packages/x-scheduler-headless/src/process-event/index.ts b/packages/x-scheduler-headless/src/process-event/index.ts new file mode 100644 index 0000000000000..ccc3032d30ea8 --- /dev/null +++ b/packages/x-scheduler-headless/src/process-event/index.ts @@ -0,0 +1 @@ +export * from './processEvent'; diff --git a/packages/x-scheduler-headless/src/process-event/processEvent.ts b/packages/x-scheduler-headless/src/process-event/processEvent.ts new file mode 100644 index 0000000000000..8a4042f47eee5 --- /dev/null +++ b/packages/x-scheduler-headless/src/process-event/processEvent.ts @@ -0,0 +1,20 @@ +import { SchedulerEvent, SchedulerProcessedEvent } from '../models'; +import { processDate } from '../process-date'; +import { Adapter } from '../use-adapter'; + +export function processEvent(model: SchedulerEvent, adapter: Adapter): SchedulerProcessedEvent { + return { + id: model.id, + title: model.title, + description: model.description, + start: processDate(model.start, adapter), + end: processDate(model.end, adapter), + resource: model.resource, + rrule: model.rrule, + exDates: model.exDates, + allDay: model.allDay ?? false, + readOnly: model.readOnly ?? false, + extractedFromId: model.extractedFromId, + modelInBuiltInFormat: model, + }; +} diff --git a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerSelectors.ts b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerSelectors.ts index 2cf78f798eec8..93cb2495198f9 100644 --- a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerSelectors.ts +++ b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerSelectors.ts @@ -1,10 +1,12 @@ import { createSelector, createSelectorMemoized } from '@base-ui-components/utils/store'; import { - CalendarEvent, + SchedulerProcessedEvent, CalendarEventId, RecurringEventPresetKey, RecurringEventRecurrenceRule, SchedulerValidDate, + CalendarProcessedDate, + SchedulerEvent, } from '../models'; import { SchedulerState as State } from '../utils/SchedulerStore/SchedulerStore.types'; import { getWeekDayCode } from '../utils/recurring-event-utils'; @@ -73,7 +75,7 @@ export const selectors = { return () => true; } - return (property: keyof CalendarEvent) => { + return (property: keyof SchedulerEvent) => { if (eventModelStructure?.[property] && !eventModelStructure?.[property].setter) { return true; } @@ -109,10 +111,10 @@ export const selectors = { isOccurrenceStartedOrEnded: createSelector( (state: State) => state.adapter, (state: State) => state.nowUpdatedEveryMinute, - (adapter, now, start: SchedulerValidDate, end: SchedulerValidDate) => { + (adapter, now, start: CalendarProcessedDate, end: CalendarProcessedDate) => { return { - started: adapter.isBefore(start, now) || adapter.isEqual(start, now), - ended: adapter.isBefore(end, now), + started: adapter.isBefore(start.value, now) || adapter.isEqual(start.value, now), + ended: adapter.isBefore(end.value, now), }; }, ), @@ -137,7 +139,7 @@ export const selectors = { (state: State) => state.adapter, ( adapter, - date: SchedulerValidDate, + date: CalendarProcessedDate, ): Record => { return { daily: { @@ -147,12 +149,12 @@ export const selectors = { weekly: { freq: 'WEEKLY', interval: 1, - byDay: [getWeekDayCode(adapter, date)], + byDay: [getWeekDayCode(adapter, date.value)], }, monthly: { freq: 'MONTHLY', interval: 1, - byMonthDay: [adapter.getDate(date)], + byMonthDay: [adapter.getDate(date.value)], }, yearly: { freq: 'YEARLY', @@ -170,8 +172,8 @@ export const selectors = { (state: State) => state.adapter, ( adapter, - rule: CalendarEvent['rrule'] | undefined, - occurrenceStart: SchedulerValidDate, + rule: SchedulerProcessedEvent['rrule'] | undefined, + occurrenceStart: CalendarProcessedDate, ): RecurringEventPresetKey | 'custom' | null => { if (!rule) { return null; @@ -193,7 +195,7 @@ export const selectors = { case 'WEEKLY': { // Preset "Weekly" => FREQ=WEEKLY;INTERVAL=1;BYDAY=; no COUNT/UNTIL; - const occurrenceStartWeekDayCode = getWeekDayCode(adapter, occurrenceStart); + const occurrenceStartWeekDayCode = getWeekDayCode(adapter, occurrenceStart.value); const byDay = rule.byDay ?? []; const matchesDefaultByDay = @@ -209,7 +211,7 @@ export const selectors = { case 'MONTHLY': { // Preset "Monthly" => FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=; no COUNT/UNTIL; - const day = adapter.getDate(occurrenceStart); + const day = adapter.getDate(occurrenceStart.value); const byMonthDay = rule.byMonthDay ?? []; const matchesDefaultByMonthDay = byMonthDay.length === 0 || (byMonthDay.length === 1 && byMonthDay[0] === day); diff --git a/packages/x-scheduler-headless/src/timeline/event/TimelineEvent.test.tsx b/packages/x-scheduler-headless/src/timeline/event/TimelineEvent.test.tsx index ad11118ea32e4..736702ddeb4d5 100644 --- a/packages/x-scheduler-headless/src/timeline/event/TimelineEvent.test.tsx +++ b/packages/x-scheduler-headless/src/timeline/event/TimelineEvent.test.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { Timeline } from '@mui/x-scheduler-headless/timeline'; import { TimelineProvider } from '@mui/x-scheduler-headless/timeline-provider'; import { adapter, createSchedulerRenderer, describeConformance } from 'test/utils/scheduler'; +import { processDate } from '@mui/x-scheduler-headless/process-date'; describe('', () => { const { render } = createSchedulerRenderer(); - const start = adapter.startOfDay(adapter.date()); - const end = adapter.endOfDay(adapter.date()); + const start = processDate(adapter.startOfDay(adapter.date()), adapter); + const end = processDate(adapter.endOfDay(adapter.date()), adapter); describeConformance(, () => ({ refInstanceof: window.HTMLDivElement, @@ -15,7 +16,7 @@ describe('', () => { return render( - + {node} diff --git a/packages/x-scheduler-headless/src/use-agenda-event-occurrences-grouped-by-day/useAgendaEventOccurrencesGroupedByDay.test.tsx b/packages/x-scheduler-headless/src/use-agenda-event-occurrences-grouped-by-day/useAgendaEventOccurrencesGroupedByDay.test.tsx index 800f406ce85d5..46714dd0be6cf 100644 --- a/packages/x-scheduler-headless/src/use-agenda-event-occurrences-grouped-by-day/useAgendaEventOccurrencesGroupedByDay.test.tsx +++ b/packages/x-scheduler-headless/src/use-agenda-event-occurrences-grouped-by-day/useAgendaEventOccurrencesGroupedByDay.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { renderHook } from '@mui/internal-test-utils'; -import { adapter } from 'test/utils/scheduler'; +import { adapter, createProcessedEvent } from 'test/utils/scheduler'; import { processDate } from '../process-date'; -import { CalendarEvent, SchedulerValidDate } from '../models'; +import { SchedulerEvent, SchedulerProcessedEvent, SchedulerValidDate } from '../models'; import { useAgendaEventOccurrencesGroupedByDay, useAgendaEventOccurrencesGroupedByDayOptions, @@ -16,14 +16,15 @@ describe('useAgendaEventOccurrencesGroupedByDay', () => { id: string, startISO: string, endISO: string, - extra: Partial = {}, - ): CalendarEvent => ({ - id, - start: adapter.date(startISO), - end: adapter.date(endISO), - title: `Event ${id}`, - ...extra, - }); + extra: Partial = {}, + ) => + createProcessedEvent({ + id, + start: adapter.date(startISO), + end: adapter.date(endISO), + title: `Event ${id}`, + ...extra, + }); function testHook({ events = [], @@ -31,7 +32,7 @@ describe('useAgendaEventOccurrencesGroupedByDay', () => { showWeekends, showEmptyDaysInAgenda, }: { - events?: CalendarEvent[]; + events?: SchedulerProcessedEvent[]; visibleDate: SchedulerValidDate; showWeekends: boolean; showEmptyDaysInAgenda: boolean; @@ -65,7 +66,7 @@ describe('useAgendaEventOccurrencesGroupedByDay', () => { }); it('should extend forward until it fills AGENDA_VIEW_DAYS_AMOUNT days that contain events when showEmptyDays=false', () => { - const events: CalendarEvent[] = [ + const events: SchedulerProcessedEvent[] = [ createEvent('1', '2025-01-01', '2025-01-01'), createEvent('2', '2025-01-03', '2025-01-03'), createEvent('3', '2025-01-05', '2025-01-05'), @@ -111,7 +112,7 @@ describe('useAgendaEventOccurrencesGroupedByDay', () => { }); it('should respect showWeekends preference when building the day list', () => { - const events: CalendarEvent[] = [ + const events: SchedulerProcessedEvent[] = [ createEvent('1', '2025-10-03', '2025-10-03'), // Fri createEvent('2', '2025-10-04', '2025-10-04'), // Sat createEvent('3', '2025-10-05', '2025-10-05'), // Sun diff --git a/packages/x-scheduler-headless/src/use-event-calendar/tests/selectors.EventCalendarStore.test.ts b/packages/x-scheduler-headless/src/use-event-calendar/tests/selectors.EventCalendarStore.test.ts index c7dfaa064319f..ff6c9a37d76f9 100644 --- a/packages/x-scheduler-headless/src/use-event-calendar/tests/selectors.EventCalendarStore.test.ts +++ b/packages/x-scheduler-headless/src/use-event-calendar/tests/selectors.EventCalendarStore.test.ts @@ -1,4 +1,4 @@ -import { adapter } from 'test/utils/scheduler'; +import { adapter, createOccurrenceFromEvent } from 'test/utils/scheduler'; import { selectors } from './../EventCalendarStore.selectors'; import { EventCalendarState as State } from '../EventCalendarStore.types'; @@ -54,13 +54,12 @@ describe('EventCalendarStore.selectors', () => { surfaceType: 'day-grid', start: adapter.startOfDay(day), end: adapter.endOfDay(day), - originalOccurrence: { - key: 'event-id-key', + originalOccurrence: createOccurrenceFromEvent({ id: 'event-id', title: 'Event', start: adapter.startOfDay(day), end: adapter.endOfDay(day), - }, + }), }, }); expect(selectors.isCreatingNewEventInDayCell(state, day)).to.equal(false); @@ -123,13 +122,12 @@ describe('EventCalendarStore.selectors', () => { surfaceType: 'time-grid', start: adapter.startOfDay(day), end: adapter.endOfDay(day), - originalOccurrence: { + originalOccurrence: createOccurrenceFromEvent({ id: 'event-id', - key: 'event-id-key', title: 'Event', start: adapter.startOfDay(day), end: adapter.endOfDay(day), - }, + }), }, }); expect(selectors.isCreatingNewEventInTimeRange(state, dayStart, dayEnd)).to.equal(false); diff --git a/packages/x-scheduler-headless/src/use-event-calendar/tests/utils.ts b/packages/x-scheduler-headless/src/use-event-calendar/tests/utils.ts index 9571e1c333ea4..e749eead6a92c 100644 --- a/packages/x-scheduler-headless/src/use-event-calendar/tests/utils.ts +++ b/packages/x-scheduler-headless/src/use-event-calendar/tests/utils.ts @@ -1,13 +1,13 @@ import { SchedulerValidDate } from '../../models/date'; -import { CalendarEvent } from '../../models/event'; +import { SchedulerEvent } from '../../models/event'; export function buildEvent( id: string, title: string, start: SchedulerValidDate, end: SchedulerValidDate, - extra: Partial = {}, -): CalendarEvent { + extra: Partial = {}, +): SchedulerEvent { return { id, title, diff --git a/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-day/useEventOccurrencesGroupedByDay.ts b/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-day/useEventOccurrencesGroupedByDay.ts index 02848a6f7b6bd..e5e95200a6702 100644 --- a/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-day/useEventOccurrencesGroupedByDay.ts +++ b/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-day/useEventOccurrencesGroupedByDay.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { useStore } from '@base-ui-components/utils/store'; -import { CalendarEvent, CalendarEventOccurrence, CalendarProcessedDate } from '../models'; +import { SchedulerProcessedEvent, CalendarEventOccurrence, CalendarProcessedDate } from '../models'; import { getDaysTheOccurrenceIsVisibleOn, getOccurrencesFromEvents } from '../utils/event-utils'; import { useAdapter } from '../use-adapter/useAdapter'; import { useEventCalendarStoreContext } from '../use-event-calendar-store-context'; @@ -46,7 +46,7 @@ export namespace useEventOccurrencesGroupedByDay { export function innerGetEventOccurrencesGroupedByDay( adapter: Adapter, days: CalendarProcessedDate[], - events: CalendarEvent[], + events: SchedulerProcessedEvent[], visibleResources: Map, ): Map { // STEP 4: Create a Map of the occurrences grouped by day diff --git a/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-resource/useEventOccurencesGroupedByResource.tsx b/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-resource/useEventOccurencesGroupedByResource.tsx index 73caef40aeda8..9624fffc58c85 100644 --- a/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-resource/useEventOccurencesGroupedByResource.tsx +++ b/packages/x-scheduler-headless/src/use-event-occurrences-grouped-by-resource/useEventOccurencesGroupedByResource.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useStore } from '@base-ui-components/utils/store'; import { - CalendarEvent, + SchedulerProcessedEvent, CalendarEventOccurrence, CalendarResource, SchedulerValidDate, @@ -53,7 +53,7 @@ export namespace useEventOccurrencesGroupedByResource { */ export function innerGetEventOccurrencesGroupedByResource( adapter: Adapter, - events: CalendarEvent[], + events: SchedulerProcessedEvent[], visibleResources: Map, resources: readonly CalendarResource[], start: SchedulerValidDate, diff --git a/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.test.ts b/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.test.ts index bd970cdf07283..810470b260913 100644 --- a/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.test.ts +++ b/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.test.ts @@ -1,8 +1,8 @@ -import { adapter } from 'test/utils/scheduler'; +import { adapter, createProcessedEvent } from 'test/utils/scheduler'; import { renderHook } from '@mui/internal-test-utils'; import { useEventOccurrencesWithDayGridPosition } from './useEventOccurrencesWithDayGridPosition'; import { processDate } from '../process-date'; -import { CalendarEvent } from '../models'; +import { SchedulerProcessedEvent } from '../models'; import { innerGetEventOccurrencesGroupedByDay } from '../use-event-occurrences-grouped-by-day'; describe('useDayListEventOccurrencesWithPosition', () => { @@ -12,7 +12,7 @@ describe('useDayListEventOccurrencesWithPosition', () => { processDate(adapter.date('2024-01-17'), adapter), ]; - function testHook(events: CalendarEvent[]) { + function testHook(events: SchedulerProcessedEvent[]) { const { result } = renderHook(() => { const occurrencesMap = innerGetEventOccurrencesGroupedByDay(adapter, days, events, new Map()); return useEventOccurrencesWithDayGridPosition({ days, occurrencesMap }); @@ -21,12 +21,13 @@ describe('useDayListEventOccurrencesWithPosition', () => { return result.current; } - const createEvent = (id: string, start: string, end: string): CalendarEvent => ({ - id, - start: adapter.date(start), - end: adapter.date(end), - title: `Event ${id}`, - }); + const createEvent = (id: string, start: string, end: string) => + createProcessedEvent({ + id, + start: adapter.date(start), + end: adapter.date(end), + title: `Event ${id}`, + }); it('should set index to 1 for the first event on a day', () => { const result = testHook([createEvent('A', '2024-01-15', '2024-01-15')]); diff --git a/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.ts b/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.ts index e78609eba5d34..a170a795f8f69 100644 --- a/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.ts +++ b/packages/x-scheduler-headless/src/use-event-occurrences-with-day-grid-position/useEventOccurrencesWithDayGridPosition.ts @@ -53,7 +53,7 @@ export function useEventOccurrencesWithDayGridPosition( i += 1; } - const durationInDays = diffIn(adapter, occurrence.end, day.value, 'days') + 1; + const durationInDays = diffIn(adapter, occurrence.end.value, day.value, 'days') + 1; position = { index: i, daySpan: Math.min(durationInDays, dayListSize - dayIndex), // Don't go past the day list end diff --git a/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts b/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts index 71a9eee8c3942..03a7cdbb9ca32 100644 --- a/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts +++ b/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.test.ts @@ -1,14 +1,14 @@ -import { adapter } from 'test/utils/scheduler'; +import { adapter, createProcessedEvent } from 'test/utils/scheduler'; import { renderHook } from '@mui/internal-test-utils'; import { useEventOccurrencesWithTimelinePosition } from './useEventOccurrencesWithTimelinePosition'; import { getOccurrencesFromEvents } from '../utils/event-utils'; -import { CalendarEvent } from '../models'; +import { SchedulerProcessedEvent } from '../models'; describe('useDayListEventOccurrencesWithPosition', () => { const collectionStart = adapter.date('2024-01-15'); const collectionEnd = adapter.endOfDay(adapter.date('2024-01-15')); - function testHook(events: CalendarEvent[], maxSpan: number) { + function testHook(events: SchedulerProcessedEvent[], maxSpan: number) { const { result } = renderHook(() => { const occurrences = getOccurrencesFromEvents({ adapter, @@ -23,12 +23,13 @@ describe('useDayListEventOccurrencesWithPosition', () => { return result.current; } - const createEvent = (id: string, start: string, end: string): CalendarEvent => ({ - id, - start: adapter.date(start), - end: adapter.date(end), - title: `Event ${id}`, - }); + const createEvent = (id: string, start: string, end: string) => + createProcessedEvent({ + id, + start: adapter.date(start), + end: adapter.date(end), + title: `Event ${id}`, + }); it('should set firstIndex and lastIndex to all events when no events are overlapping', () => { const result = testHook( diff --git a/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts b/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts index a03365b7ae36e..c8f579c9fd85e 100644 --- a/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts +++ b/packages/x-scheduler-headless/src/use-event-occurrences-with-timeline-position/useEventOccurrencesWithTimelinePosition.ts @@ -73,8 +73,8 @@ function buildOccurrenceConflicts( // Computes the properties needed for each occurrence. for (const occurrence of occurrences) { // TODO: Avoid JS Date conversion - const startTimestamp = adapter.toJsDate(occurrence.start).getTime(); - const endTimestamp = adapter.toJsDate(occurrence.end).getTime(); + const startTimestamp = occurrence.start.timestamp; + const endTimestamp = occurrence.end.timestamp; const occurrenceDurationMs = endTimestamp - startTimestamp; if (startTimestamp >= lastEndTimestamp) { diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts index e79bce0c8657d..9c6ca4d418e9d 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts @@ -2,7 +2,7 @@ import { Store } from '@base-ui-components/utils/store'; // TODO: Use the Base UI warning utility once it supports cleanup in tests. import { warnOnce } from '@mui/x-internals/warning'; import { - CalendarEvent, + SchedulerProcessedEvent, CalendarEventId, CalendarEventOccurrence, CalendarOccurrencePlaceholder, @@ -10,6 +10,7 @@ import { SchedulerValidDate, CalendarEventUpdatedProperties, RecurringEventUpdateScope, + SchedulerEvent, } from '../../models'; import { SchedulerState, @@ -37,7 +38,9 @@ import { TimeoutManager } from '../TimeoutManager'; import { DEFAULT_EVENT_COLOR } from '../../constants'; // TODO: Add a prop to configure the behavior. -export const DEFAULT_IS_MULTI_DAY_EVENT = (event: CalendarEvent | CalendarEventOccurrence) => { +export const DEFAULT_IS_MULTI_DAY_EVENT = ( + event: SchedulerProcessedEvent | CalendarEventOccurrence, +) => { if (event.allDay) { return true; } @@ -74,7 +77,7 @@ export class SchedulerStore< ) { const schedulerInitialState: SchedulerState = { ...SchedulerStore.deriveStateFromParameters(parameters, adapter), - ...buildEventsState(parameters), + ...buildEventsState(parameters, adapter), ...buildResourcesState(parameters), adapter, occurrencePlaceholder: null, @@ -176,9 +179,10 @@ export class SchedulerStore< if ( parameters.events !== this.parameters.events || - parameters.eventModelStructure !== this.parameters.eventModelStructure + parameters.eventModelStructure !== this.parameters.eventModelStructure || + adapter !== this.state.adapter ) { - Object.assign(newSchedulerState, buildEventsState(parameters)); + Object.assign(newSchedulerState, buildEventsState(parameters, adapter)); } if ( @@ -270,9 +274,8 @@ export class SchedulerStore< /** * Creates a new event in the calendar. */ - public createEvent = (calendarEvent: CalendarEvent): CalendarEvent => { + public createEvent = (calendarEvent: SchedulerEvent) => { this.updateEvents({ created: [calendarEvent] }); - return calendarEvent; }; /** diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts index 4f4fb740c2f9b..c663cc56573ee 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts @@ -1,5 +1,5 @@ import { - CalendarEvent, + SchedulerProcessedEvent, CalendarEventColor, CalendarEventOccurrence, CalendarOccurrencePlaceholder, @@ -10,6 +10,7 @@ import { CalendarEventId, SchedulerResourceModelStructure, SchedulerEventModelStructure, + SchedulerEvent, } from '../../models'; import { Adapter } from '../../use-adapter/useAdapter.types'; @@ -38,7 +39,7 @@ export interface SchedulerState { /** * A lookup to get the processed event from its ID. */ - processedEventLookup: Map; + processedEventLookup: Map; /** * The structure of the event model. * It defines how to read and write properties of the event model. @@ -103,7 +104,7 @@ export interface SchedulerState { * A multi day event is rendered in the day grid instead of the time grid when both are available. * It can also be styled differently in the day grid. */ - isMultiDayEvent: (event: CalendarEvent | CalendarEventOccurrence) => boolean; + isMultiDayEvent: (event: SchedulerProcessedEvent | CalendarEventOccurrence) => boolean; /** * Whether the calendar is in read-only mode. * @default false @@ -251,6 +252,6 @@ export type SchedulerModelUpdater< export interface UpdateEventsParameters { deleted?: CalendarEventId[]; - created?: CalendarEvent[]; + created?: SchedulerEvent[]; updated?: CalendarEventUpdatedProperties[]; } diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.utils.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.utils.ts index c86f1c502ca1f..4fe5d0f14dafe 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.utils.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.utils.ts @@ -1,13 +1,15 @@ import { EMPTY_ARRAY } from '@base-ui-components/utils/empty'; import { - CalendarEvent, + SchedulerProcessedEvent, CalendarEventId, CalendarOccurrencePlaceholder, CalendarResource, CalendarResourceId, SchedulerEventModelStructure, SchedulerResourceModelStructure, + SchedulerEvent, } from '../../models'; +import { processEvent } from '../../process-event'; import { Adapter } from '../../use-adapter/useAdapter.types'; import { SchedulerParameters, SchedulerState } from './SchedulerStore.types'; @@ -41,7 +43,7 @@ export function shouldUpdateOccurrencePlaceholder( export const DEFAULT_EVENT_MODEL_STRUCTURE: SchedulerEventModelStructure = {}; -const EVENT_PROPERTIES_LOOKUP: { [P in keyof CalendarEvent]-?: true } = { +const EVENT_PROPERTIES_LOOKUP: { [P in keyof SchedulerEvent]-?: true } = { id: true, title: true, description: true, @@ -55,7 +57,7 @@ const EVENT_PROPERTIES_LOOKUP: { [P in keyof CalendarEvent]-?: true } = { exDates: true, }; -const EVENT_PROPERTIES = Object.keys(EVENT_PROPERTIES_LOOKUP) as (keyof CalendarEvent)[]; +const EVENT_PROPERTIES = Object.keys(EVENT_PROPERTIES_LOOKUP) as (keyof SchedulerProcessedEvent)[]; const RESOURCE_PROPERTIES_LOOKUP: { [P in keyof CalendarResource]-?: true } = { id: true, @@ -69,31 +71,35 @@ const RESOURCE_PROPERTIES = Object.keys(RESOURCE_PROPERTIES_LOOKUP) as (keyof Ca * Converts an event model to a processed event using the provided model structure. */ export function getProcessedEventFromModel( - event: TEvent, + model: TEvent, + adapter: Adapter, eventModelStructure: SchedulerEventModelStructure | undefined, -): CalendarEvent { - const processedEvent = {} as CalendarEvent; +): SchedulerProcessedEvent { + // 1. Convert the model to a default event model + const modelInDefaultFormat = {} as SchedulerEvent; for (const key of EVENT_PROPERTIES) { + // @ts-ignore const getter = eventModelStructure?.[key]?.getter; // @ts-ignore - processedEvent[key] = getter ? getter(event) : event[key]; + modelInDefaultFormat[key] = getter ? getter(model) : model[key]; } - return processedEvent; + // 2. Convert the default event model to a processed event + return processEvent(modelInDefaultFormat, adapter); } /** * Updates an event model based on the provided changes and model structure. */ export function getUpdatedEventModelFromChanges( - event: TEvent, - changes: Partial, + oldModel: TEvent, + changes: Partial, eventModelStructure: SchedulerEventModelStructure | undefined, ): TEvent { - return createOrUpdateEventModelFromProcessedEvent( - event, + return createOrUpdateEventModelFromBuiltInEvnetModel( + oldModel, changes, eventModelStructure, ); @@ -103,30 +109,30 @@ export function getUpdatedEventModelFromChanges( * Create an event model from a processed event using the provided model structure. */ export function createEventModel( - processedEvent: CalendarEvent, + event: SchedulerEvent, eventModelStructure: SchedulerEventModelStructure | undefined, ): TEvent { - return createOrUpdateEventModelFromProcessedEvent( + return createOrUpdateEventModelFromBuiltInEvnetModel( null, - processedEvent, + event, eventModelStructure, ); } -function createOrUpdateEventModelFromProcessedEvent< +function createOrUpdateEventModelFromBuiltInEvnetModel< TEvent extends object, TIsCreating extends boolean, >( - initialProcessedEvent: TIsCreating extends true ? null : TEvent, - changes: TIsCreating extends true ? CalendarEvent : Partial, + oldModel: TIsCreating extends true ? null : TEvent, + changes: TIsCreating extends true ? SchedulerEvent : Partial, eventModelStructure: SchedulerEventModelStructure | undefined, ) { - let eventModel = initialProcessedEvent == null ? {} : { ...initialProcessedEvent }; + let eventModel = oldModel == null ? {} : { ...oldModel }; const propertiesWithSetter: [AnyEventSetter, any][] = []; for (const key in changes) { if (changes.hasOwnProperty(key)) { - const typedKey = key as keyof CalendarEvent; + const typedKey = key as keyof SchedulerEvent; const setter = eventModelStructure?.[typedKey]?.setter; if (setter) { // @ts-ignore @@ -171,6 +177,7 @@ type AnyEventSetter = ( export function buildEventsState( parameters: Pick, 'events' | 'eventModelStructure'>, + adapter: Adapter, ): Pick< SchedulerState, | 'eventIdList' @@ -183,9 +190,9 @@ export function buildEventsState(); - const processedEventLookup = new Map(); + const processedEventLookup = new Map(); for (const event of events) { - const processedEvent = getProcessedEventFromModel(event, eventModelStructure); + const processedEvent = getProcessedEventFromModel(event, adapter, eventModelStructure); eventIdList.push(processedEvent.id); eventModelLookup.set(processedEvent.id, event); processedEventLookup.set(processedEvent.id, processedEvent); diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts index dd7a05295a26a..76b5691332ee2 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts @@ -1,9 +1,9 @@ import { adapter } from 'test/utils/scheduler'; -import { CalendarEvent } from '@mui/x-scheduler-headless/models'; +import { SchedulerEvent } from '@mui/x-scheduler-headless/models'; import { storeClasses, buildEvent } from './utils'; import { selectors } from '../../../scheduler-selectors'; -const DEFAULT_PARAMS = { events: [] as CalendarEvent[] }; +const DEFAULT_PARAMS = { events: [] as SchedulerEvent[] }; storeClasses.forEach((storeClass) => { describe(`Core - ${storeClass.name}`, () => { diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/event.SchedulerStore.test.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/event.SchedulerStore.test.ts index 924cc2b40d6bc..4b71019fa3ea1 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/event.SchedulerStore.test.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/event.SchedulerStore.test.ts @@ -327,7 +327,8 @@ storeClasses.forEach((storeClass) => { { description: 'New event description', allDay: true }, ); - const created = store.createEvent(newEvent); + store.createEvent(newEvent); + const created = selectors.event(store.state, '2')!; expect(created.id).to.equal('2'); expect(created.title).to.equal('New Event'); diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/recurring-event.SchedulerStore.test.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/recurring-event.SchedulerStore.test.ts index dc4fcc9aa600d..497b497eaf499 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/recurring-event.SchedulerStore.test.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/recurring-event.SchedulerStore.test.ts @@ -1,5 +1,6 @@ import { adapter } from 'test/utils/scheduler'; import { RecurringEventRecurrenceRule } from '@mui/x-scheduler-headless/models'; +import { processDate } from '@mui/x-scheduler-headless/process-date'; import { storeClasses } from './utils'; import { getWeekDayCode } from '../../recurring-event-utils'; import { selectors } from '../../../scheduler-selectors'; @@ -16,7 +17,7 @@ storeClasses.forEach((storeClass) => { describe('Selector: recurrencePresets', () => { it('returns daily, weekly, monthly and yearly presets', () => { const state = baseState(); - const start = adapter.date('2025-08-05T09:00:00Z'); // Tuesday + const start = processDate(adapter.date('2025-08-05T09:00:00Z'), adapter); // Tuesday const presets = selectors.recurrencePresets(state, start); expect(presets.daily).to.deep.equal({ @@ -26,7 +27,7 @@ storeClasses.forEach((storeClass) => { expect(presets.weekly).to.deep.equal({ freq: 'WEEKLY', interval: 1, - byDay: [getWeekDayCode(adapter, start)], + byDay: [getWeekDayCode(adapter, start.value)], }); expect(presets.monthly).to.deep.equal({ freq: 'MONTHLY', @@ -42,7 +43,7 @@ storeClasses.forEach((storeClass) => { describe('Selector: defaultRecurrencePresetKey', () => { const state = baseState(); - const start = adapter.date('2025-08-05T09:00:00'); // Tuesday + const start = processDate(adapter.date('2025-08-05T09:00:00'), adapter); // Tuesday const presets = selectors.recurrencePresets(state, start); it('returns null when rule undefined', () => { diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/utils.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/utils.ts index 31f501c4291ad..4bd6073abab68 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/utils.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/utils.ts @@ -1,5 +1,5 @@ import { SchedulerValidDate } from '../../../models/date'; -import { CalendarEvent } from '../../../models/event'; +import { SchedulerEvent } from '../../../models/event'; import { EventCalendarStore } from '../../../use-event-calendar'; import { TimelineStore } from '../../../use-timeline'; @@ -8,8 +8,8 @@ export function buildEvent( title: string, start: SchedulerValidDate, end: SchedulerValidDate, - extra: Partial = {}, -): CalendarEvent { + extra: Partial = {}, +): SchedulerEvent { return { id, title, diff --git a/packages/x-scheduler-headless/src/utils/event-utils.test.ts b/packages/x-scheduler-headless/src/utils/event-utils.test.ts index 942c3667ad3ad..6dc19fa2ef27e 100644 --- a/packages/x-scheduler-headless/src/utils/event-utils.test.ts +++ b/packages/x-scheduler-headless/src/utils/event-utils.test.ts @@ -1,21 +1,16 @@ -import { adapter } from 'test/utils/scheduler'; -import { CalendarEventOccurrence } from '@mui/x-scheduler-headless/models'; +import { adapter, createOccurrenceFromEvent } from 'test/utils/scheduler'; import { getDaysTheOccurrenceIsVisibleOn } from './event-utils'; import { processDate } from '../process-date'; describe('event-utils', () => { - const createEventOccurrence = ( - id: string, - start: string, - end: string, - ): CalendarEventOccurrence => ({ - id, - key: id, - start: adapter.date(start), - end: adapter.date(end), - title: `Event ${id}`, - allDay: true, - }); + const createEventOccurrence = (id: string, start: string, end: string) => + createOccurrenceFromEvent({ + id, + start: adapter.date(start), + end: adapter.date(end), + title: `Event ${id}`, + allDay: true, + }); describe('getDaysTheOccurrenceIsVisibleOn', () => { const days = [ diff --git a/packages/x-scheduler-headless/src/utils/event-utils.ts b/packages/x-scheduler-headless/src/utils/event-utils.ts index eea86c59181ef..56e2f434c17b3 100644 --- a/packages/x-scheduler-headless/src/utils/event-utils.ts +++ b/packages/x-scheduler-headless/src/utils/event-utils.ts @@ -1,6 +1,6 @@ import { SchedulerValidDate, - CalendarEvent, + SchedulerProcessedEvent, CalendarProcessedDate, CalendarEventOccurrence, } from '../models'; @@ -18,12 +18,12 @@ export function getDaysTheOccurrenceIsVisibleOn( const dayKeys: string[] = []; for (const day of days) { // If the day is before the event start, skip to the next day - if (adapter.isBeforeDay(day.value, event.start)) { + if (adapter.isBeforeDay(day.value, event.start.value)) { continue; } // If the day is after the event end, break as the days are sorted by start date - if (adapter.isAfterDay(day.value, event.end)) { + if (adapter.isAfterDay(day.value, event.end.value)) { break; } dayKeys.push(day.key); @@ -52,7 +52,7 @@ export function getOccurrencesFromEvents(parameters: GetOccurrencesFromEventsPar } // STEP 2-B: Non-recurring event processing, skip events that are not within the visible days - if (adapter.isAfter(event.start, end) || adapter.isBefore(event.end, start)) { + if (adapter.isAfter(event.start.value, end) || adapter.isBefore(event.end.value, start)) { continue; } @@ -67,8 +67,8 @@ export function getOccurrencesFromEvents(parameters: GetOccurrencesFromEventsPar // TODO: Avoid JS Date conversion .map((occurrence) => ({ occurrence, - start: adapter.toJsDate(occurrence.start).getTime(), - end: adapter.toJsDate(occurrence.end).getTime(), + start: adapter.toJsDate(occurrence.start.value).getTime(), + end: adapter.toJsDate(occurrence.end.value).getTime(), })) .sort((a, b) => a.start - b.start || b.end - a.end) .map((item) => item.occurrence) @@ -79,6 +79,6 @@ interface GetOccurrencesFromEventsParameters { adapter: Adapter; start: SchedulerValidDate; end: SchedulerValidDate; - events: CalendarEvent[]; + events: SchedulerProcessedEvent[]; visibleResources: Map; } diff --git a/packages/x-scheduler-headless/src/utils/recurring-event-utils.test.ts b/packages/x-scheduler-headless/src/utils/recurring-event-utils.test.ts index 02c874e96173d..445fb7ec8f5e2 100644 --- a/packages/x-scheduler-headless/src/utils/recurring-event-utils.test.ts +++ b/packages/x-scheduler-headless/src/utils/recurring-event-utils.test.ts @@ -1,11 +1,12 @@ -import { adapter, adapterFr } from 'test/utils/scheduler'; +import { adapter, adapterFr, createProcessedEvent } from 'test/utils/scheduler'; import { RecurringEventWeekDayCode, RecurringEventByDayValue, - CalendarEvent, + SchedulerProcessedEvent, CalendarEventUpdatedProperties, RecurringEventRecurrenceRule, SchedulerValidDate, + SchedulerEvent, } from '@mui/x-scheduler-headless/models'; import { getRecurringEventOccurrencesForVisibleDays, @@ -33,15 +34,16 @@ import { diffIn } from '../use-adapter'; import { mergeDateAndTime } from './date-utils'; describe('recurring-event-utils', () => { - const makeRecurringEvent = (overrides: Partial = {}): CalendarEvent => ({ - id: 'recurring', - title: 'Recurring Event', - start: adapter.date('2025-01-01T09:00:00Z'), - end: adapter.date('2025-01-01T10:00:00Z'), - allDay: false, - rrule: { freq: 'DAILY', interval: 1 }, - ...overrides, - }); + const createRecurringEvent = (overrides: Partial = {}) => + createProcessedEvent({ + id: 'recurring', + title: 'Recurring Event', + start: adapter.date('2025-01-01T09:00:00Z'), + end: adapter.date('2025-01-01T10:00:00Z'), + allDay: false, + rrule: { freq: 'DAILY', interval: 1 }, + ...overrides, + }); describe('getWeekDayCodeForDate', () => { it('should work with fr (week starts on Monday)', () => { @@ -90,14 +92,15 @@ describe('recurring-event-utils', () => { }); describe('getAllDaySpanDays', () => { - const createEvent = (overrides: Partial): CalendarEvent => ({ - id: 'event-1', - title: 'Test Event', - start: adapter.date('2025-01-01T09:00:00Z'), - end: adapter.date('2025-01-01T10:00:00Z'), - allDay: false, - ...overrides, - }); + const createEvent = (overrides: Partial) => + createProcessedEvent({ + id: 'event-1', + title: 'Test Event', + start: adapter.date('2025-01-01T09:00:00Z'), + end: adapter.date('2025-01-01T10:00:00Z'), + allDay: false, + ...overrides, + }); // TODO: This should change after we implement support for timed events that span multiple days it('returns 1 for non-allDay multi-day event', () => { @@ -365,12 +368,13 @@ describe('recurring-event-utils', () => { describe('matchesRecurrence', () => { const baseStart = adapter.date('2025-01-10T09:30:00Z'); // Friday - const createEvent = (start = baseStart): CalendarEvent => ({ - id: 'event-1', - title: 'Test Event', - start, - end: adapter.addHours(start, 1), - }); + const createEvent = (start = baseStart) => + createProcessedEvent({ + id: 'event-1', + title: 'Test Event', + start, + end: adapter.addHours(start, 1), + }); describe('daily frequency', () => { it('returns false for date before series start', () => { @@ -395,13 +399,13 @@ describe('recurring-event-utils', () => { describe('weekly frequency', () => { it('returns true when the weekday is in byDay', () => { const event = createEvent(); - const code = getWeekDayCode(adapter, event.start); + const code = getWeekDayCode(adapter, event.start.value); const rule: RecurringEventRecurrenceRule = { freq: 'WEEKLY', interval: 1, byDay: [code], }; - expect(matchesRecurrence(rule, event.start, adapter, event)).to.equal(true); + expect(matchesRecurrence(rule, event.start.value, adapter, event)).to.equal(true); }); it('returns false when the weekday is not in byDay', () => { @@ -411,12 +415,12 @@ describe('recurring-event-utils', () => { interval: 1, byDay: ['MO'], // Monday }; - expect(matchesRecurrence(rule, event.start, adapter, event)).to.equal(false); // Friday start + expect(matchesRecurrence(rule, event.start.value, adapter, event)).to.equal(false); // Friday start }); it('interval > 1 (every 2 weeks) includes only correct weeks', () => { const event = createEvent(baseStart); - const code = getWeekDayCode(adapter, event.start); // FR + const code = getWeekDayCode(adapter, event.start.value); // FR const rule: RecurringEventRecurrenceRule = { freq: 'WEEKLY', interval: 2, @@ -437,7 +441,7 @@ describe('recurring-event-utils', () => { interval: 1, byDay: ['MO', 'TU', 'FR'], }; - expect(matchesRecurrence(rule, event.start, adapter, event)).to.equal(true); // Friday + expect(matchesRecurrence(rule, event.start.value, adapter, event)).to.equal(true); // Friday }); it('does not match days before DTSTART within the first week', () => { @@ -479,7 +483,7 @@ describe('recurring-event-utils', () => { it('throws an error for ordinal BYDAY values (e.g., 1MO)', () => { const event = createEvent(); const bad: RecurringEventRecurrenceRule = { freq: 'WEEKLY', byDay: ['1MO'] }; - expect(() => matchesRecurrence(bad, event.start, adapter, event)).to.throw(); + expect(() => matchesRecurrence(bad, event.start.value, adapter, event)).to.throw(); }); }); @@ -487,13 +491,13 @@ describe('recurring-event-utils', () => { describe('byMonthDay', () => { it('returns true on start month/day', () => { const event = createEvent(); - const day = adapter.getDate(event.start); + const day = adapter.getDate(event.start.value); const rule: RecurringEventRecurrenceRule = { freq: 'MONTHLY', interval: 1, byMonthDay: [day], }; - expect(matchesRecurrence(rule, event.start, adapter, event)).to.equal(true); + expect(matchesRecurrence(rule, event.start.value, adapter, event)).to.equal(true); }); it('interval > 1 (every 2 months) includes only correct months', () => { @@ -629,7 +633,7 @@ describe('recurring-event-utils', () => { it('returns true on start year', () => { const event = createEvent(); const rule: RecurringEventRecurrenceRule = { freq: 'YEARLY', interval: 1 }; - expect(matchesRecurrence(rule, event.start, adapter, event)).to.equal(true); + expect(matchesRecurrence(rule, event.start.value, adapter, event)).to.equal(true); }); it('interval > 1 (every 2 years) includes only correct years', () => { @@ -655,9 +659,9 @@ describe('recurring-event-utils', () => { const bad1: RecurringEventRecurrenceRule = { freq: 'YEARLY', byMonth: [7] }; const bad2: RecurringEventRecurrenceRule = { freq: 'YEARLY', byMonthDay: [20] }; const bad3: RecurringEventRecurrenceRule = { freq: 'YEARLY', byDay: ['MO'] }; - expect(() => matchesRecurrence(bad1, event.start, adapter, event)).to.throw(); - expect(() => matchesRecurrence(bad2, event.start, adapter, event)).to.throw(); - expect(() => matchesRecurrence(bad3, event.start, adapter, event)).to.throw(); + expect(() => matchesRecurrence(bad1, event.start.value, adapter, event)).to.throw(); + expect(() => matchesRecurrence(bad2, event.start.value, adapter, event)).to.throw(); + expect(() => matchesRecurrence(bad3, event.start.value, adapter, event)).to.throw(); }); }); }); @@ -934,18 +938,19 @@ describe('recurring-event-utils', () => { }); describe('getRecurringEventOccurrencesForVisibleDays', () => { - const createEvent = (overrides: Partial): CalendarEvent => ({ - id: 'base-event', - title: 'Recurring Test Event', - start: adapter.date('2025-01-01T09:00:00Z'), - end: adapter.date('2025-01-01T10:30:00Z'), - allDay: false, - rrule: { - freq: 'DAILY', - interval: 1, - }, - ...overrides, - }); + const createEvent = (overrides: Partial) => + createProcessedEvent({ + id: 'base-event', + title: 'Recurring Test Event', + start: adapter.date('2025-01-01T09:00:00Z'), + end: adapter.date('2025-01-01T10:30:00Z'), + allDay: false, + rrule: { + freq: 'DAILY', + interval: 1, + }, + ...overrides, + }); it('generates daily timed occurrences within visible range preserving duration', () => { const visibleStart = adapter.date('2025-01-10T00:00:00Z'); @@ -964,11 +969,11 @@ describe('recurring-event-utils', () => { expect(result).to.have.length(5); for (let i = 0; i < result.length; i += 1) { const occ = result[i]; - expect(adapter.format(occ.start, 'keyboardDate')).to.equal( + expect(occ.start.key).to.equal( adapter.format(adapter.addDays(visibleStart, i), 'keyboardDate'), ); - expect(diffIn(adapter, occ.end, occ.start, 'minutes')).to.equal(90); - expect(occ.key).to.equal(`${event.id}::${adapter.format(occ.start, 'keyboardDate')}`); + expect(diffIn(adapter, occ.end.value, occ.start.value, 'minutes')).to.equal(90); + expect(occ.key).to.equal(`${event.id}::${occ.start.key}`); } }); @@ -988,7 +993,7 @@ describe('recurring-event-utils', () => { adapter, ); // Jan 1..5 inclusive - expect(result.map((o) => adapter.getDate(o.start))).to.deep.equal([1, 2, 3, 4, 5]); + expect(result.map((o) => adapter.getDate(o.start.value))).to.deep.equal([1, 2, 3, 4, 5]); }); it('respects "count" end rule (count=3 gives 3 occurrences)', () => { @@ -1004,7 +1009,7 @@ describe('recurring-event-utils', () => { adapter, ); expect(result).to.have.length(3); - expect(result.map((o) => adapter.getDate(o.start))).to.deep.equal([1, 2, 3]); + expect(result.map((o) => adapter.getDate(o.start.value))).to.deep.equal([1, 2, 3]); }); it('applies weekly interval > 1 (e.g. every 2 weeks)', () => { @@ -1022,7 +1027,7 @@ describe('recurring-event-utils', () => { adapter, ); // Expect Fridays at week 0, 2 and 4 - const dates = result.map((o) => adapter.getDate(o.start)); + const dates = result.map((o) => adapter.getDate(o.start.value)); expect(dates).to.deep.equal([3, 17, 31]); }); @@ -1044,7 +1049,7 @@ describe('recurring-event-utils', () => { adapter.addDays(visibleStart, 119), adapter, ); - const daysOfMonth = result.map((o) => adapter.getDate(o.start)); + const daysOfMonth = result.map((o) => adapter.getDate(o.start.value)); expect(daysOfMonth).to.deep.equal([10, 10, 10, 10]); }); @@ -1062,7 +1067,7 @@ describe('recurring-event-utils', () => { adapter.addYears(visibleStart, 5), adapter, ); - const years = result.map((o) => adapter.getYear(o.start)); + const years = result.map((o) => adapter.getYear(o.start.value)); expect(years).to.deep.equal([2025, 2027, 2029]); }); @@ -1085,8 +1090,8 @@ describe('recurring-event-utils', () => { adapter, ); expect(result).to.have.length(1); - expect(adapter.getDate(result[0].start)).to.equal(3); - expect(adapter.getDate(result[0].end)).to.equal(6); + expect(adapter.getDate(result[0].start.value)).to.equal(3); + expect(adapter.getDate(result[0].end.value)).to.equal(6); }); it('does not generate occurrences earlier than DTSTART within the first week even if byDay spans the week', () => { @@ -1096,7 +1101,7 @@ describe('recurring-event-utils', () => { // DTSTART on Wednesday of that same week const start = adapter.addDays(weekStart, 2); // Wednesday - const event: CalendarEvent = createEvent({ + const event: SchedulerProcessedEvent = createEvent({ id: 'standup', title: 'Standup', start, @@ -1110,7 +1115,7 @@ describe('recurring-event-utils', () => { adapter.addDays(visibleStart, 7), adapter, ); - const dows = result.map((o) => getWeekDayCode(adapter, o.start)); + const dows = result.map((o) => getWeekDayCode(adapter, o.start.value)); // Only WE, TH, FR in the first week expect(dows).to.deep.equal(['WE', 'TH', 'FR']); @@ -1145,7 +1150,7 @@ describe('recurring-event-utils', () => { const call = ( originalRule: RecurringEventRecurrenceRule, - changes: Partial = {}, + changes: Partial = {}, originalSeriesStart: SchedulerValidDate = seriesStart, split: SchedulerValidDate = splitStart, ) => decideSplitRRule(adapter, originalRule, originalSeriesStart, split, changes); @@ -1280,7 +1285,7 @@ describe('recurring-event-utils', () => { describe('applyRecurringUpdateFollowing', () => { it('should set extractedFromId for the new series', () => { // Original: daily from Jan 01 - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = adapter.date('2025-01-07T09:00:00Z'); const changes: CalendarEventUpdatedProperties = { @@ -1302,7 +1307,7 @@ describe('recurring-event-utils', () => { it('should truncate the original series at the day before the edited occurrence and appends the new series', () => { // Original: daily from Jan 01 - const original = makeRecurringEvent(); + const original = createRecurringEvent(); // Edit an occurrence on Jan 05 const occurrenceStart = adapter.date('2025-01-05T09:00:00Z'); @@ -1348,15 +1353,15 @@ describe('recurring-event-utils', () => { it('should drop the original series when occurrence is on the DTSTART day (no remaining occurrences)', () => { // Original: daily from Jan 10 - const original = makeRecurringEvent({ + const original = createRecurringEvent({ start: adapter.date('2025-01-10T09:00:00Z'), end: adapter.date('2025-01-10T10:00:00Z'), }); // occurrenceStart same calendar day as DTSTART → shouldDropOldSeries = true const occurrenceStart = adapter.date('2025-01-10T09:00:00Z'); - const changes: CalendarEvent = { - ...original, + const changes: CalendarEventUpdatedProperties = { + id: original.id, start: adapter.date('2025-01-10T12:00:00Z'), end: adapter.date('2025-01-10T13:00:00Z'), title: 'Edited First', @@ -1388,10 +1393,10 @@ describe('recurring-event-utils', () => { it('should use provided changes.rrule for the new series', () => { // Original: daily from Jan 01 - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = adapter.date('2025-01-03T09:00:00Z'); - const changes: CalendarEvent = { - ...original, + const changes: CalendarEventUpdatedProperties = { + id: original.id, start: adapter.date('2025-01-03T10:00:00Z'), end: adapter.date('2025-01-03T11:00:00Z'), rrule: { @@ -1418,7 +1423,7 @@ describe('recurring-event-utils', () => { it('should remove recurrence for the new series when changes.rrule is explicitly undefined', () => { // Original: daily from Jan 01 - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = adapter.date('2025-01-04T09:00:00Z'); const changes = { @@ -1436,7 +1441,7 @@ describe('recurring-event-utils', () => { it('should inherit the original rule when changes.rrule is omitted', () => { // Original: daily from Jan 01 - const original = makeRecurringEvent({ rrule: { freq: 'DAILY', interval: 2 } }); + const original = createRecurringEvent({ rrule: { freq: 'DAILY', interval: 2 } }); const occurrenceStart = adapter.date('2025-01-06T09:00:00Z'); const changes: CalendarEventUpdatedProperties = { @@ -1521,7 +1526,7 @@ describe('recurring-event-utils', () => { describe('applyRecurringUpdateAll', () => { it('should replace exactly one event without creating duplicates', () => { - const original = makeRecurringEvent({ id: 'rec-1' }); + const original = createRecurringEvent({ id: 'rec-1' }); const occurrenceStart = adapter.date('2025-01-05T09:00:00Z'); const changes = { id: original.id, @@ -1536,7 +1541,7 @@ describe('recurring-event-utils', () => { }); it('should use the rrule provided in changes when present', () => { - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = original.start; const changes: CalendarEventUpdatedProperties = { @@ -1547,7 +1552,12 @@ describe('recurring-event-utils', () => { end: adapter.date('2025-01-01T11:00:00Z'), }; - const updatedEvents = applyRecurringUpdateAll(adapter, original, occurrenceStart, changes); + const updatedEvents = applyRecurringUpdateAll( + adapter, + original, + occurrenceStart.value, + changes, + ); expect(updatedEvents.deleted).to.equal(undefined); expect(updatedEvents.created).to.equal(undefined); @@ -1555,7 +1565,7 @@ describe('recurring-event-utils', () => { }); it('should remove recurrence when changes.rrule is explicitly undefined', () => { - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = original.start; const changes: CalendarEventUpdatedProperties = { @@ -1564,7 +1574,12 @@ describe('recurring-event-utils', () => { rrule: undefined, }; - const updatedEvents = applyRecurringUpdateAll(adapter, original, occurrenceStart, changes); + const updatedEvents = applyRecurringUpdateAll( + adapter, + original, + occurrenceStart.value, + changes, + ); expect(updatedEvents.deleted).to.equal(undefined); expect(updatedEvents.created).to.equal(undefined); @@ -1572,7 +1587,7 @@ describe('recurring-event-utils', () => { }); it('should keep the original date and just update hours/minutes when changing the time of a non-first occurrence', () => { - const original = makeRecurringEvent(); + const original = createRecurringEvent(); // Edited the Jan 05 occurrence and changed only the time const occurrenceStart = adapter.date('2025-01-05T09:00:00Z'); @@ -1593,14 +1608,14 @@ describe('recurring-event-utils', () => { expect(updatedEvents.updated).to.deep.equal([ { ...changes, - start: mergeDateAndTime(adapter, original.start, newStart), - end: mergeDateAndTime(adapter, original.end, newEnd), + start: mergeDateAndTime(adapter, original.start.value, newStart), + end: mergeDateAndTime(adapter, original.end.value, newEnd), }, ]); }); it('should update the rrule when editing a non-first occurrence with a different day', () => { - const original = makeRecurringEvent({ rrule: { byDay: ['SU'], freq: 'WEEKLY' } }); + const original = createRecurringEvent({ rrule: { byDay: ['SU'], freq: 'WEEKLY' } }); const occurrenceStart = adapter.date('2025-01-05T09:00:00Z'); // Jan 5, a Sunday const changes: CalendarEventUpdatedProperties = { id: original.id, @@ -1615,15 +1630,15 @@ describe('recurring-event-utils', () => { expect(updatedEvents.updated).to.deep.equal([ { ...changes, - start: mergeDateAndTime(adapter, original.start, changes.start!), - end: mergeDateAndTime(adapter, original.end, changes.end!), + start: mergeDateAndTime(adapter, original.start.value, changes.start!), + end: mergeDateAndTime(adapter, original.end.value, changes.end!), rrule: { byDay: ['SA'], freq: 'WEEKLY' }, }, ]); }); it('should update the start date of the original event when editing the first occurrence (DTSTART)', () => { - const original = makeRecurringEvent(); // DTSTART = 2025-01-01 + const original = createRecurringEvent(); // DTSTART = 2025-01-01 const occurrenceStart = original.start; const changes: CalendarEventUpdatedProperties = { @@ -1632,7 +1647,12 @@ describe('recurring-event-utils', () => { end: adapter.date('2025-01-12T12:00:00Z'), }; - const updatedEvents = applyRecurringUpdateAll(adapter, original, occurrenceStart, changes); + const updatedEvents = applyRecurringUpdateAll( + adapter, + original, + occurrenceStart.value, + changes, + ); expect(updatedEvents.updated).to.deep.equal([ { @@ -1645,7 +1665,7 @@ describe('recurring-event-utils', () => { describe('applyRecurringUpdateOnlyThis', () => { it('should create a detached event with exDate on the original and keep the rest intact', () => { - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = adapter.date('2025-01-05T09:00:00Z'); const changes: CalendarEventUpdatedProperties = { @@ -1679,7 +1699,7 @@ describe('recurring-event-utils', () => { it('should accumulate previous exDates', () => { const prevEx = adapter.startOfDay(adapter.date('2025-01-03T09:00:00Z')); - const original = makeRecurringEvent({ exDates: [prevEx] }); + const original = createRecurringEvent({ exDates: [prevEx] }); const occurrenceStart = adapter.date('2025-01-05T09:00:00Z'); const changes: CalendarEventUpdatedProperties = { @@ -1705,7 +1725,7 @@ describe('recurring-event-utils', () => { }); it('should use changes.start to generate the detachedId', () => { - const original = makeRecurringEvent(); + const original = createRecurringEvent(); const occurrenceStart = adapter.date('2025-01-07T09:00:00Z'); const changes: CalendarEventUpdatedProperties = { diff --git a/packages/x-scheduler-headless/src/utils/recurring-event-utils.ts b/packages/x-scheduler-headless/src/utils/recurring-event-utils.ts index c24275bd1c6d0..3a9669ec7c4c1 100644 --- a/packages/x-scheduler-headless/src/utils/recurring-event-utils.ts +++ b/packages/x-scheduler-headless/src/utils/recurring-event-utils.ts @@ -2,15 +2,17 @@ import { Adapter } from '../use-adapter/useAdapter.types'; import { RecurringEventWeekDayCode, RecurringEventByDayValue, - CalendarEvent, + SchedulerProcessedEvent, CalendarEventOccurrence, CalendarEventUpdatedProperties, RecurringEventRecurrenceRule, SchedulerValidDate, + SchedulerEvent, } from '../models'; import { mergeDateAndTime, getDateKey } from './date-utils'; import { diffIn } from '../use-adapter'; import { UpdateEventsParameters } from './SchedulerStore'; +import { processDate } from '../process-date'; /** * The week day codes for all 7 days of the week. @@ -131,7 +133,7 @@ export function parsesByDayForMonthlyFrequency(ruleByDay: RecurringEventByDayVal * Inclusive span (in days) for all-day events. * @returns At least 1, start==end yields 1. */ -export function getAllDaySpanDays(adapter: Adapter, event: CalendarEvent): number { +export function getAllDaySpanDays(adapter: Adapter, event: SchedulerProcessedEvent): number { // TODO: Now only all-day events are implemented, we should add support for timed events that span multiple days later if (!event.allDay) { return 1; @@ -139,7 +141,12 @@ export function getAllDaySpanDays(adapter: Adapter, event: CalendarEvent): numbe // +1 so start/end same day = 1 day, spans include last day return Math.max( 1, - diffIn(adapter, adapter.startOfDay(event.end), adapter.startOfDay(event.start), 'days') + 1, + diffIn( + adapter, + adapter.startOfDay(event.end.value), + adapter.startOfDay(event.start.value), + 'days', + ) + 1, ); } @@ -150,7 +157,7 @@ export function getAllDaySpanDays(adapter: Adapter, event: CalendarEvent): numbe * @returns Sorted list (by start) of concrete occurrences. */ export function getRecurringEventOccurrencesForVisibleDays( - event: CalendarEvent, + event: SchedulerProcessedEvent, start: SchedulerValidDate, end: SchedulerValidDate, adapter: Adapter, @@ -158,8 +165,8 @@ export function getRecurringEventOccurrencesForVisibleDays( const rule = event.rrule!; const occurrences: CalendarEventOccurrence[] = []; - const endGuard = buildEndGuard(rule, event.start, adapter); - const durationMinutes = diffIn(adapter, event.end, event.start, 'minutes'); + const endGuard = buildEndGuard(rule, event.start.value, adapter); + const durationMinutes = diffIn(adapter, event.end.value, event.start.value, 'minutes'); const allDaySpanDays = getAllDaySpanDays(adapter, event); const scanStart = adapter.addDays(start, -(allDaySpanDays - 1)); @@ -180,7 +187,7 @@ export function getRecurringEventOccurrencesForVisibleDays( const occurrenceStart = event.allDay ? adapter.startOfDay(day) - : mergeDateAndTime(adapter, day, event.start); + : mergeDateAndTime(adapter, day, event.start.value); const occurrenceEnd = event.allDay ? adapter.endOfDay(adapter.addDays(occurrenceStart, allDaySpanDays - 1)) @@ -195,8 +202,8 @@ export function getRecurringEventOccurrencesForVisibleDays( occurrences.push({ ...event, key, - start: occurrenceStart, - end: occurrenceEnd, + start: processDate(occurrenceStart, adapter), + end: processDate(occurrenceEnd, adapter), }); } @@ -304,10 +311,10 @@ export function matchesRecurrence( rule: RecurringEventRecurrenceRule, date: SchedulerValidDate, adapter: Adapter, - event: CalendarEvent, + event: SchedulerProcessedEvent, ): boolean { const interval = Math.max(1, rule.interval ?? 1); - const seriesStartDay = adapter.startOfDay(event.start); + const seriesStartDay = adapter.startOfDay(event.start.value); const candidateDay = adapter.startOfDay(date); if (adapter.isBefore(candidateDay, seriesStartDay)) { @@ -745,7 +752,7 @@ export function decideSplitRRule( originalRule: RecurringEventRecurrenceRule, originalSeriesStart: SchedulerValidDate, splitStart: SchedulerValidDate, - changes: Partial, + changes: Partial, ): RecurringEventRecurrenceRule | undefined { // Normalize base pattern (drop COUNT/UNTIL) const { count, until, ...baseRule } = originalRule; @@ -823,11 +830,11 @@ export function decideSplitRRule( */ export function applyRecurringUpdateFollowing( adapter: Adapter, - originalEvent: CalendarEvent, + originalEvent: SchedulerProcessedEvent, occurrenceStart: SchedulerValidDate, changes: CalendarEventUpdatedProperties, ): UpdateEventsParameters { - const newStart = changes.start ?? originalEvent.start; + const newStart = changes.start ?? originalEvent.start.value; // 1) Old series: truncate rule to end the day before the edited occurrence const occurrenceDayStart = adapter.startOfDay(occurrenceStart); @@ -840,14 +847,14 @@ export function applyRecurringUpdateFollowing( const newRRule = decideSplitRRule( adapter, originalRule, - originalEvent.start, + originalEvent.start.value, occurrenceStart, changes, ); const newEventId = `${originalEvent.id}::${getDateKey(newStart, adapter)}`; - const newEvent: CalendarEvent = { - ...originalEvent, + const newEvent: SchedulerEvent = { + ...originalEvent.modelInBuiltInFormat!, ...changes, id: newEventId, rrule: newRRule, @@ -857,7 +864,7 @@ export function applyRecurringUpdateFollowing( // 3) If UNTIL falls before DTSTART, the original series has no remaining occurrences -> drop it, otherwise truncate it. const shouldDropOldSeries = adapter.isBefore( adapter.endOfDay(untilDate), - adapter.startOfDay(originalEvent.start), + adapter.startOfDay(originalEvent.start.value), ); if (shouldDropOldSeries) { @@ -927,7 +934,7 @@ export function adjustRRuleForAllMove( */ export function applyRecurringUpdateAll( adapter: Adapter, - originalEvent: CalendarEvent, + originalEvent: SchedulerProcessedEvent, occurrenceStart: SchedulerValidDate, changes: CalendarEventUpdatedProperties, ): UpdateEventsParameters { @@ -936,14 +943,14 @@ export function applyRecurringUpdateAll( // 1) Detect if caller changed the date part of start or end (vs only time) const occurrenceEnd = adapter.addMinutes( occurrenceStart, - diffIn(adapter, originalEvent.end, originalEvent.start, 'minutes'), + diffIn(adapter, originalEvent.end.value, originalEvent.start.value, 'minutes'), ); const touchedStartDate = changes.start != null && !adapter.isSameDay(occurrenceStart, changes.start); const touchedEndDate = changes.end != null && !adapter.isSameDay(occurrenceEnd, changes.end); // 2) Is the edited occurrence the first of the series (DTSTART)? - const editedIsDtstart = adapter.isSameDay(occurrenceStart, originalEvent.start); + const editedIsDtstart = adapter.isSameDay(occurrenceStart, originalEvent.start.value); // 3) Decide new start/end if (changes.start != null) { @@ -956,13 +963,17 @@ export function applyRecurringUpdateAll( // Not first: keep original DTSTART date, merge only time eventUpdatedProperties.start = mergeDateAndTime( adapter, - originalEvent.start, + originalEvent.start.value, changes.start, ); } } else { // Same day -> merge time into original date - eventUpdatedProperties.start = mergeDateAndTime(adapter, originalEvent.start, changes.start); + eventUpdatedProperties.start = mergeDateAndTime( + adapter, + originalEvent.start.value, + changes.start, + ); } } @@ -971,10 +982,14 @@ export function applyRecurringUpdateAll( if (editedIsDtstart) { eventUpdatedProperties.end = changes.end; } else { - eventUpdatedProperties.end = mergeDateAndTime(adapter, originalEvent.end, changes.end); + eventUpdatedProperties.end = mergeDateAndTime( + adapter, + originalEvent.end.value, + changes.end, + ); } } else { - eventUpdatedProperties.end = mergeDateAndTime(adapter, originalEvent.end, changes.end); + eventUpdatedProperties.end = mergeDateAndTime(adapter, originalEvent.end.value, changes.end); } } @@ -1007,14 +1022,14 @@ export function applyRecurringUpdateAll( */ export function applyRecurringUpdateOnlyThis( adapter: Adapter, - originalEvent: CalendarEvent, + originalEvent: SchedulerProcessedEvent, occurrenceStart: SchedulerValidDate, changes: CalendarEventUpdatedProperties, ): UpdateEventsParameters { - const detachedId = `${originalEvent.id}::${getDateKey(changes.start ?? originalEvent.start, adapter)}`; + const detachedId = `${originalEvent.id}::${getDateKey(changes.start ?? originalEvent.start.value, adapter)}`; - const detachedEvent: CalendarEvent = { - ...originalEvent, + const detachedEvent: SchedulerEvent = { + ...originalEvent.modelInBuiltInFormat!, ...changes, id: detachedId, rrule: undefined, diff --git a/packages/x-scheduler-headless/src/utils/useDraggableEvent.ts b/packages/x-scheduler-headless/src/utils/useDraggableEvent.ts index c174e2b266993..06b42230d6121 100644 --- a/packages/x-scheduler-headless/src/utils/useDraggableEvent.ts +++ b/packages/x-scheduler-headless/src/utils/useDraggableEvent.ts @@ -84,8 +84,8 @@ export function useDraggableEvent( const contextValue: useDraggableEvent.ContextValue = React.useMemo( () => ({ setIsResizing, - doesEventStartBeforeCollectionStart: adapter.isBefore(start, collectionStart), - doesEventEndAfterCollectionEnd: adapter.isAfter(end, collectionEnd), + doesEventStartBeforeCollectionStart: adapter.isBefore(start.value, collectionStart), + doesEventEndAfterCollectionEnd: adapter.isAfter(end.value, collectionEnd), }), [adapter, start, end, collectionStart, collectionEnd], ); diff --git a/packages/x-scheduler-headless/src/utils/useDropTarget.ts b/packages/x-scheduler-headless/src/utils/useDropTarget.ts index 941a41e94d43f..646f42e133e93 100644 --- a/packages/x-scheduler-headless/src/utils/useDropTarget.ts +++ b/packages/x-scheduler-headless/src/utils/useDropTarget.ts @@ -2,13 +2,14 @@ import * as React from 'react'; import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { - CalendarEvent, + SchedulerProcessedEvent, CalendarOccurrencePlaceholder, CalendarOccurrencePlaceholderExternalDrag, CalendarOccurrencePlaceholderInternalDragOrResize, EventSurfaceType, CalendarEventUpdatedProperties, SchedulerValidDate, + SchedulerEvent, } from '../models'; import { EventDropData, @@ -126,7 +127,7 @@ export namespace useDropTarget { /** * Add properties to the event dropped in the element before storing it in the store. */ - addPropertiesToDroppedEvent?: () => Partial; + addPropertiesToDroppedEvent?: () => Partial; } export type CreateDropData = ( @@ -148,7 +149,7 @@ export namespace useDropTarget { async function applyInternalDragOrResizeOccurrencePlaceholder( store: SchedulerStoreInContext, placeholder: CalendarOccurrencePlaceholderInternalDragOrResize, - addPropertiesToDroppedEvent?: () => Partial, + addPropertiesToDroppedEvent?: () => Partial, ): Promise { // TODO: Try to do a single state update. store.setOccurrencePlaceholder(null); @@ -167,7 +168,7 @@ async function applyInternalDragOrResizeOccurrencePlaceholder( if (original.rrule) { store.updateRecurringEvent({ - occurrenceStart: originalOccurrence.start, + occurrenceStart: originalOccurrence.start.value, changes, }); return; @@ -179,9 +180,9 @@ async function applyInternalDragOrResizeOccurrencePlaceholder( function applyExternalDragOccurrencePlaceholder( store: SchedulerStoreInContext, placeholder: CalendarOccurrencePlaceholderExternalDrag, - addPropertiesToDroppedEvent?: () => Partial, + addPropertiesToDroppedEvent?: () => Partial, ) { - const event: CalendarEvent = { + const event: SchedulerEvent = { start: placeholder.start, end: placeholder.end, ...placeholder.eventData, diff --git a/packages/x-scheduler-headless/src/utils/useElementPositionInCollection.ts b/packages/x-scheduler-headless/src/utils/useElementPositionInCollection.ts index 9c01f98b2a71a..1f559f2380a49 100644 --- a/packages/x-scheduler-headless/src/utils/useElementPositionInCollection.ts +++ b/packages/x-scheduler-headless/src/utils/useElementPositionInCollection.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { useAdapter } from '../use-adapter/useAdapter'; -import { SchedulerValidDate } from '../models'; +import { CalendarProcessedDate, SchedulerValidDate } from '../models'; export function useElementPositionInCollection( parameters: useElementPositionInCollection.Parameters, @@ -16,8 +16,8 @@ export function useElementPositionInCollection( const collectionStartTimestamp = getTimestamp(collectionStart); const collectionEndTimestamp = getTimestamp(collectionEnd); const collectionDurationMs = collectionEndTimestamp - collectionStartTimestamp; - const startTimestamp = Math.max(getTimestamp(start), collectionStartTimestamp); - const endTimestamp = Math.min(getTimestamp(end), collectionEndTimestamp); + const startTimestamp = Math.max(start.timestamp, collectionStartTimestamp); + const endTimestamp = Math.min(end.timestamp, collectionEndTimestamp); return { position: (startTimestamp - collectionStartTimestamp) / collectionDurationMs, @@ -28,8 +28,8 @@ export function useElementPositionInCollection( namespace useElementPositionInCollection { export interface Parameters { - start: SchedulerValidDate; - end: SchedulerValidDate; + start: CalendarProcessedDate; + end: CalendarProcessedDate; collectionStart: SchedulerValidDate; collectionEnd: SchedulerValidDate; } diff --git a/packages/x-scheduler-headless/src/utils/useEvent.ts b/packages/x-scheduler-headless/src/utils/useEvent.ts index 589793583f88f..cd73a7717928e 100644 --- a/packages/x-scheduler-headless/src/utils/useEvent.ts +++ b/packages/x-scheduler-headless/src/utils/useEvent.ts @@ -1,6 +1,6 @@ 'use client'; import { useStore } from '@base-ui-components/utils/store'; -import { SchedulerValidDate } from '../models'; +import { CalendarProcessedDate } from '../models'; import { useSchedulerStoreContext } from '../use-scheduler-store-context/useSchedulerStoreContext'; import { selectors } from '../scheduler-selectors'; @@ -18,11 +18,11 @@ export namespace useEvent { /** * The time at which the event starts. */ - start: SchedulerValidDate; + start: CalendarProcessedDate; /** * The time at which the event ends. */ - end: SchedulerValidDate; + end: CalendarProcessedDate; } export interface ReturnValue { diff --git a/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx b/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx index 21e5ac77c6fa0..cc0b87a8ef21c 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/EventPopover.test.tsx @@ -2,12 +2,12 @@ import * as React from 'react'; import { spy } from 'sinon'; import { adapter, + createOccurrenceFromEvent, createSchedulerRenderer, SchedulerStoreRunner, StateWatcher, StoreSpy, } from 'test/utils/scheduler'; - import { screen } from '@mui/internal-test-utils'; import { CalendarEventOccurrence, @@ -22,15 +22,14 @@ import { EventPopoverContent } from './EventPopover'; import { getColorClassName } from '../../utils/color-utils'; import { RecurringScopeDialog } from '../scope-dialog/ScopeDialog'; -const occurrence: CalendarEventOccurrence = { +const occurrence = createOccurrenceFromEvent({ id: '1', - key: '1', start: adapter.date('2025-05-26T07:30:00'), end: adapter.date('2025-05-26T08:15:00'), title: 'Running', description: 'Morning run', resource: 'r2', -}; +}); const resources: CalendarResource[] = [ { @@ -111,8 +110,8 @@ describe('', () => { key: '1', title: 'Running test', description: 'Morning run', - start: adapter.startOfDay(occurrence.start), - end: adapter.endOfDay(occurrence.end), + start: adapter.startOfDay(occurrence.start.value), + end: adapter.endOfDay(occurrence.end.value), allDay: true, rrule: { freq: 'DAILY', interval: 1 }, resource: 'r1', @@ -289,15 +288,14 @@ describe('', () => { const end = adapter.date('2025-05-26T08:30:00'); const handleSurfaceChange = spy(); - const creationOccurrence = { + const creationOccurrence = createOccurrenceFromEvent({ id: 'tmp', - key: 'tmp', start, end, title: '', description: '', allDay: false, - }; + }); const { user } = render( @@ -336,15 +334,14 @@ describe('', () => { const end = adapter.date('2025-05-26T08:30:00'); const handleSurfaceChange = spy(); - const creationOccurrence = { + const creationOccurrence = createOccurrenceFromEvent({ id: 'tmp', - key: 'tmp', start, end, title: '', description: '', allDay: true, - }; + }); const { user } = render( @@ -435,15 +432,14 @@ describe('', () => { lockSurfaceType: false, }; - const creationOccurrence = { + const creationOccurrence = createOccurrenceFromEvent({ id: 'placeholder-id', - key: 'placeholder-key', start, end, title: '', description: '', allDay: false, - }; + }); const onEventsChange = spy(); let createEventSpy; @@ -492,16 +488,15 @@ describe('', () => { describe('Event editing', () => { describe('Recurring events', () => { it('should not call updateRecurringEvent if the user cancels the scope dialog', async () => { - const originalRecurringEvent = { + const originalRecurringEvent = createOccurrenceFromEvent({ id: 'recurring-1', - key: 'recurring-1-key', title: 'Daily standup', description: 'sync', start: adapter.date('2025-06-11T10:00:00'), end: adapter.date('2025-06-11T10:30:00'), allDay: false, rrule: { freq: 'DAILY' as const, interval: 1 }, - }; + }); let updateRecurringEventSpy, selectRecurringEventUpdateScopeSpy; const containerRef = React.createRef(); @@ -549,16 +544,15 @@ describe('', () => { }); it("should call updateRecurringEvent with scope 'all' and not include rrule if not modified on Submit", async () => { - const originalRecurringEvent = { + const originalRecurringEvent = createOccurrenceFromEvent({ id: 'recurring-1', - key: 'recurring-1-key', title: 'Daily standup', description: 'sync', start: adapter.date('2025-06-11T10:00:00'), end: adapter.date('2025-06-11T10:30:00'), allDay: false, rrule: { freq: 'DAILY' as const, interval: 1 }, - }; + }); let updateRecurringEventSpy, selectRecurringEventUpdateScopeSpy; const containerRef = React.createRef(); @@ -615,16 +609,15 @@ describe('', () => { }); it("should call updateRecurringEvent with scope 'only-this' and include rrule if modified on Submit", async () => { - const originalRecurringEvent = { + const originalRecurringEvent = createOccurrenceFromEvent({ id: 'recurring-2', - key: 'recurring-2-key', title: 'Daily standup', description: 'sync', start: adapter.date('2025-06-11T10:00:00'), end: adapter.date('2025-06-11T10:30:00'), allDay: false, rrule: { freq: 'DAILY' as const, interval: 1 }, - }; + }); let updateRecurringEventSpy, selectRecurringEventUpdateScopeSpy; const containerRef = React.createRef(); @@ -680,16 +673,15 @@ describe('', () => { }); it('should call updateRecurringEvent with scope "this-and-following" and send rrule as undefined when "no repeat" is selected on Submit', async () => { - const originalRecurringEvent = { + const originalRecurringEvent = createOccurrenceFromEvent({ id: 'recurring-3', - key: 'recurring-3-key', title: 'Daily standup', description: 'sync', start: adapter.date('2025-06-11T10:00:00'), end: adapter.date('2025-06-11T10:30:00'), allDay: false, rrule: { freq: 'DAILY' as const, interval: 1 }, - }; + }); let updateRecurringEventSpy, selectRecurringEventUpdateScopeSpy; const containerRef = React.createRef(); @@ -743,15 +735,14 @@ describe('', () => { describe('Non-recurring events', () => { it('should call updateEvent with updated values on Submit', async () => { - const nonRecurringEvent = { + const nonRecurringEvent = createOccurrenceFromEvent({ id: 'non-recurring-1', - key: 'non-recurring-1-key', title: 'Task', description: 'description', start: adapter.date('2025-06-12T14:00:00'), end: adapter.date('2025-06-12T15:00:00'), allDay: false, - }; + }); let updateEventSpy; @@ -790,14 +781,13 @@ describe('', () => { }); it('should call updateEvent with updated values and send rrule if recurrence was selected on Submit', async () => { - const nonRecurringEvent = { + const nonRecurringEvent = createOccurrenceFromEvent({ id: 'non-recurring-1', - key: 'non-recurring-1-key', title: 'Task', description: 'description', start: adapter.date('2025-06-12T14:00:00'), end: adapter.date('2025-06-12T15:00:00'), - }; + }); let updateEventSpy; diff --git a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx index 1077c27dc9761..0a71725593071 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/FormContent.tsx @@ -12,9 +12,9 @@ import { CheckIcon, ChevronDown } from 'lucide-react'; import { CalendarEventOccurrence, CalendarEventUpdatedProperties, + CalendarProcessedDate, CalendarResourceId, RecurringEventPresetKey, - SchedulerValidDate, } from '@mui/x-scheduler-headless/models'; import { useSchedulerStoreContext } from '@mui/x-scheduler-headless/use-scheduler-store-context'; import { useAdapter } from '@mui/x-scheduler-headless/use-adapter'; @@ -54,8 +54,8 @@ export default function FormContent(props: FormContentProps) { const [errors, setErrors] = React.useState({}); const [isAllDay, setIsAllDay] = React.useState(Boolean(occurrence.allDay)); const [when, setWhen] = React.useState(() => { - const fmtDate = (d: SchedulerValidDate) => adapter.formatByString(d, 'yyyy-MM-dd'); - const fmtTime = (d: SchedulerValidDate) => adapter.formatByString(d, 'HH:mm'); + const fmtDate = (d: CalendarProcessedDate) => adapter.formatByString(d.value, 'yyyy-MM-dd'); + const fmtTime = (d: CalendarProcessedDate) => adapter.formatByString(d.value, 'HH:mm'); return { startDate: fmtDate(occurrence.start), @@ -132,7 +132,13 @@ export default function FormContent(props: FormContentProps) { }; if (rawPlaceholder?.type === 'creation') { - store.createEvent({ id: crypto.randomUUID(), ...metaChanges, start, end, rrule }); + store.createEvent({ + id: crypto.randomUUID(), + ...metaChanges, + start, + end, + rrule, + }); } else if (occurrence.rrule) { const changes: CalendarEventUpdatedProperties = { ...metaChanges, @@ -143,7 +149,7 @@ export default function FormContent(props: FormContentProps) { }; await store.updateRecurringEvent({ - occurrenceStart: occurrence.start, + occurrenceStart: occurrence.start.value, changes, onSubmit: onClose, }); @@ -170,8 +176,8 @@ export default function FormContent(props: FormContentProps) { ]; }, [resources, translations.labelNoResource]); - const weekday = adapter.format(occurrence.start, 'weekday'); - const normalDate = adapter.format(occurrence.start, 'normalDate'); + const weekday = adapter.format(occurrence.start.value, 'weekday'); + const normalDate = adapter.format(occurrence.start.value, 'normalDate'); const recurrenceOptions: { label: string; @@ -184,7 +190,7 @@ export default function FormContent(props: FormContentProps) { value: 'weekly', }, { - label: `${translations.recurrenceMonthlyPresetLabel(adapter.getDate(occurrence.start))}`, + label: `${translations.recurrenceMonthlyPresetLabel(adapter.getDate(occurrence.start.value))}`, value: 'monthly', }, { diff --git a/packages/x-scheduler/src/internals/components/event-popover/ReadonlyContent.tsx b/packages/x-scheduler/src/internals/components/event-popover/ReadonlyContent.tsx index 1c236016b7f03..5ece98e752fd9 100644 --- a/packages/x-scheduler/src/internals/components/event-popover/ReadonlyContent.tsx +++ b/packages/x-scheduler/src/internals/components/event-popover/ReadonlyContent.tsx @@ -66,15 +66,18 @@ export default function ReadonlyContent(props: ReadonlyContentProps) { className={clsx('EventPopoverDateTime', 'LinesClamp')} style={{ '--number-of-lines': 1 } as React.CSSProperties} > - {isRecurring && (