From fdc1c6b6fa1f01fc9a2ef4e8992cea5e9c5e0e7a Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 19 Feb 2026 15:38:30 +0800 Subject: [PATCH 1/9] Implement feature --- .../js/__internal/scheduler/m_scheduler.ts | 4 ++ .../view_model/appointments_layout_manager.ts | 8 +++ .../view_model/filtration/get_occurrences.ts | 50 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 9d0f11dceef1..7a1fb392b307 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -2148,6 +2148,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } + getOccurrences(startDate: Date, endDate: Date, rawAppointment: Appointment[]): Appointment[] { + return this._layoutManager.getOccurrences(startDate, endDate, rawAppointment); + } + getFirstDayOfWeek(): FirstDayOfWeek { return isDefined(this.getViewOption('firstDayOfWeek')) ? this.getViewOption('firstDayOfWeek') as FirstDayOfWeek diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts index 60b063072665..33c452ed5f7d 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts @@ -2,6 +2,7 @@ import type { Appointment } from '@js/ui/scheduler'; import type Scheduler from '../m_scheduler'; import { filterAppointments } from './filtration/filter_appointments'; +import { getOccurrences } from './filtration/get_occurrences'; import { generateAgendaViewModel } from './generate_view_model/generate_agenda_view_model'; import { generateGridViewModel } from './generate_view_model/generate_grid_view_model'; import type { RealSize } from './generate_view_model/steps/add_geometry/types'; @@ -33,6 +34,13 @@ class AppointmentLayoutManager { this.filteredItems = filterAppointments(this.schedulerStore, this.preparedItems); } + public getOccurrences(startDate: Date, endDate: Date, items?: Appointment[]): Appointment[] { + const preparedItems = prepareAppointments(this.schedulerStore, items); + const occurrences = getOccurrences(this.schedulerStore, startDate, endDate, preparedItems); + + return occurrences; + } + public hasAllDayAppointments(): boolean { return this.filteredItems.filter((item: ListEntity) => item.isAllDayPanelOccupied).length > 0; } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts new file mode 100644 index 000000000000..e25b15003e27 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts @@ -0,0 +1,50 @@ +import type { Appointment } from '@js/ui/scheduler'; + +import type Scheduler from '../../m_scheduler'; +import timeZoneUtils from '../../m_utils_time_zone'; +import type { + CompareOptions, FilterOptions, MinimalAppointmentEntity, +} from '../types'; +import { addAllDayPanelOccupation } from './utils/add_all_day_panel_occupation'; +import { filterByIntervals } from './utils/filter_by_intervals/filter_by_intervals'; +import { getFilterOptions } from './utils/get_filter_options/get_filter_options'; +import { splitByRecurrence } from './utils/split_by_recurrence/split_by_recurrence'; + +export const getOccurrences = ( + schedulerStore: Scheduler, + startDate: Date, + endDate: Date, + appointments: MinimalAppointmentEntity[], +): Appointment[] => { + const compareOptions = { + startDayHour: 0, + endDayHour: 24, + min: timeZoneUtils.createUTCDateWithLocalOffset(startDate).getTime(), + max: timeZoneUtils.createUTCDateWithLocalOffset(endDate).getTime(), + skippedDays: [], + } as CompareOptions; + + const filterOptions = { + ...getFilterOptions(schedulerStore, compareOptions), + + // NOTE: to return allDay appointments if they intersect with [startDate; endDate] + allDayPanelMode: 'allDay', + supportAllDayPanel: true, + } as FilterOptions; + + const step1 = addAllDayPanelOccupation(appointments, filterOptions); + const step2 = splitByRecurrence(step1, filterOptions); + const step3 = filterByIntervals(step2, filterOptions); + + const step4 = step3.map((appointment) => { + const { startDate: sourceStartDate, endDate: sourceEndDate } = appointment.source; + const occurrence = { ...appointment.itemData }; + + schedulerStore._dataAccessors.set('startDate', occurrence, new Date(sourceStartDate)); + schedulerStore._dataAccessors.set('endDate', occurrence, new Date(sourceEndDate)); + + return occurrence; + }); + + return step4; +}; From ac54515528fb047ffff02079cd17f261521b58b4 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 19 Feb 2026 18:40:08 +0800 Subject: [PATCH 2/9] implement tests --- .../appointments_layout_manager.test.ts | 431 ++++++++++++++++++ .../view_model/filtration/get_occurrences.ts | 7 +- .../get_filter_options.test.ts | 38 +- 3 files changed, 439 insertions(+), 37 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts new file mode 100644 index 000000000000..f1c9f550c60b --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts @@ -0,0 +1,431 @@ +import { describe, expect, it } from '@jest/globals'; + +import { mockAppointmentDataAccessor } from '../__mock__/appointment_data_accessor.mock'; +import { mockTimeZoneCalculator } from '../__mock__/timezone_calculator.mock'; +import type Scheduler from '../m_scheduler'; +import { ResourceManager } from '../utils/resource_manager/resource_manager'; +import AppointmentLayoutManager from './appointments_layout_manager'; + +export const getSchedulerMock = ({ + type, + startDayHour, + endDayHour, + offsetMinutes, + resourceManager, + dateRange, + skippedDays, +}: { + type: string; + startDayHour: number; + endDayHour: number; + offsetMinutes: number; + resourceManager?: ResourceManager; + skippedDays?: number[]; + dateRange?: Date[]; +}): Scheduler => ({ + timeZoneCalculator: mockTimeZoneCalculator, + currentView: { type, skippedDays: skippedDays ?? [] }, + getWorkSpace: () => ({ + getDateRange: () => dateRange ?? [ + new Date(2000, 0, 10, startDayHour), + new Date(2000, 0, 11, endDayHour), + ], + }), + getTimeZone: () => 'Etc/UTC', + getViewOption: (name: string) => ({ + startDayHour, + endDayHour, + allDayPanelMode: 'allDay', + cellDuration: 30, + }[name]), + option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]), + getViewOffsetMs: () => offsetMinutes * 60_000, + resourceManager: resourceManager ?? new ResourceManager([]), + _dataAccessors: mockAppointmentDataAccessor, +}) as unknown as Scheduler; + +const defaultOptions = { + type: 'week', + startDayHour: 9, + endDayHour: 18, + offsetMinutes: 0, +}; + +describe('getOccurrences', () => { + describe('common appointments', () => { + it('should return occurrence', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should return occurrences with extra field', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + customField: 'custom value', + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should return occurrences if appointment intersects partially', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T18:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T12:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should return occurrences if interval is in appointment', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T18:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T07:00:00Z'), + new Date('2000-01-01T12:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should not return occurrences out of interval - 1', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-03T06:00:00Z'), + endDate: new Date('2000-01-03T07:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([]); + }); + + it('should not return occurrences out of interval - 2', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-02T00:01:00Z'), + endDate: new Date('2000-01-02T01:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([]); + }); + + it('should not return occurrences out of interval - 3', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T23:00:00Z'), + endDate: new Date('2000-01-01T23:59:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-02T00:00:00Z'), + new Date('2000-01-03T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([]); + }); + + it('should return occurrences if scheduler has skipped days, and appointment intersects with them', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock({ + ...defaultOptions, + skippedDays: [0, 6], + })); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-03T00:00:00Z'), + [appointment], + ); + + expect(new Date('2000-01-01T00:00:00Z').getDay()).toBe(6); // weekend + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should return occurrences out of scheduler day hours', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock({ + ...defaultOptions, + startDayHour: 8, + endDayHour: 17, + })); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T02:00:00Z'), + endDate: new Date('2000-01-01T03:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should return occurrences out of scheduler visible dates', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock({ + ...defaultOptions, + dateRange: [ + new Date(2000, 0, 10, 9), + new Date(2000, 0, 16, 18), + ], + })); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should return occurrence if appointment has visible=false', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + visible: false, + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + }); + + describe('recurring appointments', () => { + it('should return occurrences', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-07T00:00:00Z'), + [{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + }], + ); + + expect(occurrences).toEqual([{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + }, { + text: 'test', + startDate: new Date('2000-01-05T06:00:00Z'), + endDate: new Date('2000-01-05T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + }]); + }); + + it('should return occurrences with extra fields', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-07T00:00:00Z'), + [{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + customField: 'custom value', + }], + ); + + expect(occurrences).toEqual([{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + customField: 'custom value', + }, { + text: 'test', + startDate: new Date('2000-01-05T06:00:00Z'), + endDate: new Date('2000-01-05T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + customField: 'custom value', + }]); + }); + + it('should return occurrences with exceptions', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-06T00:00:00Z'), + [{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + recurrenceException: '20000105T060000Z', + }], + ); + + expect(occurrences).toEqual([{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + recurrenceException: '20000105T060000Z', + }, { + text: 'test', + startDate: new Date('2000-01-03T06:00:00Z'), + endDate: new Date('2000-01-03T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + recurrenceException: '20000105T060000Z', + }]); + }); + + it('should return occurrences out of recurring appointment with startDate out of interval', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-05T00:00:00Z'), + new Date('2000-01-08T00:00:00Z'), + [{ + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + }], + ); + + expect(occurrences).toEqual([{ + text: 'test', + startDate: new Date('2000-01-05T06:00:00Z'), + endDate: new Date('2000-01-05T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + }, { + text: 'test', + startDate: new Date('2000-01-07T06:00:00Z'), + endDate: new Date('2000-01-07T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + }]); + }); + }); + + describe('all day appointments', () => { + it('should return occurrences if all day appointment\' date intersects with the given interval', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T01:00:00Z'), + endDate: new Date('2000-01-01T02:00:00Z'), + allDay: true, + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T12:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([{ ...appointment }]); + }); + + it('should not return occurrences if appointment is not in interval - 1', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T01:00:00Z'), + endDate: new Date('2000-01-01T02:00:00Z'), + allDay: true, + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-02T00:00:00Z'), + new Date('2000-01-03T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([]); + }); + + it('should not return occurrences if appointment is not in interval - 2', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test', + startDate: new Date('2000-03-01T01:00:00Z'), + endDate: new Date('2000-03-01T02:00:00Z'), + allDay: true, + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-02T00:00:00Z'), + [appointment], + ); + + expect(occurrences).toEqual([]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts index e25b15003e27..c5c83f325eda 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts @@ -1,7 +1,6 @@ import type { Appointment } from '@js/ui/scheduler'; import type Scheduler from '../../m_scheduler'; -import timeZoneUtils from '../../m_utils_time_zone'; import type { CompareOptions, FilterOptions, MinimalAppointmentEntity, } from '../types'; @@ -19,17 +18,17 @@ export const getOccurrences = ( const compareOptions = { startDayHour: 0, endDayHour: 24, - min: timeZoneUtils.createUTCDateWithLocalOffset(startDate).getTime(), - max: timeZoneUtils.createUTCDateWithLocalOffset(endDate).getTime(), + min: startDate.getTime(), + max: endDate.getTime(), skippedDays: [], } as CompareOptions; const filterOptions = { ...getFilterOptions(schedulerStore, compareOptions), - // NOTE: to return allDay appointments if they intersect with [startDate; endDate] allDayPanelMode: 'allDay', supportAllDayPanel: true, + isDateTimeView: true, } as FilterOptions; const step1 = addAllDayPanelOccupation(appointments, filterOptions); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts index b3f99c056691..af3aeae35da4 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts @@ -1,41 +1,9 @@ import { describe, expect, it } from '@jest/globals'; +import { getSchedulerMock } from '@ts/scheduler/view_model/appointments_layout_manager.test'; import { getCompareOptions } from '@ts/scheduler/view_model/common/get_compare_options'; -import { mockAppointmentDataAccessor } from '../../../../__mock__/appointment_data_accessor.mock'; -import type Scheduler from '../../../../m_scheduler'; -import { ResourceManager } from '../../../../utils/resource_manager/resource_manager'; import { getFilterOptions } from './get_filter_options'; -export const getSchedulerMock = ({ - type, - startDayHour, - endDayHour, - offsetMinutes, - resourceManager, - dateRange, -}: { - type: string; - startDayHour: number; - endDayHour: number; - offsetMinutes: number; - resourceManager?: ResourceManager; - dateRange?: Date[]; -}): Scheduler => ({ - currentView: { type, skippedDays: [] }, - getWorkSpace: () => ({ - getDateRange: () => dateRange ?? [ - new Date(2000, 0, 10, startDayHour), - new Date(2000, 0, 11, endDayHour), - ], - }), - getTimeZone: () => 'Etc/UTC', - getViewOption: (name: string) => ({ startDayHour, endDayHour, allDayPanelMode: 'allDay' }[name]), - option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]), - getViewOffsetMs: () => offsetMinutes * 60_000, - resourceManager: resourceManager ?? new ResourceManager([]), - _dataAccessors: mockAppointmentDataAccessor, -}) as unknown as Scheduler; - describe('getFilterOptions', () => { ['agenda', 'month'].forEach((type) => { it(`should return correct filter options for ${type} view`, () => { @@ -44,6 +12,10 @@ describe('getFilterOptions', () => { startDayHour: 0, endDayHour: 24, offsetMinutes: 30, + dateRange: [ + new Date(2000, 0, 10, 0), + new Date(2000, 0, 11, 24), + ], }); const compareOptions = getCompareOptions(schedulerStore); From f48889387f6b892bedf0b071f2283c7701753ed7 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 19 Feb 2026 20:54:08 +0800 Subject: [PATCH 3/9] fix test and add another --- .../appointments_layout_manager.test.ts | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts index f1c9f550c60b..8e99263c1889 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts @@ -190,7 +190,7 @@ describe('getOccurrences', () => { [appointment], ); - expect(new Date('2000-01-01T00:00:00Z').getDay()).toBe(6); // weekend + expect(new Date('2000-01-01T06:00:00Z').getDay()).toBe(6); // weekend expect(occurrences).toEqual([{ ...appointment }]); }); @@ -428,4 +428,41 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([]); }); }); + + it('should return occurrences for common and recurring appointments together', async () => { + const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + + const appointment = { + text: 'test 1', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + }; + const recurringAppointment = { + text: 'test 2', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=3', + }; + const occurrences = layoutManager.getOccurrences( + new Date('2000-01-01T00:00:00Z'), + new Date('2000-01-06T00:00:00Z'), + [appointment, recurringAppointment], + ); + + expect(occurrences).toEqual([{ + text: 'test 1', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + }, { + text: 'test 2', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=3', + }, { + text: 'test 2', + startDate: new Date('2000-01-04T06:00:00Z'), + endDate: new Date('2000-01-04T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=3', + }]); + }); }); From bcb2bfadb9115b64916803875057be3aca7645c3 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 19 Feb 2026 21:20:18 +0800 Subject: [PATCH 4/9] apply copilot's review --- .../js/__internal/scheduler/m_scheduler.ts | 4 ++-- .../view_model/appointments_layout_manager.ts | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 7a1fb392b307..17f18fa4bb09 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -2148,8 +2148,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - getOccurrences(startDate: Date, endDate: Date, rawAppointment: Appointment[]): Appointment[] { - return this._layoutManager.getOccurrences(startDate, endDate, rawAppointment); + getOccurrences(startDate: Date, endDate: Date, rawAppointments: Appointment[]): Appointment[] { + return this._layoutManager.getOccurrences(startDate, endDate, rawAppointments); } getFirstDayOfWeek(): FirstDayOfWeek { diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts index 33c452ed5f7d..ff6267315e94 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts @@ -34,9 +34,18 @@ class AppointmentLayoutManager { this.filteredItems = filterAppointments(this.schedulerStore, this.preparedItems); } - public getOccurrences(startDate: Date, endDate: Date, items?: Appointment[]): Appointment[] { - const preparedItems = prepareAppointments(this.schedulerStore, items); - const occurrences = getOccurrences(this.schedulerStore, startDate, endDate, preparedItems); + public getOccurrences( + startDate: Date, + endDate: Date, + rawAppointments?: Appointment[], + ): Appointment[] { + const preparedAppointments = prepareAppointments(this.schedulerStore, rawAppointments); + const occurrences = getOccurrences( + this.schedulerStore, + startDate, + endDate, + preparedAppointments, + ); return occurrences; } From 84f2e6ca9030d008189f32bfbafc57957e56935a Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 20 Feb 2026 17:15:25 +0800 Subject: [PATCH 5/9] apply sergei's review --- .../scheduler/view_model/appointments_layout_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts index ff6267315e94..e92aade3da55 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts @@ -37,7 +37,7 @@ class AppointmentLayoutManager { public getOccurrences( startDate: Date, endDate: Date, - rawAppointments?: Appointment[], + rawAppointments: Appointment[], ): Appointment[] { const preparedAppointments = prepareAppointments(this.schedulerStore, rawAppointments); const occurrences = getOccurrences( From 5dfc2defcf0c89100b22639d818e7d6fa5e9e43f Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 20 Feb 2026 20:13:00 +0800 Subject: [PATCH 6/9] apply aleksei's review --- .../view_model/__mock__/scheduler.mock.ts | 42 ++++++++++ .../appointments_layout_manager.test.ts | 81 +++++-------------- .../view_model/filtration/get_occurrences.ts | 24 +++--- .../get_filter_options.test.ts | 2 +- 4 files changed, 75 insertions(+), 74 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts diff --git a/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts b/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts new file mode 100644 index 000000000000..729bace90338 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts @@ -0,0 +1,42 @@ +import { mockAppointmentDataAccessor } from '../../__mock__/appointment_data_accessor.mock'; +import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock'; +import type Scheduler from '../../m_scheduler'; +import { ResourceManager } from '../../utils/resource_manager/resource_manager'; + +export const getSchedulerMock = ({ + type, + startDayHour, + endDayHour, + offsetMinutes, + resourceManager, + dateRange, + skippedDays, +}: { + type: string; + startDayHour: number; + endDayHour: number; + offsetMinutes: number; + resourceManager?: ResourceManager; + skippedDays?: number[]; + dateRange?: Date[]; +}): Scheduler => ({ + timeZoneCalculator: mockTimeZoneCalculator, + currentView: { type, skippedDays: skippedDays ?? [] }, + getWorkSpace: () => ({ + getDateRange: () => dateRange ?? [ + new Date(2000, 0, 10, startDayHour), + new Date(2000, 0, 11, endDayHour), + ], + }), + getTimeZone: () => 'Etc/UTC', + getViewOption: (name: string) => ({ + startDayHour, + endDayHour, + allDayPanelMode: 'allDay', + cellDuration: 30, + }[name]), + option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]), + getViewOffsetMs: () => offsetMinutes * 60_000, + resourceManager: resourceManager ?? new ResourceManager([]), + _dataAccessors: mockAppointmentDataAccessor, +}) as unknown as Scheduler; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts index 8e99263c1889..d4a85265c1e9 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts @@ -1,49 +1,8 @@ import { describe, expect, it } from '@jest/globals'; -import { mockAppointmentDataAccessor } from '../__mock__/appointment_data_accessor.mock'; -import { mockTimeZoneCalculator } from '../__mock__/timezone_calculator.mock'; -import type Scheduler from '../m_scheduler'; -import { ResourceManager } from '../utils/resource_manager/resource_manager'; +import { getSchedulerMock } from './__mock__/scheduler.mock'; import AppointmentLayoutManager from './appointments_layout_manager'; -export const getSchedulerMock = ({ - type, - startDayHour, - endDayHour, - offsetMinutes, - resourceManager, - dateRange, - skippedDays, -}: { - type: string; - startDayHour: number; - endDayHour: number; - offsetMinutes: number; - resourceManager?: ResourceManager; - skippedDays?: number[]; - dateRange?: Date[]; -}): Scheduler => ({ - timeZoneCalculator: mockTimeZoneCalculator, - currentView: { type, skippedDays: skippedDays ?? [] }, - getWorkSpace: () => ({ - getDateRange: () => dateRange ?? [ - new Date(2000, 0, 10, startDayHour), - new Date(2000, 0, 11, endDayHour), - ], - }), - getTimeZone: () => 'Etc/UTC', - getViewOption: (name: string) => ({ - startDayHour, - endDayHour, - allDayPanelMode: 'allDay', - cellDuration: 30, - }[name]), - option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]), - getViewOffsetMs: () => offsetMinutes * 60_000, - resourceManager: resourceManager ?? new ResourceManager([]), - _dataAccessors: mockAppointmentDataAccessor, -}) as unknown as Scheduler; - const defaultOptions = { type: 'week', startDayHour: 9, @@ -53,7 +12,7 @@ const defaultOptions = { describe('getOccurrences', () => { describe('common appointments', () => { - it('should return occurrence', async () => { + it('should return occurrence', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -70,7 +29,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should return occurrences with extra field', async () => { + it('should return occurrences with extra field', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -88,7 +47,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should return occurrences if appointment intersects partially', async () => { + it('should return occurrences if appointment intersects partially', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -105,7 +64,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should return occurrences if interval is in appointment', async () => { + it('should return occurrences if interval is in appointment', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -122,7 +81,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should not return occurrences out of interval - 1', async () => { + it('should not return occurrences out of interval - 1', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -139,7 +98,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([]); }); - it('should not return occurrences out of interval - 2', async () => { + it('should not return occurrences out of interval - 2', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -156,7 +115,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([]); }); - it('should not return occurrences out of interval - 3', async () => { + it('should not return occurrences out of interval - 3', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -173,7 +132,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([]); }); - it('should return occurrences if scheduler has skipped days, and appointment intersects with them', async () => { + it('should return occurrences if scheduler has skipped days, and appointment intersects with them', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock({ ...defaultOptions, skippedDays: [0, 6], @@ -194,7 +153,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should return occurrences out of scheduler day hours', async () => { + it('should return occurrences out of scheduler day hours', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock({ ...defaultOptions, startDayHour: 8, @@ -215,7 +174,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should return occurrences out of scheduler visible dates', async () => { + it('should return occurrences out of scheduler visible dates', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock({ ...defaultOptions, dateRange: [ @@ -238,7 +197,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should return occurrence if appointment has visible=false', async () => { + it('should return occurrence if appointment has visible=false', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -258,7 +217,7 @@ describe('getOccurrences', () => { }); describe('recurring appointments', () => { - it('should return occurrences', async () => { + it('should return occurrences', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const occurrences = layoutManager.getOccurrences( @@ -285,7 +244,7 @@ describe('getOccurrences', () => { }]); }); - it('should return occurrences with extra fields', async () => { + it('should return occurrences with extra fields', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const occurrences = layoutManager.getOccurrences( @@ -315,7 +274,7 @@ describe('getOccurrences', () => { }]); }); - it('should return occurrences with exceptions', async () => { + it('should return occurrences with exceptions', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const occurrences = layoutManager.getOccurrences( @@ -345,7 +304,7 @@ describe('getOccurrences', () => { }]); }); - it('should return occurrences out of recurring appointment with startDate out of interval', async () => { + it('should return occurrences out of recurring appointment with startDate out of interval', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const occurrences = layoutManager.getOccurrences( @@ -374,7 +333,7 @@ describe('getOccurrences', () => { }); describe('all day appointments', () => { - it('should return occurrences if all day appointment\' date intersects with the given interval', async () => { + it('should return occurrences if all day appointment\' date intersects with the given interval', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -392,7 +351,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([{ ...appointment }]); }); - it('should not return occurrences if appointment is not in interval - 1', async () => { + it('should not return occurrences if appointment is not in interval - 1', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -410,7 +369,7 @@ describe('getOccurrences', () => { expect(occurrences).toEqual([]); }); - it('should not return occurrences if appointment is not in interval - 2', async () => { + it('should not return occurrences if appointment is not in interval - 2', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { @@ -429,7 +388,7 @@ describe('getOccurrences', () => { }); }); - it('should return occurrences for common and recurring appointments together', async () => { + it('should return occurrences for common and recurring appointments together', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); const appointment = { diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts index c5c83f325eda..725a6f29fbe6 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts @@ -1,5 +1,3 @@ -import type { Appointment } from '@js/ui/scheduler'; - import type Scheduler from '../../m_scheduler'; import type { CompareOptions, FilterOptions, MinimalAppointmentEntity, @@ -9,12 +7,18 @@ import { filterByIntervals } from './utils/filter_by_intervals/filter_by_interva import { getFilterOptions } from './utils/get_filter_options/get_filter_options'; import { splitByRecurrence } from './utils/split_by_recurrence/split_by_recurrence'; +export interface Occurrence { + startDate: Date; + endDate: Date; + appointmentData: object; +} + export const getOccurrences = ( schedulerStore: Scheduler, startDate: Date, endDate: Date, appointments: MinimalAppointmentEntity[], -): Appointment[] => { +): Occurrence[] => { const compareOptions = { startDayHour: 0, endDayHour: 24, @@ -35,15 +39,11 @@ export const getOccurrences = ( const step2 = splitByRecurrence(step1, filterOptions); const step3 = filterByIntervals(step2, filterOptions); - const step4 = step3.map((appointment) => { - const { startDate: sourceStartDate, endDate: sourceEndDate } = appointment.source; - const occurrence = { ...appointment.itemData }; - - schedulerStore._dataAccessors.set('startDate', occurrence, new Date(sourceStartDate)); - schedulerStore._dataAccessors.set('endDate', occurrence, new Date(sourceEndDate)); - - return occurrence; - }); + const step4 = step3.map((appointment) => ({ + startDate: new Date(appointment.source.startDate), + endDate: new Date(appointment.source.endDate), + appointmentData: appointment.itemData, + } as Occurrence)); return step4; }; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts index af3aeae35da4..43a7d4160d7b 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/utils/get_filter_options/get_filter_options.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { getSchedulerMock } from '@ts/scheduler/view_model/appointments_layout_manager.test'; +import { getSchedulerMock } from '@ts/scheduler/view_model/__mock__/scheduler.mock'; import { getCompareOptions } from '@ts/scheduler/view_model/common/get_compare_options'; import { getFilterOptions } from './get_filter_options'; From 89c88ba544a6bca851400a4b525a89c2c63b753e Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 20 Feb 2026 21:00:57 +0800 Subject: [PATCH 7/9] fix jest tests --- .../appointments_layout_manager.test.ts | 146 +++++++++++------- 1 file changed, 86 insertions(+), 60 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts index d4a85265c1e9..e0a869423564 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.test.ts @@ -26,7 +26,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should return occurrences with extra field', () => { @@ -44,7 +48,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should return occurrences if appointment intersects partially', () => { @@ -61,7 +69,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should return occurrences if interval is in appointment', () => { @@ -78,7 +90,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should not return occurrences out of interval - 1', () => { @@ -150,7 +166,11 @@ describe('getOccurrences', () => { ); expect(new Date('2000-01-01T06:00:00Z').getDay()).toBe(6); // weekend - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should return occurrences out of scheduler day hours', () => { @@ -171,7 +191,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should return occurrences out of scheduler visible dates', () => { @@ -194,7 +218,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should return occurrence if appointment has visible=false', () => { @@ -212,7 +240,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); }); @@ -220,114 +252,106 @@ describe('getOccurrences', () => { it('should return occurrences', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + }; const occurrences = layoutManager.getOccurrences( new Date('2000-01-01T00:00:00Z'), new Date('2000-01-07T00:00:00Z'), - [{ - text: 'test', - startDate: new Date('2000-01-01T06:00:00Z'), - endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=4', - }], + [appointment], ); expect(occurrences).toEqual([{ - text: 'test', startDate: new Date('2000-01-01T06:00:00Z'), endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + appointmentData: { ...appointment }, }, { - text: 'test', startDate: new Date('2000-01-05T06:00:00Z'), endDate: new Date('2000-01-05T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + appointmentData: { ...appointment }, }]); }); it('should return occurrences with extra fields', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=4', + customField: 'custom value', + }; const occurrences = layoutManager.getOccurrences( new Date('2000-01-01T00:00:00Z'), new Date('2000-01-07T00:00:00Z'), - [{ - text: 'test', - startDate: new Date('2000-01-01T06:00:00Z'), - endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=4', - customField: 'custom value', - }], + [appointment], ); expect(occurrences).toEqual([{ - text: 'test', startDate: new Date('2000-01-01T06:00:00Z'), endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=4', - customField: 'custom value', + appointmentData: { ...appointment }, }, { - text: 'test', startDate: new Date('2000-01-05T06:00:00Z'), endDate: new Date('2000-01-05T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=4', - customField: 'custom value', + appointmentData: { ...appointment }, }]); }); it('should return occurrences with exceptions', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + recurrenceException: '20000105T060000Z', + }; const occurrences = layoutManager.getOccurrences( new Date('2000-01-01T00:00:00Z'), new Date('2000-01-06T00:00:00Z'), - [{ - text: 'test', - startDate: new Date('2000-01-01T06:00:00Z'), - endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', - recurrenceException: '20000105T060000Z', - }], + [appointment], ); expect(occurrences).toEqual([{ - text: 'test', startDate: new Date('2000-01-01T06:00:00Z'), endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', - recurrenceException: '20000105T060000Z', + appointmentData: { ...appointment }, }, { - text: 'test', startDate: new Date('2000-01-03T06:00:00Z'), endDate: new Date('2000-01-03T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', - recurrenceException: '20000105T060000Z', + appointmentData: { ...appointment }, }]); }); it('should return occurrences out of recurring appointment with startDate out of interval', () => { const layoutManager = new AppointmentLayoutManager(getSchedulerMock(defaultOptions)); + const appointment = { + text: 'test', + startDate: new Date('2000-01-01T06:00:00Z'), + endDate: new Date('2000-01-01T07:00:00Z'), + recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + }; const occurrences = layoutManager.getOccurrences( new Date('2000-01-05T00:00:00Z'), new Date('2000-01-08T00:00:00Z'), - [{ - text: 'test', - startDate: new Date('2000-01-01T06:00:00Z'), - endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', - }], + [appointment], ); expect(occurrences).toEqual([{ - text: 'test', startDate: new Date('2000-01-05T06:00:00Z'), endDate: new Date('2000-01-05T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + appointmentData: { ...appointment }, }, { - text: 'test', startDate: new Date('2000-01-07T06:00:00Z'), endDate: new Date('2000-01-07T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=2', + appointmentData: { ...appointment }, }]); }); }); @@ -348,7 +372,11 @@ describe('getOccurrences', () => { [appointment], ); - expect(occurrences).toEqual([{ ...appointment }]); + expect(occurrences).toEqual([{ + startDate: appointment.startDate, + endDate: appointment.endDate, + appointmentData: { ...appointment }, + }]); }); it('should not return occurrences if appointment is not in interval - 1', () => { @@ -409,19 +437,17 @@ describe('getOccurrences', () => { ); expect(occurrences).toEqual([{ - text: 'test 1', startDate: new Date('2000-01-01T06:00:00Z'), endDate: new Date('2000-01-01T07:00:00Z'), + appointmentData: { ...appointment }, }, { - text: 'test 2', startDate: new Date('2000-01-01T06:00:00Z'), endDate: new Date('2000-01-01T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=3', + appointmentData: { ...recurringAppointment }, }, { - text: 'test 2', startDate: new Date('2000-01-04T06:00:00Z'), endDate: new Date('2000-01-04T07:00:00Z'), - recurrenceRule: 'FREQ=DAILY;INTERVAL=3', + appointmentData: { ...recurringAppointment }, }]); }); }); From 9c71b07184df7556a90d4ad09fba5d301536ddae Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 20 Feb 2026 21:15:57 +0800 Subject: [PATCH 8/9] apply copilot's review --- .../js/__internal/scheduler/m_scheduler.ts | 3 ++- .../view_model/filtration/get_occurrences.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 17f18fa4bb09..73054577d215 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -78,6 +78,7 @@ import { setAppointmentGroupValues } from './utils/resource_manager/appointment_ import { createResourceEditorModel } from './utils/resource_manager/popup_utils'; import { ResourceManager } from './utils/resource_manager/resource_manager'; import AppointmentLayoutManager from './view_model/appointments_layout_manager'; +import type { Occurrence } from './view_model/filtration/get_occurrences'; import { AppointmentDataSource } from './view_model/m_appointment_data_source'; import type { AppointmentViewModelPlain } from './view_model/types'; import SchedulerAgenda from './workspaces/m_agenda'; @@ -2148,7 +2149,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - getOccurrences(startDate: Date, endDate: Date, rawAppointments: Appointment[]): Appointment[] { + getOccurrences(startDate: Date, endDate: Date, rawAppointments: Appointment[]): Occurrence[] { return this._layoutManager.getOccurrences(startDate, endDate, rawAppointments); } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts index 725a6f29fbe6..551071fc13c6 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/filtration/get_occurrences.ts @@ -19,31 +19,33 @@ export const getOccurrences = ( endDate: Date, appointments: MinimalAppointmentEntity[], ): Occurrence[] => { - const compareOptions = { + const compareOptions: CompareOptions = { + // NOTE: days hours are intentionally set to 0 and 24 + // to return all appointments that intersect with [startDate; endDate] startDayHour: 0, endDayHour: 24, min: startDate.getTime(), max: endDate.getTime(), skippedDays: [], - } as CompareOptions; + }; - const filterOptions = { + const filterOptions: FilterOptions = { ...getFilterOptions(schedulerStore, compareOptions), // NOTE: to return allDay appointments if they intersect with [startDate; endDate] allDayPanelMode: 'allDay', supportAllDayPanel: true, isDateTimeView: true, - } as FilterOptions; + }; const step1 = addAllDayPanelOccupation(appointments, filterOptions); const step2 = splitByRecurrence(step1, filterOptions); const step3 = filterByIntervals(step2, filterOptions); - const step4 = step3.map((appointment) => ({ + const step4: Occurrence[] = step3.map((appointment) => ({ startDate: new Date(appointment.source.startDate), endDate: new Date(appointment.source.endDate), appointmentData: appointment.itemData, - } as Occurrence)); + })); return step4; }; From c8d973fd2157c471bb663ab091e05444d405b290 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Fri, 20 Feb 2026 21:38:07 +0800 Subject: [PATCH 9/9] fix ts error --- .../scheduler/view_model/appointments_layout_manager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts index e92aade3da55..6ea190eaa0dc 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts @@ -2,6 +2,7 @@ import type { Appointment } from '@js/ui/scheduler'; import type Scheduler from '../m_scheduler'; import { filterAppointments } from './filtration/filter_appointments'; +import type { Occurrence } from './filtration/get_occurrences'; import { getOccurrences } from './filtration/get_occurrences'; import { generateAgendaViewModel } from './generate_view_model/generate_agenda_view_model'; import { generateGridViewModel } from './generate_view_model/generate_grid_view_model'; @@ -38,7 +39,7 @@ class AppointmentLayoutManager { startDate: Date, endDate: Date, rawAppointments: Appointment[], - ): Appointment[] { + ): Occurrence[] { const preparedAppointments = prepareAppointments(this.schedulerStore, rawAppointments); const occurrences = getOccurrences( this.schedulerStore,