From 0016a3236752e0f42f8d2cdc5bfdd5334b900f00 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Tue, 7 Apr 2026 12:02:56 +0000 Subject: [PATCH 01/30] =?UTF-8?q?Scheduler=20=E2=80=94=20Add=20hiddenDays?= =?UTF-8?q?=20option=20to=20hide=20arbitrary=20days=20of=20the=20week?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__internal/scheduler/header/m_header.ts | 1 + .../js/__internal/scheduler/header/m_utils.ts | 61 +++++- .../js/__internal/scheduler/header/types.ts | 1 + .../js/__internal/scheduler/m_scheduler.ts | 3 + .../scheduler_options_base_widget.ts | 6 +- .../scheduler/utils/options/constants.ts | 2 + .../scheduler/utils/options/utils.test.ts | 99 ++++++++- .../scheduler/utils/options/utils.ts | 91 +++++++- .../scheduler/workspaces/m_work_space.ts | 1 + .../view_model/m_view_data_generator.test.ts | 195 ++++++++++++++++++ .../view_model/m_view_data_generator.ts | 82 +++++++- .../view_model/m_view_data_generator_month.ts | 6 +- .../view_model/m_view_data_generator_week.ts | 2 +- .../m_view_data_generator_work_week.ts | 8 +- packages/devextreme/js/ui/widget/ui.errors.js | 4 + packages/devextreme/ts/dx.all.d.ts | 8 + 16 files changed, 540 insertions(+), 30 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/header/m_header.ts b/packages/devextreme/js/__internal/scheduler/header/m_header.ts index b9b5b19e14fe..2203fbfe7bdf 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_header.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_header.ts @@ -66,6 +66,7 @@ export class SchedulerHeader extends Widget { firstDayOfWeek, intervalCount: currentView.intervalCount, agendaDuration: currentView.agendaDuration, + skippedDays: (currentView as { skippedDays?: number[] }).skippedDays, }; } diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index f5d981394439..16798ede4474 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -48,6 +48,8 @@ const nextMonth = (date: Date): Date => { const isWeekend = (date: Date): boolean => [SATURDAY_INDEX, SUNDAY_INDEX].includes(date.getDay()); +const isSkippedDay = (date: Date, skippedDays: number[]): boolean => skippedDays.includes(date.getDay()); + const getWorkWeekStart = (firstDayOfWeek: Date): Date => { let date = new Date(firstDayOfWeek); while (isWeekend(date)) { @@ -57,6 +59,14 @@ const getWorkWeekStart = (firstDayOfWeek: Date): Date => { return date; }; +const getFirstVisibleDay = (start: Date, skippedDays: number[]): Date => { + let date = new Date(start); + while (isSkippedDay(date, skippedDays)) { + date = nextDay(date); + } + return date; +}; + const getDateAfterWorkWeek = (workWeekStart: Date): Date => { let date = new Date(workWeekStart); @@ -72,19 +82,43 @@ const getDateAfterWorkWeek = (workWeekStart: Date): Date => { return date; }; +const getDateAfterVisibleWeek = (start: Date, skippedDays: number[]): Date => { + const visibleCount = 7 - skippedDays.length; + if (visibleCount <= 0) { + return new Date(start); + } + let date = new Date(start); + let visited = 0; + while (visited < visibleCount) { + if (!isSkippedDay(date, skippedDays)) { + visited += 1; + } + date = nextDay(date); + } + return date; +}; + const nextAgendaStart = ( date: Date, agendaDuration: number, ): Date => addDateInterval(date, { days: agendaDuration }, 1); const getIntervalStartDate = (options: IntervalOptions): Date => { - const { date, step, firstDayOfWeek } = options; + const { + date, step, firstDayOfWeek, skippedDays, + } = options; switch (step) { case 'day': - case 'week': case 'month': return getPeriodStart(date, step, false, firstDayOfWeek) as Date; + case 'week': { + const weekStart = getPeriodStart(date, step, false, firstDayOfWeek) as Date; + if (skippedDays && skippedDays.length > 0) { + return getFirstVisibleDay(weekStart, skippedDays); + } + return weekStart; + } case 'workWeek': return getWorkWeekStart(getWeekStart(date, firstDayOfWeek)); case 'agenda': @@ -98,10 +132,13 @@ const getPeriodEndDate = ( currentPeriodStartDate: Date, step: Step, agendaDuration: number, + skippedDays?: number[], ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), - week: () => nextWeek(currentPeriodStartDate), + week: () => (skippedDays && skippedDays.length > 0 + ? getDateAfterVisibleWeek(currentPeriodStartDate, skippedDays) + : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate), agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), @@ -110,20 +147,30 @@ const getPeriodEndDate = ( return subMS(calculators[step]()); }; -const getNextPeriodStartDate = (currentPeriodEndDate: Date, step: Step): Date => { +const getNextPeriodStartDate = ( + currentPeriodEndDate: Date, + step: Step, + skippedDays?: number[], +): Date => { let date = addMS(currentPeriodEndDate); if (step === 'workWeek') { while (isWeekend(date)) { date = nextDay(date); } + } else if (step === 'week' && skippedDays && skippedDays.length > 0) { + while (isSkippedDay(date, skippedDays)) { + date = nextDay(date); + } } return date; }; const getIntervalEndDate = (startDate: Date, options: IntervalOptions): Date => { - const { intervalCount, step, agendaDuration } = options; + const { + intervalCount, step, agendaDuration, skippedDays, + } = options; let periodStartDate = new Date(startDate); let periodEndDate = new Date(startDate); @@ -132,9 +179,9 @@ const getIntervalEndDate = (startDate: Date, options: IntervalOptions): Date => for (let i = 0; i < intervalCount; i += 1) { periodStartDate = nextPeriodStartDate; - periodEndDate = getPeriodEndDate(periodStartDate, step, agendaDuration ?? 0); + periodEndDate = getPeriodEndDate(periodStartDate, step, agendaDuration ?? 0, skippedDays); - nextPeriodStartDate = getNextPeriodStartDate(periodEndDate, step); + nextPeriodStartDate = getNextPeriodStartDate(periodEndDate, step, skippedDays); } return periodEndDate; diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 9fe0089ae77e..181e6ded7193 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -30,6 +30,7 @@ export interface IntervalOptions { firstDayOfWeek?: number; intervalCount: number; agendaDuration?: number; + skippedDays?: number[]; } export interface HeaderCalendarOptions { diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index b04bf4ca533c..c9c7131060fc 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -314,6 +314,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateOption('header', 'views', this.views); } break; + case 'hiddenDays': + this.repaint(); + break; case 'useDropDownViewSwitcher': this.updateOption('header', name, value); break; diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 23883fcf5d21..0d5a2de17e9b 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -50,10 +50,13 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - this.views = getViews(views); + const hiddenDays = this.option('hiddenDays' as keyof SafeSchedulerOptions) as + number[] | undefined; + this.views = getViews(views, hiddenDays); this.currentView = getCurrentView( this.option('currentView') ?? '', views, + hiddenDays, ); } @@ -71,6 +74,7 @@ export class SchedulerOptionsBaseWidget extends Widget { switch (args.name) { case 'currentView': case 'views': + case 'hiddenDays' as K: this.updateViews(); break; default: diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index b77adcfbdb06..5bc17302fd53 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -54,6 +54,8 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = { maxAppointmentsPerCell: 'auto', selectedCellData: [], groupByDate: false, + // @ts-expect-error + hiddenDays: undefined, onAppointmentRendered: undefined, onAppointmentClick: undefined, onAppointmentDblClick: undefined, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index 5118a8c7ce1f..6b20fcdf66ff 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -1,9 +1,14 @@ import { - describe, expect, it, + describe, expect, it, jest, } from '@jest/globals'; +import errors from '@js/ui/widget/ui.errors'; import { - getCurrentView, getViewOption, getViews, parseCurrentDate, parseDateOption, + getCurrentView, + getViewOption, + getViews, + parseCurrentDate, + parseDateOption, } from './utils'; describe('views utils', () => { @@ -128,6 +133,96 @@ describe('views utils', () => { ])('should return normalized $input.type view', ({ input, output }) => { expect(getViews([input] as any)).toEqual([{ ...output, skippedDays: [0, 6] }]); }); + + describe('hiddenDays', () => { + const getSkipped = ( + views: any[], + viewType: string, + globalHiddenDays?: number[], + ): number[] => { + const result = getViews(views, globalHiddenDays); + const view = result.find((v) => v.type === viewType); + return (view as any).skippedDays; + }; + + it('per-view hiddenDays on week → uses per-view value', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week')).toEqual([3]); + }); + + it('per-view hiddenDays: [] on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenDays: [] }], 'workWeek')).toEqual([]); + }); + + it('per-view hiddenDays on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenDays: [3] }], 'workWeek')).toEqual([3]); + }); + + it('global hiddenDays on workWeek → ignored, built-in default wins', () => { + expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([0, 6]); + }); + + it('global hiddenDays on week → applied', () => { + expect(getSkipped(['week'], 'week', [3])).toEqual([3]); + }); + + it('global hiddenDays on month → applied', () => { + expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); + }); + + it('global hiddenDays on timelineWeek → applied', () => { + expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); + }); + + it('global hiddenDays on timelineMonth → applied', () => { + expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); + }); + + it('global hiddenDays on day → ignored (unsupported view)', () => { + expect(getSkipped(['day'], 'day', [3])).toEqual([]); + }); + + it('global hiddenDays on agenda → ignored (unsupported view)', () => { + expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([]); + }); + + it('per-view hiddenDays dedupes duplicates', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); + }); + + it('per-view hiddenDays sorts ascending', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); + }); + + it('per-view hiddenDays filters out invalid values', () => { + expect( + getSkipped([{ type: 'week', hiddenDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), + ).toEqual([3]); + }); + + it('hiddenDays covering all 7 days → falls back to [] and logs W1029', () => { + const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); + try { + expect( + getSkipped([{ type: 'week', hiddenDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), + ).toEqual([]); + expect(logSpy).toHaveBeenCalledWith('W1029'); + } finally { + logSpy.mockRestore(); + } + }); + + it('global hiddenDays + per-view undefined on week → uses global', () => { + expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); + }); + + it('global hiddenDays + per-view [3] on week → per-view wins', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week', [0, 6])).toEqual([3]); + }); + + it('no hiddenDays anywhere on week → []', () => { + expect(getSkipped(['week'], 'week')).toEqual([]); + }); + }); }); describe('getCurrentView', () => { diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 2a266891a5a9..39391ae6a614 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -1,4 +1,5 @@ import { isObject } from '@js/core/utils/type'; +import errors from '@js/ui/widget/ui.errors'; import { dateUtils } from '@ts/core/utils/m_date'; import { dateSerialization } from '@ts/core/utils/m_date_serialization'; import { extend } from '@ts/core/utils/m_extend'; @@ -8,24 +9,102 @@ import type { DateOption, NormalizedView, RawViewType, SafeSchedulerOptions, ViewType, } from './types'; +const VIEWS_SUPPORTING_HIDDEN_DAYS: ReadonlySet = new Set([ + 'week', 'month', 'timelineWeek', 'timelineMonth', +]); + +const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ + 'workWeek', 'timelineWorkWeek', +]); + +const normalizeHiddenDays = ( + days: readonly unknown[] | undefined, +): number[] | undefined => { + if (!Array.isArray(days)) { + return undefined; + } + const valid = [...new Set(days)] + .filter((d): d is number => typeof d === 'number' && Number.isInteger(d) && d >= 0 && d <= 6) + .sort((a, b) => a - b); + if (valid.length >= 7) { + errors.log('W1029'); + return []; + } + return valid; +}; + +const resolveSkippedDays = ( + viewType: ViewType, + perViewHiddenDays: unknown, + globalHiddenDays: number[] | undefined, + viewDefault: number[], +): number[] => { + const perView = normalizeHiddenDays(perViewHiddenDays as readonly unknown[] | undefined); + if (perView !== undefined) { + return perView; + } + if (VIEWS_WITH_BUILTIN_SKIPPED.has(viewType)) { + return viewDefault; + } + if (globalHiddenDays !== undefined && VIEWS_SUPPORTING_HIDDEN_DAYS.has(viewType)) { + return normalizeHiddenDays(globalHiddenDays) ?? []; + } + return viewDefault; +}; + const isKnownView = (view: RawViewType): boolean => VIEW_TYPES .includes((isObject(view) ? view.type : view) as ViewType); const isExistedView = (view: NormalizedView | undefined): view is NormalizedView => Boolean(view); -const normalizeView = (view: RawViewType): NormalizedView | undefined => (isObject(view) - ? extend({}, DEFAULT_VIEW_OPTIONS[view.type as string], view) as NormalizedView - : DEFAULT_VIEW_OPTIONS[view]); +const normalizeView = ( + view: RawViewType, + globalHiddenDays?: number[], +): NormalizedView | undefined => { + if (isObject(view)) { + const viewType = view.type as ViewType; + const viewDefault = DEFAULT_VIEW_OPTIONS[viewType]; + if (!viewDefault) { + return undefined; + } + const merged = extend({}, viewDefault, view) as NormalizedView; + merged.skippedDays = resolveSkippedDays( + viewType, + (view as { hiddenDays?: unknown }).hiddenDays, + globalHiddenDays, + viewDefault.skippedDays, + ); + return merged; + } + const defaultView = DEFAULT_VIEW_OPTIONS[view]; + if (!defaultView) { + return undefined; + } + const skippedDays = resolveSkippedDays( + view as ViewType, + undefined, + globalHiddenDays, + defaultView.skippedDays, + ); + if (skippedDays === defaultView.skippedDays) { + return defaultView; + } + return { ...defaultView, skippedDays } as NormalizedView; +}; -export const getViews = (views: RawViewType[]): NormalizedView[] => views +export const getViews = ( + views: RawViewType[], + globalHiddenDays?: number[], +): NormalizedView[] => views .filter(isKnownView) - .map(normalizeView) + .map((v) => normalizeView(v, globalHiddenDays)) .filter(isExistedView); export function getCurrentView( currentView: string | ViewType, views: RawViewType[], + globalHiddenDays?: number[], ): NormalizedView { - const viewsProps = getViews(views); + const viewsProps = getViews(views, globalHiddenDays); const currentViewProps = viewsProps.find( (view) => [view.name, view.type].includes(currentView), ); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index ce529c625c80..e66a70ef97c8 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -908,6 +908,7 @@ class SchedulerWorkSpace extends Widget { startDate: this.option('startDate'), firstDayOfWeek: this.option('firstDayOfWeek'), showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'), + skippedDays: (this.option('skippedDays' as any) as number[] | undefined) ?? [], ...this.virtualScrollingDispatcher.getRenderState(), }; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts new file mode 100644 index 000000000000..b9cd28685a1f --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it } from '@jest/globals'; + +import type { ViewType } from '../../types'; +import { ViewDataGenerator } from './m_view_data_generator'; +import { ViewDataGeneratorMonth } from './m_view_data_generator_month'; +import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; +import { ViewDataGeneratorWorkWeek } from './m_view_data_generator_work_week'; + +describe('ViewDataGenerator hiddenDays support', () => { + describe('isSkippedDate', () => { + it('returns false when skippedDays is empty', () => { + const gen = new ViewDataGenerator('week' as ViewType); + gen.skippedDays = []; + expect(gen.isSkippedDate(new Date(2026, 3, 8))).toBe(false); + }); + + it('returns true for a day in skippedDays', () => { + const gen = new ViewDataGenerator('week' as ViewType); + gen.skippedDays = [3]; + expect(gen.isSkippedDate(new Date(2026, 3, 8))).toBe(true); + expect(gen.isSkippedDate(new Date(2026, 3, 9))).toBe(false); + }); + + it('returns true for any day in a multi-day skippedDays', () => { + const gen = new ViewDataGenerator('week' as ViewType); + gen.skippedDays = [0, 6]; + expect(gen.isSkippedDate(new Date(2026, 3, 11))).toBe(true); + expect(gen.isSkippedDate(new Date(2026, 3, 12))).toBe(true); + expect(gen.isSkippedDate(new Date(2026, 3, 13))).toBe(false); + }); + }); + + describe('daysInInterval getter', () => { + it('week view: 7 with empty skippedDays', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + gen.skippedDays = []; + expect(gen.daysInInterval).toBe(7); + }); + + it('week view: 5 with skippedDays [0,6]', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + gen.skippedDays = [0, 6]; + expect(gen.daysInInterval).toBe(5); + }); + + it('week view: 6 with skippedDays [3]', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + gen.skippedDays = [3]; + expect(gen.daysInInterval).toBe(6); + }); + + it('workWeek view: 5 with default skippedDays [0,6]', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + gen.skippedDays = [0, 6]; + expect(gen.daysInInterval).toBe(5); + }); + + it('day view: 1 (unaffected by skippedDays)', () => { + const gen = new ViewDataGenerator('day' as ViewType); + gen.skippedDays = []; + expect(gen.daysInInterval).toBe(1); + }); + }); + + describe('getVisibleDaysOfWeek', () => { + it('returns all 7 days when skippedDays is empty, rotated by firstDayOfWeek', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + gen.skippedDays = []; + expect(gen.getVisibleDaysOfWeek(0)).toEqual([0, 1, 2, 3, 4, 5, 6]); + expect(gen.getVisibleDaysOfWeek(1)).toEqual([1, 2, 3, 4, 5, 6, 0]); + }); + + it('skips hidden days, preserving visible-day order from firstDayOfWeek', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + gen.skippedDays = [0, 6]; + expect(gen.getVisibleDaysOfWeek(0)).toEqual([1, 2, 3, 4, 5]); + expect(gen.getVisibleDaysOfWeek(1)).toEqual([1, 2, 3, 4, 5]); + }); + + it('skips a single mid-week day', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + gen.skippedDays = [3]; + expect(gen.getVisibleDaysOfWeek(0)).toEqual([0, 1, 2, 4, 5, 6]); + expect(gen.getVisibleDaysOfWeek(1)).toEqual([1, 2, 4, 5, 6, 0]); + }); + }); + + describe('getVisibleDayOffset for week-style layout', () => { + const gen = new ViewDataGeneratorWeek('week' as ViewType); + + const callGetVisibleDayOffset = ( + g: ViewDataGeneratorWeek, + rowIndex: number, + columnIndex: number, + firstDayOfWeek: number, + ): number => (g as unknown as { + getVisibleDayOffset: (r: number, c: number, firstDay: number) => number; + }).getVisibleDayOffset(rowIndex, columnIndex, firstDayOfWeek); + + it('zero offset for empty skippedDays', () => { + gen.skippedDays = []; + expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 5, 0)).toBe(0); + }); + + it('week with [0,6], firstDayOfWeek=1 (Mon): col 0..4 → 0 offset, col 5 → +2', () => { + gen.skippedDays = [0, 6]; + [0, 1, 2, 3, 4].forEach((col) => { + expect(callGetVisibleDayOffset(gen, 0, col, 1)).toBe(0); + }); + expect(callGetVisibleDayOffset(gen, 0, 5, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 9, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 10, 1)).toBe(4); + }); + + it('week with [3] (skip Wed), firstDayOfWeek=0 (Sun): col 3 → +1 to skip Wed', () => { + gen.skippedDays = [3]; + expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 1, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 2, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 3, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 4, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 5, 0)).toBe(1); + }); + + it('week with [1,3,5] (skip Mon, Wed, Fri), firstDayOfWeek=0', () => { + gen.skippedDays = [1, 3, 5]; + expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 1, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 2, 0)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 3, 0)).toBe(3); + expect(callGetVisibleDayOffset(gen, 0, 4, 0)).toBe(3); + }); + }); + + describe('getVisibleDayOffset for month-style layout', () => { + const gen = new ViewDataGeneratorMonth('month' as ViewType); + + const callGetVisibleDayOffset = ( + g: ViewDataGeneratorMonth, + rowIndex: number, + columnIndex: number, + firstDayOfWeek: number, + ): number => (g as unknown as { + getVisibleDayOffset: (r: number, c: number, firstDay: number) => number; + }).getVisibleDayOffset(rowIndex, columnIndex, firstDayOfWeek); + + it('returns 0 for empty skippedDays', () => { + gen.skippedDays = []; + expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 3, 5, 0)).toBe(0); + }); + + it('month with [0,6], firstDayOfWeek=1: row=1 col=0 → +2 (jumps over Sat+Sun)', () => { + gen.skippedDays = [0, 6]; + expect(callGetVisibleDayOffset(gen, 0, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 4, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 1, 0, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 1, 4, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 2, 0, 1)).toBe(4); + }); + + it('month with [3] (skip Wed), firstDayOfWeek=0: visible days = Sun,Mon,Tue,Thu,Fri,Sat', () => { + gen.skippedDays = [3]; + expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 2, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 3, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 5, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 1, 0, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 1, 3, 0)).toBe(2); + expect(callGetVisibleDayOffset(gen, 1, 5, 0)).toBe(2); + expect(callGetVisibleDayOffset(gen, 2, 0, 0)).toBe(2); + }); + }); + + describe('Month view getCellCount honors skippedDays', () => { + it('returns 7 with empty skippedDays', () => { + const gen = new ViewDataGeneratorMonth('month' as ViewType); + gen.skippedDays = []; + expect(gen.getCellCount()).toBe(7); + }); + + it('returns 5 with skippedDays [0, 6]', () => { + const gen = new ViewDataGeneratorMonth('month' as ViewType); + gen.skippedDays = [0, 6]; + expect(gen.getCellCount()).toBe(5); + }); + + it('returns 6 with skippedDays [3]', () => { + const gen = new ViewDataGeneratorMonth('month' as ViewType); + gen.skippedDays = [3]; + expect(gen.getCellCount()).toBe(6); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index d7626023448d..6726b774cc74 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -28,14 +28,27 @@ import type { const toMs = dateUtils.dateToMilliseconds; export class ViewDataGenerator { - readonly daysInInterval: number = 1; + protected baseDaysInInterval = 1; protected tableAllDay = false; public hiddenInterval = 0; + public skippedDays: number[] = []; + constructor(public readonly viewType: ViewType) {} + get daysInInterval(): number { + if (this.skippedDays.length === 0) { + return this.baseDaysInInterval; + } + const visibleDayCount = 7 - this.skippedDays.length; + if (this.baseDaysInInterval >= 7) { + return visibleDayCount; + } + return this.baseDaysInInterval; + } + public isWorkWeekView(): boolean { return [ VIEWS.WORK_WEEK, @@ -43,11 +56,54 @@ export class ViewDataGenerator { ].includes(this.viewType); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public isSkippedDate(date: any) { + protected usesWeeklyDayLayout(): boolean { + return this.baseDaysInInterval >= 7; + } + + protected usesMonthDayLayout(): boolean { return false; } + public getVisibleDaysOfWeek(firstDayOfWeek: number): number[] { + const rotated: number[] = []; + for (let count = 0; count < 7; count += 1) { + const dayOfWeek = (firstDayOfWeek + count) % 7; + if (!this.skippedDays.includes(dayOfWeek)) { + rotated.push(dayOfWeek); + } + } + return rotated; + } + + protected getVisibleDayOffset( + rowIndex: number, + columnIndex: number, + firstDayOfWeek: number, + ): number { + const rotated = this.getVisibleDaysOfWeek(firstDayOfWeek); + const visibleCount = rotated.length; + if (visibleCount === 0) { + return 0; + } + if (this.usesMonthDayLayout()) { + const targetDayOfWeek = rotated[columnIndex]; + const naiveDayOffset = rowIndex * visibleCount + columnIndex; + const actualDayOffset = rowIndex * 7 + + ((targetDayOfWeek - firstDayOfWeek + 7) % 7); + return actualDayOffset - naiveDayOffset; + } + const week = Math.floor(columnIndex / visibleCount); + const idxInWeek = columnIndex % visibleCount; + const targetDayOfWeek = rotated[idxInWeek]; + const naiveDayOffset = columnIndex; + const actualDayOffset = week * 7 + ((targetDayOfWeek - firstDayOfWeek + 7) % 7); + return actualDayOffset - naiveDayOffset; + } + + public isSkippedDate(date: Date): boolean { + return this.skippedDays.includes(date.getDay()); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars protected calculateStartViewDate(options: any): Date { return new Date(); @@ -74,6 +130,7 @@ export class ViewDataGenerator { hoursInterval, } = options; + this.skippedDays = ((options as any)?.skippedDays as number[] | undefined) ?? []; this.setVisibilityDates(options); this.setHiddenInterval(startDayHour, endDayHour, hoursInterval); @@ -512,13 +569,26 @@ export class ViewDataGenerator { const cellIndex = this.calculateCellIndex(rowIndex, columnIndex, rowCountBase, columnCountBase); const millisecondsOffset = this.getMillisecondsOffset(cellIndex, interval, cellCountInDay); - const offsetByCount = this.isWorkWeekView() - ? this.getTimeOffsetByColumnIndex( + let offsetByCount: number; + if (this.isWorkWeekView()) { + offsetByCount = this.getTimeOffsetByColumnIndex( columnIndex, this.getFirstDayOfWeek(firstDayOfWeek), columnCountBase, intervalCount, - ) : 0; + ); + } else if ( + this.skippedDays.length > 0 + && (this.usesWeeklyDayLayout() || this.usesMonthDayLayout()) + ) { + offsetByCount = this.getVisibleDayOffset( + rowIndex, + columnIndex, + this.getFirstDayOfWeek(firstDayOfWeek) ?? 0, + ) * toMs('day'); + } else { + offsetByCount = 0; + } const isStartViewDateDuringDST = startViewDate.getHours() !== Math.floor(startDayHour); let startViewDateTime = startViewDate.getTime(); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_month.ts index 519bdf076052..9f979cfa05b1 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_month.ts @@ -90,7 +90,11 @@ export class ViewDataGeneratorMonth extends ViewDataGenerator { } getCellCount() { - return DAYS_IN_WEEK; + return DAYS_IN_WEEK - this.skippedDays.length; + } + + protected usesMonthDayLayout(): boolean { + return true; } getRowCount(options) { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_week.ts index c62a758cc001..e2c8262a2993 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_week.ts @@ -2,7 +2,7 @@ import { weekUtils } from '../../r1/utils/index'; import { ViewDataGenerator } from './m_view_data_generator'; export class ViewDataGeneratorWeek extends ViewDataGenerator { - readonly daysInInterval: number = 7; + protected baseDaysInInterval = 7; _getIntervalDuration(intervalCount) { return weekUtils.getIntervalDuration(intervalCount); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index ecf2465f32b7..e818624cd675 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -1,12 +1,8 @@ -import { isDataOnWeekend, workWeekUtils } from '../../r1/utils/index'; +import { workWeekUtils } from '../../r1/utils/index'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { - readonly daysInInterval = 5; - - isSkippedDate(date) { - return isDataOnWeekend(date); - } + protected baseDaysInInterval = 5; protected calculateStartViewDate(options) { return workWeekUtils.calculateStartViewDate( diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index e340779e1e15..eedc323aa341 100644 --- a/packages/devextreme/js/ui/widget/ui.errors.js +++ b/packages/devextreme/js/ui/widget/ui.errors.js @@ -397,4 +397,8 @@ export default errorUtils(errors.ERROR_MESSAGES, { * @name ErrorsUIWidgets.W1028 */ W1028: 'Nested/banded columns do not support the following properties: {0}.', + /** + * @name ErrorsUIWidgets.W1029 + */ + W1029: 'The "hiddenDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', }); diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 51da85b9e775..28f88f5ede00 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -26590,6 +26590,10 @@ declare module DevExpress.ui { * [descr:dxSchedulerOptions.groups] */ groups?: Array; + /** + * [descr:dxSchedulerOptions.hiddenDays] + */ + hiddenDays?: Array; /** * [descr:dxSchedulerOptions.indicatorUpdateInterval] */ @@ -26921,6 +26925,10 @@ declare module DevExpress.ui { * [descr:dxSchedulerOptions.views.groups] */ groups?: Array; + /** + * [descr:dxSchedulerOptions.views.hiddenDays] + */ + hiddenDays?: Array; /** * [descr:dxSchedulerOptions.views.intervalCount] */ From 24676000d4aa76dd16aebdd4a76465473d6b24a1 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Wed, 8 Apr 2026 13:01:35 +0000 Subject: [PATCH 02/30] =?UTF-8?q?Scheduler=20=E2=80=94=20Rename=20hiddenDa?= =?UTF-8?q?ys=20option=20to=20hiddenWeekDays=20per=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../js/__internal/scheduler/m_scheduler.ts | 2 +- .../scheduler_options_base_widget.ts | 8 +-- .../scheduler/utils/options/constants.ts | 2 +- .../scheduler/utils/options/utils.test.ts | 52 +++++++++---------- .../scheduler/utils/options/utils.ts | 2 +- .../view_model/m_view_data_generator.test.ts | 2 +- packages/devextreme/js/ui/widget/ui.errors.js | 2 +- packages/devextreme/ts/dx.all.d.ts | 8 +-- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index c9c7131060fc..f2ee28d53680 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -314,7 +314,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateOption('header', 'views', this.views); } break; - case 'hiddenDays': + case 'hiddenWeekDays': this.repaint(); break; case 'useDropDownViewSwitcher': diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 0d5a2de17e9b..c7a6c970f15e 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -50,13 +50,13 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - const hiddenDays = this.option('hiddenDays' as keyof SafeSchedulerOptions) as + const hiddenWeekDays = this.option('hiddenWeekDays' as keyof SafeSchedulerOptions) as number[] | undefined; - this.views = getViews(views, hiddenDays); + this.views = getViews(views, hiddenWeekDays); this.currentView = getCurrentView( this.option('currentView') ?? '', views, - hiddenDays, + hiddenWeekDays, ); } @@ -74,7 +74,7 @@ export class SchedulerOptionsBaseWidget extends Widget { switch (args.name) { case 'currentView': case 'views': - case 'hiddenDays' as K: + case 'hiddenWeekDays' as K: this.updateViews(); break; default: diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index 5bc17302fd53..e034391e0bcb 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -55,7 +55,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = { selectedCellData: [], groupByDate: false, // @ts-expect-error - hiddenDays: undefined, + hiddenWeekDays: undefined, onAppointmentRendered: undefined, onAppointmentClick: undefined, onAppointmentDblClick: undefined, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index 6b20fcdf66ff..f3099c0273c7 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -134,7 +134,7 @@ describe('views utils', () => { expect(getViews([input] as any)).toEqual([{ ...output, skippedDays: [0, 6] }]); }); - describe('hiddenDays', () => { + describe('hiddenWeekDays', () => { const getSkipped = ( views: any[], viewType: string, @@ -145,65 +145,65 @@ describe('views utils', () => { return (view as any).skippedDays; }; - it('per-view hiddenDays on week → uses per-view value', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week')).toEqual([3]); + it('per-view hiddenWeekDays on week → uses per-view value', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); }); - it('per-view hiddenDays: [] on workWeek → overrides built-in default', () => { - expect(getSkipped([{ type: 'workWeek', hiddenDays: [] }], 'workWeek')).toEqual([]); + it('per-view hiddenWeekDays: [] on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); }); - it('per-view hiddenDays on workWeek → overrides built-in default', () => { - expect(getSkipped([{ type: 'workWeek', hiddenDays: [3] }], 'workWeek')).toEqual([3]); + it('per-view hiddenWeekDays on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek')).toEqual([3]); }); - it('global hiddenDays on workWeek → ignored, built-in default wins', () => { + it('global hiddenWeekDays on workWeek → ignored, built-in default wins', () => { expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([0, 6]); }); - it('global hiddenDays on week → applied', () => { + it('global hiddenWeekDays on week → applied', () => { expect(getSkipped(['week'], 'week', [3])).toEqual([3]); }); - it('global hiddenDays on month → applied', () => { + it('global hiddenWeekDays on month → applied', () => { expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); }); - it('global hiddenDays on timelineWeek → applied', () => { + it('global hiddenWeekDays on timelineWeek → applied', () => { expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); }); - it('global hiddenDays on timelineMonth → applied', () => { + it('global hiddenWeekDays on timelineMonth → applied', () => { expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); }); - it('global hiddenDays on day → ignored (unsupported view)', () => { + it('global hiddenWeekDays on day → ignored (unsupported view)', () => { expect(getSkipped(['day'], 'day', [3])).toEqual([]); }); - it('global hiddenDays on agenda → ignored (unsupported view)', () => { + it('global hiddenWeekDays on agenda → ignored (unsupported view)', () => { expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([]); }); - it('per-view hiddenDays dedupes duplicates', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); + it('per-view hiddenWeekDays dedupes duplicates', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); }); - it('per-view hiddenDays sorts ascending', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); + it('per-view hiddenWeekDays sorts ascending', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); }); - it('per-view hiddenDays filters out invalid values', () => { + it('per-view hiddenWeekDays filters out invalid values', () => { expect( - getSkipped([{ type: 'week', hiddenDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), + getSkipped([{ type: 'week', hiddenWeekDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), ).toEqual([3]); }); - it('hiddenDays covering all 7 days → falls back to [] and logs W1029', () => { + it('hiddenWeekDays covering all 7 days → falls back to [] and logs W1029', () => { const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); try { expect( - getSkipped([{ type: 'week', hiddenDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), + getSkipped([{ type: 'week', hiddenWeekDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), ).toEqual([]); expect(logSpy).toHaveBeenCalledWith('W1029'); } finally { @@ -211,15 +211,15 @@ describe('views utils', () => { } }); - it('global hiddenDays + per-view undefined on week → uses global', () => { + it('global hiddenWeekDays + per-view undefined on week → uses global', () => { expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); }); - it('global hiddenDays + per-view [3] on week → per-view wins', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week', [0, 6])).toEqual([3]); + it('global hiddenWeekDays + per-view [3] on week → per-view wins', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week', [0, 6])).toEqual([3]); }); - it('no hiddenDays anywhere on week → []', () => { + it('no hiddenWeekDays anywhere on week → []', () => { expect(getSkipped(['week'], 'week')).toEqual([]); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 39391ae6a614..d36367cc2a8b 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -69,7 +69,7 @@ const normalizeView = ( const merged = extend({}, viewDefault, view) as NormalizedView; merged.skippedDays = resolveSkippedDays( viewType, - (view as { hiddenDays?: unknown }).hiddenDays, + (view as { hiddenWeekDays?: unknown }).hiddenWeekDays, globalHiddenDays, viewDefault.skippedDays, ); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index b9cd28685a1f..87e0641a2c2c 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -6,7 +6,7 @@ import { ViewDataGeneratorMonth } from './m_view_data_generator_month'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; import { ViewDataGeneratorWorkWeek } from './m_view_data_generator_work_week'; -describe('ViewDataGenerator hiddenDays support', () => { +describe('ViewDataGenerator hiddenWeekDays support', () => { describe('isSkippedDate', () => { it('returns false when skippedDays is empty', () => { const gen = new ViewDataGenerator('week' as ViewType); diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index eedc323aa341..dce5034ed7a1 100644 --- a/packages/devextreme/js/ui/widget/ui.errors.js +++ b/packages/devextreme/js/ui/widget/ui.errors.js @@ -400,5 +400,5 @@ export default errorUtils(errors.ERROR_MESSAGES, { /** * @name ErrorsUIWidgets.W1029 */ - W1029: 'The "hiddenDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', + W1029: 'The "hiddenWeekDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', }); diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 28f88f5ede00..30e4489e4764 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -26591,9 +26591,9 @@ declare module DevExpress.ui { */ groups?: Array; /** - * [descr:dxSchedulerOptions.hiddenDays] + * [descr:dxSchedulerOptions.hiddenWeekDays] */ - hiddenDays?: Array; + hiddenWeekDays?: Array; /** * [descr:dxSchedulerOptions.indicatorUpdateInterval] */ @@ -26926,9 +26926,9 @@ declare module DevExpress.ui { */ groups?: Array; /** - * [descr:dxSchedulerOptions.views.hiddenDays] + * [descr:dxSchedulerOptions.views.hiddenWeekDays] */ - hiddenDays?: Array; + hiddenWeekDays?: Array; /** * [descr:dxSchedulerOptions.views.intervalCount] */ From fbeace849af75e076a0e7f2054102f38e203f3fb Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Wed, 8 Apr 2026 13:07:20 +0000 Subject: [PATCH 03/30] =?UTF-8?q?Scheduler=20=E2=80=94=20Add=20hiddenWeekD?= =?UTF-8?q?ays=20to=20scheduler.d.ts=20source=20for=20ts-bundle=20regen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/devextreme/js/ui/scheduler.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/devextreme/js/ui/scheduler.d.ts b/packages/devextreme/js/ui/scheduler.d.ts index df45266e11c3..3ef8a0dd2803 100644 --- a/packages/devextreme/js/ui/scheduler.d.ts +++ b/packages/devextreme/js/ui/scheduler.d.ts @@ -703,6 +703,12 @@ export interface dxSchedulerOptions extends WidgetOptions { * @public */ groups?: Array; + /** + * @docid + * @default undefined + * @public + */ + hiddenWeekDays?: Array; /** * @docid * @default 300000 @@ -1101,6 +1107,11 @@ export interface dxSchedulerOptions extends WidgetOptions { * @default [] */ groups?: Array; + /** + * @docid + * @default undefined + */ + hiddenWeekDays?: Array; /** * @docid * @default 1 From 18943b86662710ff5ad6dfe6a4194df8bfac4a94 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 15:10:28 +0200 Subject: [PATCH 04/30] =?UTF-8?q?Revert=20"Scheduler=20=E2=80=94=20Add=20h?= =?UTF-8?q?iddenWeekDays=20to=20scheduler.d.ts=20source=20for=20ts-bundle?= =?UTF-8?q?=20regen"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit fbeace849af75e076a0e7f2054102f38e203f3fb. --- packages/devextreme/js/ui/scheduler.d.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/devextreme/js/ui/scheduler.d.ts b/packages/devextreme/js/ui/scheduler.d.ts index 3ef8a0dd2803..df45266e11c3 100644 --- a/packages/devextreme/js/ui/scheduler.d.ts +++ b/packages/devextreme/js/ui/scheduler.d.ts @@ -703,12 +703,6 @@ export interface dxSchedulerOptions extends WidgetOptions { * @public */ groups?: Array; - /** - * @docid - * @default undefined - * @public - */ - hiddenWeekDays?: Array; /** * @docid * @default 300000 @@ -1107,11 +1101,6 @@ export interface dxSchedulerOptions extends WidgetOptions { * @default [] */ groups?: Array; - /** - * @docid - * @default undefined - */ - hiddenWeekDays?: Array; /** * @docid * @default 1 From c385706526511a6e2313e938bef695289036ebba Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 15:10:28 +0200 Subject: [PATCH 05/30] =?UTF-8?q?Revert=20"Scheduler=20=E2=80=94=20Rename?= =?UTF-8?q?=20hiddenDays=20option=20to=20hiddenWeekDays=20per=20spec"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 24676000d4aa76dd16aebdd4a76465473d6b24a1. --- .../js/__internal/scheduler/m_scheduler.ts | 2 +- .../scheduler_options_base_widget.ts | 8 +-- .../scheduler/utils/options/constants.ts | 2 +- .../scheduler/utils/options/utils.test.ts | 52 +++++++++---------- .../scheduler/utils/options/utils.ts | 2 +- .../view_model/m_view_data_generator.test.ts | 2 +- packages/devextreme/js/ui/widget/ui.errors.js | 2 +- packages/devextreme/ts/dx.all.d.ts | 8 +-- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index f2ee28d53680..c9c7131060fc 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -314,7 +314,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateOption('header', 'views', this.views); } break; - case 'hiddenWeekDays': + case 'hiddenDays': this.repaint(); break; case 'useDropDownViewSwitcher': diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index c7a6c970f15e..0d5a2de17e9b 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -50,13 +50,13 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - const hiddenWeekDays = this.option('hiddenWeekDays' as keyof SafeSchedulerOptions) as + const hiddenDays = this.option('hiddenDays' as keyof SafeSchedulerOptions) as number[] | undefined; - this.views = getViews(views, hiddenWeekDays); + this.views = getViews(views, hiddenDays); this.currentView = getCurrentView( this.option('currentView') ?? '', views, - hiddenWeekDays, + hiddenDays, ); } @@ -74,7 +74,7 @@ export class SchedulerOptionsBaseWidget extends Widget { switch (args.name) { case 'currentView': case 'views': - case 'hiddenWeekDays' as K: + case 'hiddenDays' as K: this.updateViews(); break; default: diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index e034391e0bcb..5bc17302fd53 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -55,7 +55,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = { selectedCellData: [], groupByDate: false, // @ts-expect-error - hiddenWeekDays: undefined, + hiddenDays: undefined, onAppointmentRendered: undefined, onAppointmentClick: undefined, onAppointmentDblClick: undefined, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index f3099c0273c7..6b20fcdf66ff 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -134,7 +134,7 @@ describe('views utils', () => { expect(getViews([input] as any)).toEqual([{ ...output, skippedDays: [0, 6] }]); }); - describe('hiddenWeekDays', () => { + describe('hiddenDays', () => { const getSkipped = ( views: any[], viewType: string, @@ -145,65 +145,65 @@ describe('views utils', () => { return (view as any).skippedDays; }; - it('per-view hiddenWeekDays on week → uses per-view value', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); + it('per-view hiddenDays on week → uses per-view value', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week')).toEqual([3]); }); - it('per-view hiddenWeekDays: [] on workWeek → overrides built-in default', () => { - expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); + it('per-view hiddenDays: [] on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenDays: [] }], 'workWeek')).toEqual([]); }); - it('per-view hiddenWeekDays on workWeek → overrides built-in default', () => { - expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek')).toEqual([3]); + it('per-view hiddenDays on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenDays: [3] }], 'workWeek')).toEqual([3]); }); - it('global hiddenWeekDays on workWeek → ignored, built-in default wins', () => { + it('global hiddenDays on workWeek → ignored, built-in default wins', () => { expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([0, 6]); }); - it('global hiddenWeekDays on week → applied', () => { + it('global hiddenDays on week → applied', () => { expect(getSkipped(['week'], 'week', [3])).toEqual([3]); }); - it('global hiddenWeekDays on month → applied', () => { + it('global hiddenDays on month → applied', () => { expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); }); - it('global hiddenWeekDays on timelineWeek → applied', () => { + it('global hiddenDays on timelineWeek → applied', () => { expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); }); - it('global hiddenWeekDays on timelineMonth → applied', () => { + it('global hiddenDays on timelineMonth → applied', () => { expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); }); - it('global hiddenWeekDays on day → ignored (unsupported view)', () => { + it('global hiddenDays on day → ignored (unsupported view)', () => { expect(getSkipped(['day'], 'day', [3])).toEqual([]); }); - it('global hiddenWeekDays on agenda → ignored (unsupported view)', () => { + it('global hiddenDays on agenda → ignored (unsupported view)', () => { expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([]); }); - it('per-view hiddenWeekDays dedupes duplicates', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); + it('per-view hiddenDays dedupes duplicates', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); }); - it('per-view hiddenWeekDays sorts ascending', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); + it('per-view hiddenDays sorts ascending', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); }); - it('per-view hiddenWeekDays filters out invalid values', () => { + it('per-view hiddenDays filters out invalid values', () => { expect( - getSkipped([{ type: 'week', hiddenWeekDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), + getSkipped([{ type: 'week', hiddenDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), ).toEqual([3]); }); - it('hiddenWeekDays covering all 7 days → falls back to [] and logs W1029', () => { + it('hiddenDays covering all 7 days → falls back to [] and logs W1029', () => { const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); try { expect( - getSkipped([{ type: 'week', hiddenWeekDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), + getSkipped([{ type: 'week', hiddenDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), ).toEqual([]); expect(logSpy).toHaveBeenCalledWith('W1029'); } finally { @@ -211,15 +211,15 @@ describe('views utils', () => { } }); - it('global hiddenWeekDays + per-view undefined on week → uses global', () => { + it('global hiddenDays + per-view undefined on week → uses global', () => { expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); }); - it('global hiddenWeekDays + per-view [3] on week → per-view wins', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week', [0, 6])).toEqual([3]); + it('global hiddenDays + per-view [3] on week → per-view wins', () => { + expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week', [0, 6])).toEqual([3]); }); - it('no hiddenWeekDays anywhere on week → []', () => { + it('no hiddenDays anywhere on week → []', () => { expect(getSkipped(['week'], 'week')).toEqual([]); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index d36367cc2a8b..39391ae6a614 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -69,7 +69,7 @@ const normalizeView = ( const merged = extend({}, viewDefault, view) as NormalizedView; merged.skippedDays = resolveSkippedDays( viewType, - (view as { hiddenWeekDays?: unknown }).hiddenWeekDays, + (view as { hiddenDays?: unknown }).hiddenDays, globalHiddenDays, viewDefault.skippedDays, ); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index 87e0641a2c2c..b9cd28685a1f 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -6,7 +6,7 @@ import { ViewDataGeneratorMonth } from './m_view_data_generator_month'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; import { ViewDataGeneratorWorkWeek } from './m_view_data_generator_work_week'; -describe('ViewDataGenerator hiddenWeekDays support', () => { +describe('ViewDataGenerator hiddenDays support', () => { describe('isSkippedDate', () => { it('returns false when skippedDays is empty', () => { const gen = new ViewDataGenerator('week' as ViewType); diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index dce5034ed7a1..eedc323aa341 100644 --- a/packages/devextreme/js/ui/widget/ui.errors.js +++ b/packages/devextreme/js/ui/widget/ui.errors.js @@ -400,5 +400,5 @@ export default errorUtils(errors.ERROR_MESSAGES, { /** * @name ErrorsUIWidgets.W1029 */ - W1029: 'The "hiddenWeekDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', + W1029: 'The "hiddenDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', }); diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 30e4489e4764..28f88f5ede00 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -26591,9 +26591,9 @@ declare module DevExpress.ui { */ groups?: Array; /** - * [descr:dxSchedulerOptions.hiddenWeekDays] + * [descr:dxSchedulerOptions.hiddenDays] */ - hiddenWeekDays?: Array; + hiddenDays?: Array; /** * [descr:dxSchedulerOptions.indicatorUpdateInterval] */ @@ -26926,9 +26926,9 @@ declare module DevExpress.ui { */ groups?: Array; /** - * [descr:dxSchedulerOptions.views.hiddenWeekDays] + * [descr:dxSchedulerOptions.views.hiddenDays] */ - hiddenWeekDays?: Array; + hiddenDays?: Array; /** * [descr:dxSchedulerOptions.views.intervalCount] */ From 7cadb0216c9f0788d36c271d181b99bb36cfdb3e Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 15:13:22 +0200 Subject: [PATCH 06/30] feat: rename hiddenDays to hiddenWeekDays and fix .d.ts --- .../src/ui/scheduler/index.ts | 26 ++++++++- .../src/ui/scheduler/nested/view-dxi.ts | 8 +++ .../make-angular-metadata.ts | 1 + packages/devextreme-react/src/scheduler.ts | 1 + packages/devextreme-vue/src/scheduler.ts | 5 ++ .../js/__internal/scheduler/m_scheduler.ts | 2 +- .../scheduler_options_base_widget.ts | 9 ++- .../scheduler/utils/options/constants.ts | 3 +- .../scheduler/utils/options/utils.test.ts | 56 +++++++++---------- .../scheduler/utils/options/utils.ts | 28 +++++----- .../view_model/m_view_data_generator.test.ts | 2 +- packages/devextreme/js/ui/scheduler.d.ts | 11 ++++ packages/devextreme/js/ui/widget/ui.errors.js | 2 +- packages/devextreme/ts/dx.all.d.ts | 16 +++--- 14 files changed, 107 insertions(+), 63 deletions(-) diff --git a/packages/devextreme-angular/src/ui/scheduler/index.ts b/packages/devextreme-angular/src/ui/scheduler/index.ts index 88811eb6e583..adbcc7672314 100644 --- a/packages/devextreme-angular/src/ui/scheduler/index.ts +++ b/packages/devextreme-angular/src/ui/scheduler/index.ts @@ -515,6 +515,16 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh } + + @Input() + get hiddenWeekDays(): Array { + return this._getOption('hiddenWeekDays'); + } + set hiddenWeekDays(value: Array) { + this._setOption('hiddenWeekDays', value); + } + + /** * [descr:WidgetOptions.hint] @@ -894,10 +904,10 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh */ @Input() - get views(): Array | string> | { agendaDuration?: number, allDayPanelMode?: AllDayPanelMode, appointmentCollectorTemplate?: any, appointmentTemplate?: any, appointmentTooltipTemplate?: any, cellDuration?: number, dataCellTemplate?: any, dateCellTemplate?: any, endDayHour?: number, firstDayOfWeek?: FirstDayOfWeek | undefined, groupByDate?: boolean, groupOrientation?: Orientation, groups?: Array, intervalCount?: number, maxAppointmentsPerCell?: CellAppointmentsLimit | number, name?: string | undefined, offset?: number, resourceCellTemplate?: any, scrolling?: dxSchedulerScrolling, snapToCellsMode?: SnapToCellsMode, startDate?: Date | number | string | undefined, startDayHour?: number, timeCellTemplate?: any, type?: undefined | ViewType }[] { + get views(): Array | string> | { agendaDuration?: number, allDayPanelMode?: AllDayPanelMode, appointmentCollectorTemplate?: any, appointmentTemplate?: any, appointmentTooltipTemplate?: any, cellDuration?: number, dataCellTemplate?: any, dateCellTemplate?: any, endDayHour?: number, firstDayOfWeek?: FirstDayOfWeek | undefined, groupByDate?: boolean, groupOrientation?: Orientation, groups?: Array, hiddenWeekDays?: Array, intervalCount?: number, maxAppointmentsPerCell?: CellAppointmentsLimit | number, name?: string | undefined, offset?: number, resourceCellTemplate?: any, scrolling?: dxSchedulerScrolling, snapToCellsMode?: SnapToCellsMode, startDate?: Date | number | string | undefined, startDayHour?: number, timeCellTemplate?: any, type?: undefined | ViewType }[] { return this._getOption('views'); } - set views(value: Array | string> | { agendaDuration?: number, allDayPanelMode?: AllDayPanelMode, appointmentCollectorTemplate?: any, appointmentTemplate?: any, appointmentTooltipTemplate?: any, cellDuration?: number, dataCellTemplate?: any, dateCellTemplate?: any, endDayHour?: number, firstDayOfWeek?: FirstDayOfWeek | undefined, groupByDate?: boolean, groupOrientation?: Orientation, groups?: Array, intervalCount?: number, maxAppointmentsPerCell?: CellAppointmentsLimit | number, name?: string | undefined, offset?: number, resourceCellTemplate?: any, scrolling?: dxSchedulerScrolling, snapToCellsMode?: SnapToCellsMode, startDate?: Date | number | string | undefined, startDayHour?: number, timeCellTemplate?: any, type?: undefined | ViewType }[]) { + set views(value: Array | string> | { agendaDuration?: number, allDayPanelMode?: AllDayPanelMode, appointmentCollectorTemplate?: any, appointmentTemplate?: any, appointmentTooltipTemplate?: any, cellDuration?: number, dataCellTemplate?: any, dateCellTemplate?: any, endDayHour?: number, firstDayOfWeek?: FirstDayOfWeek | undefined, groupByDate?: boolean, groupOrientation?: Orientation, groups?: Array, hiddenWeekDays?: Array, intervalCount?: number, maxAppointmentsPerCell?: CellAppointmentsLimit | number, name?: string | undefined, offset?: number, resourceCellTemplate?: any, scrolling?: dxSchedulerScrolling, snapToCellsMode?: SnapToCellsMode, startDate?: Date | number | string | undefined, startDayHour?: number, timeCellTemplate?: any, type?: undefined | ViewType }[]) { this._setOption('views', value); } @@ -1274,6 +1284,13 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh */ @Output() heightChange: EventEmitter; + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() hiddenWeekDaysChange: EventEmitter>; + /** * This member supports the internal infrastructure and is not intended to be used directly from your code. @@ -1482,7 +1499,7 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh * This member supports the internal infrastructure and is not intended to be used directly from your code. */ - @Output() viewsChange: EventEmitter | string> | { agendaDuration?: number, allDayPanelMode?: AllDayPanelMode, appointmentCollectorTemplate?: any, appointmentTemplate?: any, appointmentTooltipTemplate?: any, cellDuration?: number, dataCellTemplate?: any, dateCellTemplate?: any, endDayHour?: number, firstDayOfWeek?: FirstDayOfWeek | undefined, groupByDate?: boolean, groupOrientation?: Orientation, groups?: Array, intervalCount?: number, maxAppointmentsPerCell?: CellAppointmentsLimit | number, name?: string | undefined, offset?: number, resourceCellTemplate?: any, scrolling?: dxSchedulerScrolling, snapToCellsMode?: SnapToCellsMode, startDate?: Date | number | string | undefined, startDayHour?: number, timeCellTemplate?: any, type?: undefined | ViewType }[]>; + @Output() viewsChange: EventEmitter | string> | { agendaDuration?: number, allDayPanelMode?: AllDayPanelMode, appointmentCollectorTemplate?: any, appointmentTemplate?: any, appointmentTooltipTemplate?: any, cellDuration?: number, dataCellTemplate?: any, dateCellTemplate?: any, endDayHour?: number, firstDayOfWeek?: FirstDayOfWeek | undefined, groupByDate?: boolean, groupOrientation?: Orientation, groups?: Array, hiddenWeekDays?: Array, intervalCount?: number, maxAppointmentsPerCell?: CellAppointmentsLimit | number, name?: string | undefined, offset?: number, resourceCellTemplate?: any, scrolling?: dxSchedulerScrolling, snapToCellsMode?: SnapToCellsMode, startDate?: Date | number | string | undefined, startDayHour?: number, timeCellTemplate?: any, type?: undefined | ViewType }[]>; /** @@ -1558,6 +1575,7 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh { emit: 'groupByDateChange' }, { emit: 'groupsChange' }, { emit: 'heightChange' }, + { emit: 'hiddenWeekDaysChange' }, { emit: 'hintChange' }, { emit: 'indicatorUpdateIntervalChange' }, { emit: 'maxChange' }, @@ -1610,6 +1628,7 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh super.ngOnChanges(changes); this.setupChanges('dataSource', changes); this.setupChanges('groups', changes); + this.setupChanges('hiddenWeekDays', changes); this.setupChanges('resources', changes); this.setupChanges('selectedCellData', changes); this.setupChanges('views', changes); @@ -1624,6 +1643,7 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh ngDoCheck() { this._idh.doCheck('dataSource'); this._idh.doCheck('groups'); + this._idh.doCheck('hiddenWeekDays'); this._idh.doCheck('resources'); this._idh.doCheck('selectedCellData'); this._idh.doCheck('views'); diff --git a/packages/devextreme-angular/src/ui/scheduler/nested/view-dxi.ts b/packages/devextreme-angular/src/ui/scheduler/nested/view-dxi.ts index daf41c5c48c8..a4c1fd0fe77d 100644 --- a/packages/devextreme-angular/src/ui/scheduler/nested/view-dxi.ts +++ b/packages/devextreme-angular/src/ui/scheduler/nested/view-dxi.ts @@ -142,6 +142,14 @@ export class DxiSchedulerViewComponent extends CollectionNestedOption { this._setOption('groups', value); } + @Input() + get hiddenWeekDays(): Array { + return this._getOption('hiddenWeekDays'); + } + set hiddenWeekDays(value: Array) { + this._setOption('hiddenWeekDays', value); + } + @Input() get intervalCount(): number { return this._getOption('intervalCount'); diff --git a/packages/devextreme-metadata/make-angular-metadata.ts b/packages/devextreme-metadata/make-angular-metadata.ts index 8754150e81dc..dc49256b87db 100644 --- a/packages/devextreme-metadata/make-angular-metadata.ts +++ b/packages/devextreme-metadata/make-angular-metadata.ts @@ -65,6 +65,7 @@ Ng.makeMetadata({ removeMembers(/\/scheduler:dxSchedulerOptions\.editing\.popup/), removeMembers(/\/scheduler:dxSchedulerOptions\.resources\.icon/), removeMembers(/\/scheduler:.*\.snapToCellsMode/), + removeMembers(/\/scheduler:.*\.hiddenWeekDays/), removeMembers(/\/stepper:/), removeMembers(/\/speech_to_text:/), removeMembers(/\/tree_list:dxTreeListColumnButton.onClick/), diff --git a/packages/devextreme-react/src/scheduler.ts b/packages/devextreme-react/src/scheduler.ts index 1e62a2f2a4ff..f5ab778820b6 100644 --- a/packages/devextreme-react/src/scheduler.ts +++ b/packages/devextreme-react/src/scheduler.ts @@ -1454,6 +1454,7 @@ type IViewProps = React.PropsWithChildren<{ groupByDate?: boolean; groupOrientation?: Orientation; groups?: Array; + hiddenWeekDays?: Array; intervalCount?: number; maxAppointmentsPerCell?: CellAppointmentsLimit | number; name?: string | undefined; diff --git a/packages/devextreme-vue/src/scheduler.ts b/packages/devextreme-vue/src/scheduler.ts index 7d468cb8c2b6..548f4d2da72f 100644 --- a/packages/devextreme-vue/src/scheduler.ts +++ b/packages/devextreme-vue/src/scheduler.ts @@ -161,6 +161,7 @@ type AccessibleOptions = Pick>, height: [Number, String], + hiddenWeekDays: Array as PropType>, hint: String, indicatorUpdateInterval: Number, max: [Date, Number, String], @@ -331,6 +333,7 @@ const componentConfig = { "update:groupByDate": null, "update:groups": null, "update:height": null, + "update:hiddenWeekDays": null, "update:hint": null, "update:indicatorUpdateInterval": null, "update:max": null, @@ -1774,6 +1777,7 @@ const DxViewConfig = { "update:groupByDate": null, "update:groupOrientation": null, "update:groups": null, + "update:hiddenWeekDays": null, "update:intervalCount": null, "update:maxAppointmentsPerCell": null, "update:name": null, @@ -1800,6 +1804,7 @@ const DxViewConfig = { groupByDate: Boolean, groupOrientation: String as PropType, groups: Array as PropType>, + hiddenWeekDays: Array as PropType>, intervalCount: Number, maxAppointmentsPerCell: [String, Number] as PropType, name: String, diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index c9c7131060fc..f2ee28d53680 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -314,7 +314,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateOption('header', 'views', this.views); } break; - case 'hiddenDays': + case 'hiddenWeekDays': this.repaint(); break; case 'useDropDownViewSwitcher': diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 0d5a2de17e9b..27bb628740db 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -50,13 +50,12 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - const hiddenDays = this.option('hiddenDays' as keyof SafeSchedulerOptions) as - number[] | undefined; - this.views = getViews(views, hiddenDays); + const hiddenWeekDays = this.option('hiddenWeekDays'); + this.views = getViews(views, hiddenWeekDays); this.currentView = getCurrentView( this.option('currentView') ?? '', views, - hiddenDays, + hiddenWeekDays, ); } @@ -74,7 +73,7 @@ export class SchedulerOptionsBaseWidget extends Widget { switch (args.name) { case 'currentView': case 'views': - case 'hiddenDays' as K: + case 'hiddenWeekDays': this.updateViews(); break; default: diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index 5bc17302fd53..c0aa187f34bb 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -54,8 +54,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = { maxAppointmentsPerCell: 'auto', selectedCellData: [], groupByDate: false, - // @ts-expect-error - hiddenDays: undefined, + hiddenWeekDays: undefined, onAppointmentRendered: undefined, onAppointmentClick: undefined, onAppointmentDblClick: undefined, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index 6b20fcdf66ff..c247761b76dc 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -134,76 +134,76 @@ describe('views utils', () => { expect(getViews([input] as any)).toEqual([{ ...output, skippedDays: [0, 6] }]); }); - describe('hiddenDays', () => { + describe('hiddenWeekDays', () => { const getSkipped = ( views: any[], viewType: string, - globalHiddenDays?: number[], + globalHiddenWeekDays?: number[], ): number[] => { - const result = getViews(views, globalHiddenDays); + const result = getViews(views, globalHiddenWeekDays); const view = result.find((v) => v.type === viewType); return (view as any).skippedDays; }; - it('per-view hiddenDays on week → uses per-view value', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week')).toEqual([3]); + it('per-view hiddenWeekDays on week → uses per-view value', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); }); - it('per-view hiddenDays: [] on workWeek → overrides built-in default', () => { - expect(getSkipped([{ type: 'workWeek', hiddenDays: [] }], 'workWeek')).toEqual([]); + it('per-view hiddenWeekDays: [] on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); }); - it('per-view hiddenDays on workWeek → overrides built-in default', () => { - expect(getSkipped([{ type: 'workWeek', hiddenDays: [3] }], 'workWeek')).toEqual([3]); + it('per-view hiddenWeekDays on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek')).toEqual([3]); }); - it('global hiddenDays on workWeek → ignored, built-in default wins', () => { + it('global hiddenWeekDays on workWeek → ignored, built-in default wins', () => { expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([0, 6]); }); - it('global hiddenDays on week → applied', () => { + it('global hiddenWeekDays on week → applied', () => { expect(getSkipped(['week'], 'week', [3])).toEqual([3]); }); - it('global hiddenDays on month → applied', () => { + it('global hiddenWeekDays on month → applied', () => { expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); }); - it('global hiddenDays on timelineWeek → applied', () => { + it('global hiddenWeekDays on timelineWeek → applied', () => { expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); }); - it('global hiddenDays on timelineMonth → applied', () => { + it('global hiddenWeekDays on timelineMonth → applied', () => { expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); }); - it('global hiddenDays on day → ignored (unsupported view)', () => { + it('global hiddenWeekDays on day → ignored (unsupported view)', () => { expect(getSkipped(['day'], 'day', [3])).toEqual([]); }); - it('global hiddenDays on agenda → ignored (unsupported view)', () => { + it('global hiddenWeekDays on agenda → ignored (unsupported view)', () => { expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([]); }); - it('per-view hiddenDays dedupes duplicates', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); + it('per-view hiddenWeekDays dedupes duplicates', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); }); - it('per-view hiddenDays sorts ascending', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); + it('per-view hiddenWeekDays sorts ascending', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); }); - it('per-view hiddenDays filters out invalid values', () => { + it('per-view hiddenWeekDays filters out invalid values', () => { expect( - getSkipped([{ type: 'week', hiddenDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), + getSkipped([{ type: 'week', hiddenWeekDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), ).toEqual([3]); }); - it('hiddenDays covering all 7 days → falls back to [] and logs W1029', () => { + it('hiddenWeekDays covering all 7 days → falls back to [] and logs W1029', () => { const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); try { expect( - getSkipped([{ type: 'week', hiddenDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), + getSkipped([{ type: 'week', hiddenWeekDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), ).toEqual([]); expect(logSpy).toHaveBeenCalledWith('W1029'); } finally { @@ -211,15 +211,15 @@ describe('views utils', () => { } }); - it('global hiddenDays + per-view undefined on week → uses global', () => { + it('global hiddenWeekDays + per-view undefined on week → uses global', () => { expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); }); - it('global hiddenDays + per-view [3] on week → per-view wins', () => { - expect(getSkipped([{ type: 'week', hiddenDays: [3] }], 'week', [0, 6])).toEqual([3]); + it('global hiddenWeekDays + per-view [3] on week → per-view wins', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week', [0, 6])).toEqual([3]); }); - it('no hiddenDays anywhere on week → []', () => { + it('no hiddenWeekDays anywhere on week → []', () => { expect(getSkipped(['week'], 'week')).toEqual([]); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 39391ae6a614..170e69a017ba 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -17,7 +17,7 @@ const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ 'workWeek', 'timelineWorkWeek', ]); -const normalizeHiddenDays = ( +const normalizeHiddenWeekDays = ( days: readonly unknown[] | undefined, ): number[] | undefined => { if (!Array.isArray(days)) { @@ -35,19 +35,19 @@ const normalizeHiddenDays = ( const resolveSkippedDays = ( viewType: ViewType, - perViewHiddenDays: unknown, - globalHiddenDays: number[] | undefined, + perViewHiddenWeekDays: unknown, + globalHiddenWeekDays: number[] | undefined, viewDefault: number[], ): number[] => { - const perView = normalizeHiddenDays(perViewHiddenDays as readonly unknown[] | undefined); + const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays as readonly unknown[] | undefined); if (perView !== undefined) { return perView; } if (VIEWS_WITH_BUILTIN_SKIPPED.has(viewType)) { return viewDefault; } - if (globalHiddenDays !== undefined && VIEWS_SUPPORTING_HIDDEN_DAYS.has(viewType)) { - return normalizeHiddenDays(globalHiddenDays) ?? []; + if (globalHiddenWeekDays !== undefined && VIEWS_SUPPORTING_HIDDEN_DAYS.has(viewType)) { + return normalizeHiddenWeekDays(globalHiddenWeekDays) ?? []; } return viewDefault; }; @@ -58,7 +58,7 @@ const isExistedView = (view: NormalizedView | undefined): view is NormalizedView const normalizeView = ( view: RawViewType, - globalHiddenDays?: number[], + globalHiddenWeekDays?: number[], ): NormalizedView | undefined => { if (isObject(view)) { const viewType = view.type as ViewType; @@ -69,8 +69,8 @@ const normalizeView = ( const merged = extend({}, viewDefault, view) as NormalizedView; merged.skippedDays = resolveSkippedDays( viewType, - (view as { hiddenDays?: unknown }).hiddenDays, - globalHiddenDays, + view.hiddenWeekDays, + globalHiddenWeekDays, viewDefault.skippedDays, ); return merged; @@ -82,7 +82,7 @@ const normalizeView = ( const skippedDays = resolveSkippedDays( view as ViewType, undefined, - globalHiddenDays, + globalHiddenWeekDays, defaultView.skippedDays, ); if (skippedDays === defaultView.skippedDays) { @@ -93,18 +93,18 @@ const normalizeView = ( export const getViews = ( views: RawViewType[], - globalHiddenDays?: number[], + globalHiddenWeekDays?: number[], ): NormalizedView[] => views .filter(isKnownView) - .map((v) => normalizeView(v, globalHiddenDays)) + .map((v) => normalizeView(v, globalHiddenWeekDays)) .filter(isExistedView); export function getCurrentView( currentView: string | ViewType, views: RawViewType[], - globalHiddenDays?: number[], + globalHiddenWeekDays?: number[], ): NormalizedView { - const viewsProps = getViews(views, globalHiddenDays); + const viewsProps = getViews(views, globalHiddenWeekDays); const currentViewProps = viewsProps.find( (view) => [view.name, view.type].includes(currentView), ); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index b9cd28685a1f..87e0641a2c2c 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -6,7 +6,7 @@ import { ViewDataGeneratorMonth } from './m_view_data_generator_month'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; import { ViewDataGeneratorWorkWeek } from './m_view_data_generator_work_week'; -describe('ViewDataGenerator hiddenDays support', () => { +describe('ViewDataGenerator hiddenWeekDays support', () => { describe('isSkippedDate', () => { it('returns false when skippedDays is empty', () => { const gen = new ViewDataGenerator('week' as ViewType); diff --git a/packages/devextreme/js/ui/scheduler.d.ts b/packages/devextreme/js/ui/scheduler.d.ts index df45266e11c3..95c46bda15dc 100644 --- a/packages/devextreme/js/ui/scheduler.d.ts +++ b/packages/devextreme/js/ui/scheduler.d.ts @@ -685,6 +685,12 @@ export interface dxSchedulerOptions extends WidgetOptions { * @public */ firstDayOfWeek?: FirstDayOfWeek | undefined; + /** + * @docid + * @default undefined + * @public + */ + hiddenWeekDays?: Array; /** * @docid * @default true &for(desktop) @@ -1087,6 +1093,11 @@ export interface dxSchedulerOptions extends WidgetOptions { * @default undefined */ firstDayOfWeek?: FirstDayOfWeek | undefined; + /** + * @docid + * @default undefined + */ + hiddenWeekDays?: Array; /** * @docid * @default false diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index eedc323aa341..dce5034ed7a1 100644 --- a/packages/devextreme/js/ui/widget/ui.errors.js +++ b/packages/devextreme/js/ui/widget/ui.errors.js @@ -400,5 +400,5 @@ export default errorUtils(errors.ERROR_MESSAGES, { /** * @name ErrorsUIWidgets.W1029 */ - W1029: 'The "hiddenDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', + W1029: 'The "hiddenWeekDays" option cannot hide all days of the week. At least one day must remain visible. The option is ignored.', }); diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index 28f88f5ede00..eafdaa7fb4f6 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -26578,6 +26578,10 @@ declare module DevExpress.ui { * [descr:dxSchedulerOptions.firstDayOfWeek] */ firstDayOfWeek?: DevExpress.common.FirstDayOfWeek | undefined; + /** + * [descr:dxSchedulerOptions.hiddenWeekDays] + */ + hiddenWeekDays?: Array; /** * [descr:dxSchedulerOptions.focusStateEnabled] */ @@ -26590,10 +26594,6 @@ declare module DevExpress.ui { * [descr:dxSchedulerOptions.groups] */ groups?: Array; - /** - * [descr:dxSchedulerOptions.hiddenDays] - */ - hiddenDays?: Array; /** * [descr:dxSchedulerOptions.indicatorUpdateInterval] */ @@ -26913,6 +26913,10 @@ declare module DevExpress.ui { * [descr:dxSchedulerOptions.views.firstDayOfWeek] */ firstDayOfWeek?: DevExpress.common.FirstDayOfWeek | undefined; + /** + * [descr:dxSchedulerOptions.views.hiddenWeekDays] + */ + hiddenWeekDays?: Array; /** * [descr:dxSchedulerOptions.views.groupByDate] */ @@ -26925,10 +26929,6 @@ declare module DevExpress.ui { * [descr:dxSchedulerOptions.views.groups] */ groups?: Array; - /** - * [descr:dxSchedulerOptions.views.hiddenDays] - */ - hiddenDays?: Array; /** * [descr:dxSchedulerOptions.views.intervalCount] */ From 97f1fe8a725f1038f03cc9369305e15e60c80610 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 15:38:48 +0200 Subject: [PATCH 07/30] feat: add storybook --- .../SchedulerHiddenWeekDays.stories.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 apps/react-storybook/stories/scheduler/SchedulerHiddenWeekDays.stories.tsx diff --git a/apps/react-storybook/stories/scheduler/SchedulerHiddenWeekDays.stories.tsx b/apps/react-storybook/stories/scheduler/SchedulerHiddenWeekDays.stories.tsx new file mode 100644 index 000000000000..169e2826b67b --- /dev/null +++ b/apps/react-storybook/stories/scheduler/SchedulerHiddenWeekDays.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/react-webpack5"; +import dxScheduler from "devextreme/ui/scheduler"; +import { wrapDxWithReact } from "../utils"; +import { data, resources } from "./data"; + +const Scheduler = wrapDxWithReact(dxScheduler); + +const viewNames = ['day', 'week', 'workWeek', 'month', 'agenda', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth']; + +const meta: Meta = { + title: 'Components/Scheduler/HiddenWeekDays', + component: Scheduler, + parameters: { layout: 'padded' }, +}; + +export default meta; + +type Story = StoryObj; + +export const Overview: Story = { + args: { + height: 600, + views: viewNames, + currentView: 'week', + currentDate: new Date(2021, 3, 26), + firstDayOfWeek: 0, + startDayHour: 9, + endDayHour: 22, + dataSource: data, + resources, + hiddenWeekDays: [], + }, + argTypes: { + height: { control: 'number' }, + views: { control: 'object' }, + hiddenWeekDays: { control: 'object' }, + currentView: { control: 'select', options: viewNames }, + }, +}; From 4904ffab1979e4c04f67009d88bab6575e488012 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 16:12:56 +0200 Subject: [PATCH 08/30] refactor: fix typing --- .../__internal/scheduler/header/m_header.ts | 2 +- .../scheduler/utils/options/utils.test.ts | 39 ++++++++++++------- .../scheduler/utils/options/utils.ts | 6 +-- .../scheduler/workspaces/m_work_space.ts | 3 +- .../workspaces/view_model/m_types.ts | 1 + .../view_model/m_view_data_generator.ts | 2 +- 6 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_header.ts b/packages/devextreme/js/__internal/scheduler/header/m_header.ts index 2203fbfe7bdf..927546baf8bf 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_header.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_header.ts @@ -66,7 +66,7 @@ export class SchedulerHeader extends Widget { firstDayOfWeek, intervalCount: currentView.intervalCount, agendaDuration: currentView.agendaDuration, - skippedDays: (currentView as { skippedDays?: number[] }).skippedDays, + skippedDays: currentView.skippedDays, }; } diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index c247761b76dc..7ba3f281c5c0 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -3,6 +3,7 @@ import { } from '@jest/globals'; import errors from '@js/ui/widget/ui.errors'; +import type { RawViewType, ViewType } from './types'; import { getCurrentView, getViewOption, @@ -14,11 +15,13 @@ import { describe('views utils', () => { describe('getViews', () => { it('should filter view with incorrect name', () => { - expect(getViews(['unknown'] as any)).toEqual([]); + // @ts-expect-error intentionally pass an unsupported view name + expect(getViews(['unknown'])).toEqual([]); }); it('should filter view with incorrect type', () => { - expect(getViews([{ type: 'unknown' }] as any)).toEqual([]); + // @ts-expect-error intentionally pass an unsupported view type + expect(getViews([{ type: 'unknown' }])).toEqual([]); }); it('should not override view options by default options', () => { @@ -29,7 +32,7 @@ describe('views utils', () => { name: 'MyDay', groups: ['a', 'b'], }; - expect(getViews([input] as any)).toEqual([{ ...input, skippedDays: [] }]); + expect(getViews([input as RawViewType])).toEqual([{ ...input, skippedDays: [] }]); }); it.each([ @@ -111,7 +114,7 @@ describe('views utils', () => { type: 'agenda', }, }])('should return normalized $input.type view', ({ input, output }) => { - expect(getViews([input] as any)).toEqual([{ ...output, skippedDays: [] }]); + expect(getViews([input as RawViewType])).toEqual([{ ...output, skippedDays: [] }]); }); it.each([ @@ -131,18 +134,18 @@ describe('views utils', () => { }, }, ])('should return normalized $input.type view', ({ input, output }) => { - expect(getViews([input] as any)).toEqual([{ ...output, skippedDays: [0, 6] }]); + expect(getViews([input as RawViewType])).toEqual([{ ...output, skippedDays: [0, 6] }]); }); describe('hiddenWeekDays', () => { const getSkipped = ( - views: any[], - viewType: string, + views: RawViewType[], + viewType: ViewType, globalHiddenWeekDays?: number[], ): number[] => { const result = getViews(views, globalHiddenWeekDays); const view = result.find((v) => v.type === viewType); - return (view as any).skippedDays; + return view?.skippedDays ?? []; }; it('per-view hiddenWeekDays on week → uses per-view value', () => { @@ -195,7 +198,10 @@ describe('views utils', () => { it('per-view hiddenWeekDays filters out invalid values', () => { expect( - getSkipped([{ type: 'week', hiddenWeekDays: [7, -1, 1.5, 'x', null, 3] as any }], 'week'), + getSkipped([ + // @ts-expect-error intentionally pass invalid values to verify runtime filtering + { type: 'week', hiddenWeekDays: [7, -1, 1.5, 'x', null, 3] }, + ], 'week'), ).toEqual([3]); }); @@ -273,11 +279,16 @@ describe('views utils', () => { }); it('should return first known view if wrong current view requested', () => { - expect(getCurrentView('blabla', [{ - type: 'blabla', - name: 'blabla', - unknown: 'incorrect view', - } as any])).toEqual({ + expect(getCurrentView( + 'blabla', + [ + { + type: 'blabla', + name: 'blabla', + unknown: 'incorrect view', + } as unknown as RawViewType, + ], + )).toEqual({ groupOrientation: 'horizontal', intervalCount: 1, type: 'day', diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 170e69a017ba..de3ee9655246 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -18,7 +18,7 @@ const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ ]); const normalizeHiddenWeekDays = ( - days: readonly unknown[] | undefined, + days: unknown, ): number[] | undefined => { if (!Array.isArray(days)) { return undefined; @@ -39,7 +39,7 @@ const resolveSkippedDays = ( globalHiddenWeekDays: number[] | undefined, viewDefault: number[], ): number[] => { - const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays as readonly unknown[] | undefined); + const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays); if (perView !== undefined) { return perView; } @@ -88,7 +88,7 @@ const normalizeView = ( if (skippedDays === defaultView.skippedDays) { return defaultView; } - return { ...defaultView, skippedDays } as NormalizedView; + return { ...defaultView, skippedDays }; }; export const getViews = ( diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index e66a70ef97c8..ee3ba83d7a2c 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -204,6 +204,7 @@ type WorkspaceOptionsInternal = Omit & { hoursInterval: number; startDayHour: number; endDayHour: number; + skippedDays?: number[]; }; class SchedulerWorkSpace extends Widget { private viewDataProviderValue: any; @@ -908,7 +909,7 @@ class SchedulerWorkSpace extends Widget { startDate: this.option('startDate'), firstDayOfWeek: this.option('firstDayOfWeek'), showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'), - skippedDays: (this.option('skippedDays' as any) as number[] | undefined) ?? [], + skippedDays: this.option('skippedDays'), ...this.virtualScrollingDispatcher.getRenderState(), }; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts index 6662e5e7a9d6..5f4509955bdb 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts @@ -16,6 +16,7 @@ interface CommonOptions extends CountGenerationConfig { viewOffset: number; hoursInterval: number; viewType: ViewType; + skippedDays?: number[]; cellCount: number; isProvideVirtualCellsWidth: boolean; isGenerateTimePanelData?: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 6726b774cc74..4fdbe52a1032 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -130,7 +130,7 @@ export class ViewDataGenerator { hoursInterval, } = options; - this.skippedDays = ((options as any)?.skippedDays as number[] | undefined) ?? []; + this.skippedDays = options.skippedDays ?? []; this.setVisibilityDates(options); this.setHiddenInterval(startDayHour, endDayHour, hoursInterval); From a4b076732e4748f2e8710502017e8b5648bc0467 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 16:20:51 +0200 Subject: [PATCH 09/30] refactor: fix typing --- .../js/__internal/scheduler/workspaces/m_work_space.ts | 3 ++- .../js/__internal/scheduler/workspaces/view_model/m_types.ts | 2 +- .../scheduler/workspaces/view_model/m_view_data_generator.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index ee3ba83d7a2c..dbcfade80a11 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -204,7 +204,7 @@ type WorkspaceOptionsInternal = Omit & { hoursInterval: number; startDayHour: number; endDayHour: number; - skippedDays?: number[]; + skippedDays: number[]; }; class SchedulerWorkSpace extends Widget { private viewDataProviderValue: any; @@ -2293,6 +2293,7 @@ class SchedulerWorkSpace extends Widget { groupOrientation: 'horizontal', selectedCellData: [], groupByDate: false, + skippedDays: [], scrolling: { mode: 'standard', }, diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts index 5f4509955bdb..aaa7bce9f59d 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts @@ -16,7 +16,7 @@ interface CommonOptions extends CountGenerationConfig { viewOffset: number; hoursInterval: number; viewType: ViewType; - skippedDays?: number[]; + skippedDays: number[]; cellCount: number; isProvideVirtualCellsWidth: boolean; isGenerateTimePanelData?: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 4fdbe52a1032..501db06c5c02 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -130,7 +130,7 @@ export class ViewDataGenerator { hoursInterval, } = options; - this.skippedDays = options.skippedDays ?? []; + this.skippedDays = options.skippedDays; this.setVisibilityDates(options); this.setHiddenInterval(startDayHour, endDayHour, hoursInterval); From f4ac36967e95f9fb51adcaa6e2954fbb9034cce3 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 16:25:53 +0200 Subject: [PATCH 10/30] feat: create helper for skipped days --- .../js/__internal/scheduler/header/m_utils.ts | 38 +++----------- .../scheduler/utils/skipped_days.ts | 51 +++++++++++++++++++ .../view_model/m_view_data_generator.ts | 15 +++--- 3 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 16798ede4474..2266fcb9f567 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -7,6 +7,10 @@ import type { BaseFormat } from '@ts/core/localization/date'; import { camelize } from '@ts/core/utils/m_inflector'; import type { IntervalOptions, Step } from '@ts/scheduler/header/types'; import type { NormalizedView, RawViewType, ViewType } from '@ts/scheduler/utils/options/types'; +import { + getDateAfterVisibleWeek, + getFirstVisibleDate, +} from '@ts/scheduler/utils/skipped_days'; import type { Direction } from './constants'; @@ -48,8 +52,6 @@ const nextMonth = (date: Date): Date => { const isWeekend = (date: Date): boolean => [SATURDAY_INDEX, SUNDAY_INDEX].includes(date.getDay()); -const isSkippedDay = (date: Date, skippedDays: number[]): boolean => skippedDays.includes(date.getDay()); - const getWorkWeekStart = (firstDayOfWeek: Date): Date => { let date = new Date(firstDayOfWeek); while (isWeekend(date)) { @@ -59,14 +61,6 @@ const getWorkWeekStart = (firstDayOfWeek: Date): Date => { return date; }; -const getFirstVisibleDay = (start: Date, skippedDays: number[]): Date => { - let date = new Date(start); - while (isSkippedDay(date, skippedDays)) { - date = nextDay(date); - } - return date; -}; - const getDateAfterWorkWeek = (workWeekStart: Date): Date => { let date = new Date(workWeekStart); @@ -82,22 +76,6 @@ const getDateAfterWorkWeek = (workWeekStart: Date): Date => { return date; }; -const getDateAfterVisibleWeek = (start: Date, skippedDays: number[]): Date => { - const visibleCount = 7 - skippedDays.length; - if (visibleCount <= 0) { - return new Date(start); - } - let date = new Date(start); - let visited = 0; - while (visited < visibleCount) { - if (!isSkippedDay(date, skippedDays)) { - visited += 1; - } - date = nextDay(date); - } - return date; -}; - const nextAgendaStart = ( date: Date, agendaDuration: number, @@ -115,7 +93,7 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { case 'week': { const weekStart = getPeriodStart(date, step, false, firstDayOfWeek) as Date; if (skippedDays && skippedDays.length > 0) { - return getFirstVisibleDay(weekStart, skippedDays); + return getFirstVisibleDate(weekStart, skippedDays, nextDay); } return weekStart; } @@ -137,7 +115,7 @@ const getPeriodEndDate = ( const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), week: () => (skippedDays && skippedDays.length > 0 - ? getDateAfterVisibleWeek(currentPeriodStartDate, skippedDays) + ? getDateAfterVisibleWeek(currentPeriodStartDate, skippedDays, nextDay) : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate), @@ -159,9 +137,7 @@ const getNextPeriodStartDate = ( date = nextDay(date); } } else if (step === 'week' && skippedDays && skippedDays.length > 0) { - while (isSkippedDay(date, skippedDays)) { - date = nextDay(date); - } + date = getFirstVisibleDate(date, skippedDays, nextDay); } return date; diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts new file mode 100644 index 000000000000..ecb31eaadfd4 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -0,0 +1,51 @@ +export const isDateSkipped = (date: Date, skippedDays: number[]): boolean => ( + skippedDays.includes(date.getDay()) +); + +export const getVisibleDaysOfWeek = ( + firstDayOfWeek: number, + skippedDays: number[], +): number[] => { + const result: number[] = []; + for (let count = 0; count < 7; count += 1) { + const dayOfWeek = (firstDayOfWeek + count) % 7; + if (!skippedDays.includes(dayOfWeek)) { + result.push(dayOfWeek); + } + } + + return result; +}; + +export const getFirstVisibleDate = ( + start: Date, + skippedDays: number[], + nextDate: (date: Date) => Date, +): Date => { + let date = new Date(start); + while (isDateSkipped(date, skippedDays)) { + date = nextDate(date); + } + return date; +}; + +export const getDateAfterVisibleWeek = ( + start: Date, + skippedDays: number[], + nextDate: (date: Date) => Date, +): Date => { + const visibleCount = 7 - skippedDays.length; + if (visibleCount <= 0) { + return new Date(start); + } + + let date = new Date(start); + let visited = 0; + while (visited < visibleCount) { + if (!isDateSkipped(date, skippedDays)) { + visited += 1; + } + date = nextDate(date); + } + return date; +}; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 501db06c5c02..a3f96c50cfb9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -18,6 +18,10 @@ import { import type { ViewDataMap, ViewType } from '../../types'; import { VIEWS } from '../../utils/options/constants_view'; import { getAllGroupValues } from '../../utils/resource_manager/group_utils'; +import { + getVisibleDaysOfWeek, + isDateSkipped, +} from '../../utils/skipped_days'; import type { ViewCellDataSimple, ViewCellGeneratedData, @@ -65,14 +69,7 @@ export class ViewDataGenerator { } public getVisibleDaysOfWeek(firstDayOfWeek: number): number[] { - const rotated: number[] = []; - for (let count = 0; count < 7; count += 1) { - const dayOfWeek = (firstDayOfWeek + count) % 7; - if (!this.skippedDays.includes(dayOfWeek)) { - rotated.push(dayOfWeek); - } - } - return rotated; + return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); } protected getVisibleDayOffset( @@ -101,7 +98,7 @@ export class ViewDataGenerator { } public isSkippedDate(date: Date): boolean { - return this.skippedDays.includes(date.getDay()); + return isDateSkipped(date, this.skippedDays); } // eslint-disable-next-line @typescript-eslint/no-unused-vars From 91d780daf402944f2571d19a5d009e6b47fa5930 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 17:05:05 +0200 Subject: [PATCH 11/30] fix: fix calculating hiddenDays for timeline --- .../view_model/m_view_data_generator.test.ts | 86 +++++++++++-------- .../view_model/m_view_data_generator.ts | 9 +- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index 87e0641a2c2c..3e7871609da8 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -93,43 +93,56 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { rowIndex: number, columnIndex: number, firstDayOfWeek: number, + cellCountInDay: number, ): number => (g as unknown as { - getVisibleDayOffset: (r: number, c: number, firstDay: number) => number; - }).getVisibleDayOffset(rowIndex, columnIndex, firstDayOfWeek); + getVisibleDayOffset: (r: number, c: number, firstDay: number, cellCount: number) => number; + }).getVisibleDayOffset(rowIndex, columnIndex, firstDayOfWeek, cellCountInDay); it('zero offset for empty skippedDays', () => { gen.skippedDays = []; - expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 5, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 0, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 5, 0, 1)).toBe(0); }); it('week with [0,6], firstDayOfWeek=1 (Mon): col 0..4 → 0 offset, col 5 → +2', () => { gen.skippedDays = [0, 6]; [0, 1, 2, 3, 4].forEach((col) => { - expect(callGetVisibleDayOffset(gen, 0, col, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, col, 1, 1)).toBe(0); }); - expect(callGetVisibleDayOffset(gen, 0, 5, 1)).toBe(2); - expect(callGetVisibleDayOffset(gen, 0, 9, 1)).toBe(2); - expect(callGetVisibleDayOffset(gen, 0, 10, 1)).toBe(4); + expect(callGetVisibleDayOffset(gen, 0, 5, 1, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 9, 1, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 10, 1, 1)).toBe(4); }); it('week with [3] (skip Wed), firstDayOfWeek=0 (Sun): col 3 → +1 to skip Wed', () => { gen.skippedDays = [3]; - expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 1, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 2, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 3, 0)).toBe(1); - expect(callGetVisibleDayOffset(gen, 0, 4, 0)).toBe(1); - expect(callGetVisibleDayOffset(gen, 0, 5, 0)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 0, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 1, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 2, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 3, 0, 1)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 4, 0, 1)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 5, 0, 1)).toBe(1); }); it('week with [1,3,5] (skip Mon, Wed, Fri), firstDayOfWeek=0', () => { gen.skippedDays = [1, 3, 5]; - expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 1, 0)).toBe(1); - expect(callGetVisibleDayOffset(gen, 0, 2, 0)).toBe(2); - expect(callGetVisibleDayOffset(gen, 0, 3, 0)).toBe(3); - expect(callGetVisibleDayOffset(gen, 0, 4, 0)).toBe(3); + expect(callGetVisibleDayOffset(gen, 0, 0, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 1, 0, 1)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 2, 0, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 3, 0, 1)).toBe(3); + expect(callGetVisibleDayOffset(gen, 0, 4, 0, 1)).toBe(3); + }); + + it('timeline-like layout with multiple cells in day uses day index', () => { + gen.skippedDays = [0, 6]; + // 2 cells per day, first visible week day is Monday (firstDayOfWeek=1) + // Both cells of the first day must have the same offset. + expect(callGetVisibleDayOffset(gen, 0, 0, 1, 2)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 1, 1, 2)).toBe(0); + // The first cell of next visible day still has zero offset. + expect(callGetVisibleDayOffset(gen, 0, 2, 1, 2)).toBe(0); + // After 5 visible days (10 cells), the next day jumps over weekend (+2 days). + expect(callGetVisibleDayOffset(gen, 0, 10, 1, 2)).toBe(2); }); }); @@ -141,35 +154,36 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { rowIndex: number, columnIndex: number, firstDayOfWeek: number, + cellCountInDay: number, ): number => (g as unknown as { - getVisibleDayOffset: (r: number, c: number, firstDay: number) => number; - }).getVisibleDayOffset(rowIndex, columnIndex, firstDayOfWeek); + getVisibleDayOffset: (r: number, c: number, firstDay: number, cellCount: number) => number; + }).getVisibleDayOffset(rowIndex, columnIndex, firstDayOfWeek, cellCountInDay); it('returns 0 for empty skippedDays', () => { gen.skippedDays = []; - expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 3, 5, 0)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 0, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 3, 5, 0, 1)).toBe(0); }); it('month with [0,6], firstDayOfWeek=1: row=1 col=0 → +2 (jumps over Sat+Sun)', () => { gen.skippedDays = [0, 6]; - expect(callGetVisibleDayOffset(gen, 0, 0, 1)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 4, 1)).toBe(0); - expect(callGetVisibleDayOffset(gen, 1, 0, 1)).toBe(2); - expect(callGetVisibleDayOffset(gen, 1, 4, 1)).toBe(2); - expect(callGetVisibleDayOffset(gen, 2, 0, 1)).toBe(4); + expect(callGetVisibleDayOffset(gen, 0, 0, 1, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 4, 1, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 1, 0, 1, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 1, 4, 1, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 2, 0, 1, 1)).toBe(4); }); it('month with [3] (skip Wed), firstDayOfWeek=0: visible days = Sun,Mon,Tue,Thu,Fri,Sat', () => { gen.skippedDays = [3]; - expect(callGetVisibleDayOffset(gen, 0, 0, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 2, 0)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 3, 0)).toBe(1); - expect(callGetVisibleDayOffset(gen, 0, 5, 0)).toBe(1); - expect(callGetVisibleDayOffset(gen, 1, 0, 0)).toBe(1); - expect(callGetVisibleDayOffset(gen, 1, 3, 0)).toBe(2); - expect(callGetVisibleDayOffset(gen, 1, 5, 0)).toBe(2); - expect(callGetVisibleDayOffset(gen, 2, 0, 0)).toBe(2); + expect(callGetVisibleDayOffset(gen, 0, 0, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 2, 0, 1)).toBe(0); + expect(callGetVisibleDayOffset(gen, 0, 3, 0, 1)).toBe(1); + expect(callGetVisibleDayOffset(gen, 0, 5, 0, 1)).toBe(1); + expect(callGetVisibleDayOffset(gen, 1, 0, 0, 1)).toBe(1); + expect(callGetVisibleDayOffset(gen, 1, 3, 0, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 1, 5, 0, 1)).toBe(2); + expect(callGetVisibleDayOffset(gen, 2, 0, 0, 1)).toBe(2); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index a3f96c50cfb9..ccbf0c4b2f62 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -76,6 +76,7 @@ export class ViewDataGenerator { rowIndex: number, columnIndex: number, firstDayOfWeek: number, + cellCountInDay: number, ): number { const rotated = this.getVisibleDaysOfWeek(firstDayOfWeek); const visibleCount = rotated.length; @@ -89,10 +90,11 @@ export class ViewDataGenerator { + ((targetDayOfWeek - firstDayOfWeek + 7) % 7); return actualDayOffset - naiveDayOffset; } - const week = Math.floor(columnIndex / visibleCount); - const idxInWeek = columnIndex % visibleCount; + const dayIndex = Math.floor(columnIndex / cellCountInDay); + const week = Math.floor(dayIndex / visibleCount); + const idxInWeek = dayIndex % visibleCount; const targetDayOfWeek = rotated[idxInWeek]; - const naiveDayOffset = columnIndex; + const naiveDayOffset = dayIndex; const actualDayOffset = week * 7 + ((targetDayOfWeek - firstDayOfWeek + 7) % 7); return actualDayOffset - naiveDayOffset; } @@ -582,6 +584,7 @@ export class ViewDataGenerator { rowIndex, columnIndex, this.getFirstDayOfWeek(firstDayOfWeek) ?? 0, + cellCountInDay, ) * toMs('day'); } else { offsetByCount = 0; From b1263af3d8b9d16d5c1ecfc155f5fb985a4f4dbe Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 17:35:07 +0200 Subject: [PATCH 12/30] fix: fix hiddenDays for timelineMonth --- .../view_model/m_view_data_generator.test.ts | 26 +++++++++++++++++++ .../view_model/m_view_data_generator.ts | 10 ++++++- .../m_view_data_generator_timeline_month.ts | 24 ++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index 3e7871609da8..f28d31d123d5 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from '@jest/globals'; import type { ViewType } from '../../types'; import { ViewDataGenerator } from './m_view_data_generator'; import { ViewDataGeneratorMonth } from './m_view_data_generator_month'; +import { ViewDataGeneratorTimelineMonth } from './m_view_data_generator_timeline_month'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; import { ViewDataGeneratorWorkWeek } from './m_view_data_generator_work_week'; @@ -206,4 +207,29 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { expect(gen.getCellCount()).toBe(6); }); }); + + describe('TimelineMonth hiddenWeekDays support', () => { + it('maps next visible column to Monday when start is Friday and weekends are skipped', () => { + const gen = new ViewDataGeneratorTimelineMonth('timelineMonth' as ViewType); + gen.skippedDays = [0, 6]; + + const startViewDate = new Date(2026, 4, 1, 0, 0); // Friday + const options = { + startViewDate, + startDayHour: 0, + endDayHour: 24, + hoursInterval: 1, + interval: 24 * 60 * 60 * 1000, + firstDayOfWeek: 1, // Monday + intervalCount: 1, + viewOffset: 0, + currentDate: new Date(2026, 4, 15), + viewType: 'timelineMonth' as ViewType, + }; + + const date = gen.getDateByCellIndices(options, 0, 1); + expect(date.getDay()).toBe(1); + expect(date.getDate()).toBe(4); + }); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index ccbf0c4b2f62..f1e66faabb69 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -72,6 +72,14 @@ export class ViewDataGenerator { return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); } + protected getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + startViewDate: Date, + ): number { + return this.getFirstDayOfWeek(firstDayOfWeekOption) ?? 0; + } + protected getVisibleDayOffset( rowIndex: number, columnIndex: number, @@ -583,7 +591,7 @@ export class ViewDataGenerator { offsetByCount = this.getVisibleDayOffset( rowIndex, columnIndex, - this.getFirstDayOfWeek(firstDayOfWeek) ?? 0, + this.getSkippedDaysAnchorDay(firstDayOfWeek, startViewDate), cellCountInDay, ) * toMs('day'); } else { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index dd0f45ed6e7e..6d971675c663 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts @@ -7,6 +7,17 @@ import { ViewDataGenerator } from './m_view_data_generator'; const toMs = dateUtils.dateToMilliseconds; export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { + protected usesWeeklyDayLayout(): boolean { + return true; + } + + protected getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + startViewDate: Date, + ): number { + return startViewDate.getDay(); + } + calculateEndDate(startDate, interval, endDayHour) { return setOptionHour(startDate, endDayHour); } @@ -15,6 +26,10 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return toMs('day'); } + getCellCountInDay() { + return 1; + } + protected calculateStartViewDate(options: any) { return timelineMonthUtils.calculateStartViewDate( options.currentDate, @@ -30,7 +45,14 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { let cellCount = 0; for (let i = 1; i <= intervalCount; i++) { - cellCount += new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 0).getDate(); + const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 0); + const daysInMonth = monthDate.getDate(); + for (let day = 1; day <= daysInMonth; day += 1) { + const date = new Date(monthDate.getFullYear(), monthDate.getMonth(), day); + if (!this.skippedDays.includes(date.getDay())) { + cellCount += 1; + } + } } return cellCount; From 2a11d146c5679703c81913dabbb9ca8ab0b512c7 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 8 Apr 2026 17:50:43 +0200 Subject: [PATCH 13/30] refactor: optimize ts --- .../js/__internal/scheduler/header/m_utils.ts | 10 +++--- .../js/__internal/scheduler/header/types.ts | 2 +- .../view_model/m_view_data_generator.ts | 20 +++++++---- .../m_view_data_generator_timeline_month.ts | 34 ++++++++++++------- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 2266fcb9f567..01095e4227e9 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -92,7 +92,7 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { return getPeriodStart(date, step, false, firstDayOfWeek) as Date; case 'week': { const weekStart = getPeriodStart(date, step, false, firstDayOfWeek) as Date; - if (skippedDays && skippedDays.length > 0) { + if (skippedDays.length > 0) { return getFirstVisibleDate(weekStart, skippedDays, nextDay); } return weekStart; @@ -110,11 +110,11 @@ const getPeriodEndDate = ( currentPeriodStartDate: Date, step: Step, agendaDuration: number, - skippedDays?: number[], + skippedDays: number[], ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), - week: () => (skippedDays && skippedDays.length > 0 + week: () => (skippedDays.length > 0 ? getDateAfterVisibleWeek(currentPeriodStartDate, skippedDays, nextDay) : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), @@ -128,7 +128,7 @@ const getPeriodEndDate = ( const getNextPeriodStartDate = ( currentPeriodEndDate: Date, step: Step, - skippedDays?: number[], + skippedDays: number[], ): Date => { let date = addMS(currentPeriodEndDate); @@ -136,7 +136,7 @@ const getNextPeriodStartDate = ( while (isWeekend(date)) { date = nextDay(date); } - } else if (step === 'week' && skippedDays && skippedDays.length > 0) { + } else if (step === 'week' && skippedDays.length > 0) { date = getFirstVisibleDate(date, skippedDays, nextDay); } diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 181e6ded7193..061118859c4b 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -30,7 +30,7 @@ export interface IntervalOptions { firstDayOfWeek?: number; intervalCount: number; agendaDuration?: number; - skippedDays?: number[]; + skippedDays: number[]; } export interface HeaderCalendarOptions { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index f1e66faabb69..d10b1f2d7d4a 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -30,6 +30,7 @@ import type { } from './m_types'; const toMs = dateUtils.dateToMilliseconds; +type SkippedDaysAnchorKind = 'firstDayOfWeek' | 'startViewDate'; export class ViewDataGenerator { protected baseDaysInInterval = 1; @@ -72,21 +73,28 @@ export class ViewDataGenerator { return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); } - protected getSkippedDaysAnchorDay( + protected getSkippedDaysAnchorKind(): SkippedDaysAnchorKind { + return 'firstDayOfWeek'; + } + + private getSkippedDaysAnchorDay( firstDayOfWeekOption: number | undefined, - // eslint-disable-next-line @typescript-eslint/no-unused-vars startViewDate: Date, ): number { + if (this.getSkippedDaysAnchorKind() === 'startViewDate') { + return startViewDate.getDay(); + } + return this.getFirstDayOfWeek(firstDayOfWeekOption) ?? 0; } protected getVisibleDayOffset( rowIndex: number, columnIndex: number, - firstDayOfWeek: number, + anchorDay: number, cellCountInDay: number, ): number { - const rotated = this.getVisibleDaysOfWeek(firstDayOfWeek); + const rotated = this.getVisibleDaysOfWeek(anchorDay); const visibleCount = rotated.length; if (visibleCount === 0) { return 0; @@ -95,7 +103,7 @@ export class ViewDataGenerator { const targetDayOfWeek = rotated[columnIndex]; const naiveDayOffset = rowIndex * visibleCount + columnIndex; const actualDayOffset = rowIndex * 7 - + ((targetDayOfWeek - firstDayOfWeek + 7) % 7); + + ((targetDayOfWeek - anchorDay + 7) % 7); return actualDayOffset - naiveDayOffset; } const dayIndex = Math.floor(columnIndex / cellCountInDay); @@ -103,7 +111,7 @@ export class ViewDataGenerator { const idxInWeek = dayIndex % visibleCount; const targetDayOfWeek = rotated[idxInWeek]; const naiveDayOffset = dayIndex; - const actualDayOffset = week * 7 + ((targetDayOfWeek - firstDayOfWeek + 7) % 7); + const actualDayOffset = week * 7 + ((targetDayOfWeek - anchorDay + 7) % 7); return actualDayOffset - naiveDayOffset; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index 6d971675c663..97dacb53b125 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts @@ -2,6 +2,7 @@ import dateUtils from '@js/core/utils/date'; import { setOptionHour, timelineMonthUtils } from '@ts/scheduler/r1/utils/index'; import timezoneUtils from '../../m_utils_time_zone'; +import type { CountGenerationConfig } from '../../types'; import { ViewDataGenerator } from './m_view_data_generator'; const toMs = dateUtils.dateToMilliseconds; @@ -11,14 +12,11 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return true; } - protected getSkippedDaysAnchorDay( - firstDayOfWeekOption: number | undefined, - startViewDate: Date, - ): number { - return startViewDate.getDay(); + protected getSkippedDaysAnchorKind(): 'startViewDate' { + return 'startViewDate'; } - calculateEndDate(startDate, interval, endDayHour) { + calculateEndDate(startDate: Date, interval: number, endDayHour: number): Date { return setOptionHour(startDate, endDayHour); } @@ -26,25 +24,32 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return toMs('day'); } - getCellCountInDay() { + getCellCountInDay(): number { return 1; } - protected calculateStartViewDate(options: any) { + protected calculateStartViewDate( + options: { + currentDate: Date; + startDayHour: number; + intervalCount: number; + startDate?: Date; + }, + ): Date { return timelineMonthUtils.calculateStartViewDate( options.currentDate, options.startDayHour, - options.startDate, + options.startDate ?? options.currentDate, options.intervalCount, ); } - getCellCount(options) { + getCellCount(options: CountGenerationConfig): number { const { intervalCount } = options; const currentDate = new Date(options.currentDate); let cellCount = 0; - for (let i = 1; i <= intervalCount; i++) { + for (let i = 1; i <= intervalCount; i += 1) { const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 0); const daysInMonth = monthDate.getDate(); for (let day = 1; day <= daysInMonth; day += 1) { @@ -58,11 +63,14 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return cellCount; } - setHiddenInterval() { + setHiddenInterval(): void { this.hiddenInterval = 0; } - protected getCellEndDate(cellStartDate: Date, options: any): Date { + protected getCellEndDate( + cellStartDate: Date, + options: { startDayHour: number; endDayHour: number }, + ): Date { const { startDayHour, endDayHour } = options; const durationMs = (endDayHour - startDayHour) * toMs('hour'); return timezoneUtils.addOffsetsWithoutDST(cellStartDate, durationMs); From 19f3964d1764c1e48c4f4cb8455f82e91e5f7ef9 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 12:13:54 +0200 Subject: [PATCH 14/30] fix: fix bug for view_generator for workWeek --- .../view_model/m_view_data_generator.test.ts | 16 ++++++++++++++++ .../m_view_data_generator_work_week.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index f28d31d123d5..ae4f4469e4ab 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -29,6 +29,22 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { expect(gen.isSkippedDate(new Date(2026, 3, 12))).toBe(true); expect(gen.isSkippedDate(new Date(2026, 3, 13))).toBe(false); }); + + it('workWeek view skips weekends by default', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + expect(gen.isSkippedDate(new Date(2026, 3, 11))).toBe(true); + expect(gen.isSkippedDate(new Date(2026, 3, 12))).toBe(true); + expect(gen.isSkippedDate(new Date(2026, 3, 13))).toBe(false); + }); + + it('workWeek view respects custom skippedDays override', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + gen.skippedDays = [1, 2]; + expect(gen.isSkippedDate(new Date(2026, 3, 11))).toBe(false); + expect(gen.isSkippedDate(new Date(2026, 3, 12))).toBe(false); + expect(gen.isSkippedDate(new Date(2026, 3, 13))).toBe(true); + expect(gen.isSkippedDate(new Date(2026, 3, 14))).toBe(true); + }); }); describe('daysInInterval getter', () => { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index e818624cd675..1e99a186cd1e 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -4,6 +4,8 @@ import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { protected baseDaysInInterval = 5; + public skippedDays: number[] = [0, 6]; + protected calculateStartViewDate(options) { return workWeekUtils.calculateStartViewDate( options.currentDate, From d414219ae93a861a6f3a7a86ea79caebc9b03951 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 12:28:51 +0200 Subject: [PATCH 15/30] fix: fix test --- .../js/__internal/scheduler/workspaces/view_model/m_types.ts | 2 +- .../scheduler/workspaces/view_model/m_view_data_generator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts index aaa7bce9f59d..5f4509955bdb 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts @@ -16,7 +16,7 @@ interface CommonOptions extends CountGenerationConfig { viewOffset: number; hoursInterval: number; viewType: ViewType; - skippedDays: number[]; + skippedDays?: number[]; cellCount: number; isProvideVirtualCellsWidth: boolean; isGenerateTimePanelData?: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index d10b1f2d7d4a..7047a80d4302 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -145,7 +145,7 @@ export class ViewDataGenerator { hoursInterval, } = options; - this.skippedDays = options.skippedDays; + this.skippedDays = options.skippedDays ?? this.skippedDays; this.setVisibilityDates(options); this.setHiddenInterval(startDayHour, endDayHour, hoursInterval); From 2ab9e9b02436960a48fe4fc65d4de20660dfbbb7 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 12:36:11 +0200 Subject: [PATCH 16/30] refactor: remove useless typing --- .../m_view_data_generator_timeline_month.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index 97dacb53b125..1eaebe3840a9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts @@ -28,18 +28,11 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return 1; } - protected calculateStartViewDate( - options: { - currentDate: Date; - startDayHour: number; - intervalCount: number; - startDate?: Date; - }, - ): Date { + protected calculateStartViewDate(options: any): Date { return timelineMonthUtils.calculateStartViewDate( options.currentDate, options.startDayHour, - options.startDate ?? options.currentDate, + options.startDate, options.intervalCount, ); } @@ -67,10 +60,7 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { this.hiddenInterval = 0; } - protected getCellEndDate( - cellStartDate: Date, - options: { startDayHour: number; endDayHour: number }, - ): Date { + protected getCellEndDate(cellStartDate: Date, options: any): Date { const { startDayHour, endDayHour } = options; const durationMs = (endDayHour - startDayHour) * toMs('hour'); return timezoneUtils.addOffsetsWithoutDST(cellStartDate, durationMs); From d9cedc542cfbdedf2d873a63d5093f73ac744cca Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 13:27:54 +0200 Subject: [PATCH 17/30] fix: fix hiddenWeekDays for workWeek --- .../view_model/m_view_data_generator.ts | 18 +++++++++--------- .../m_view_data_generator_work_week.ts | 4 ++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 7047a80d4302..bd9317bc7b8e 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -585,16 +585,9 @@ export class ViewDataGenerator { const millisecondsOffset = this.getMillisecondsOffset(cellIndex, interval, cellCountInDay); let offsetByCount: number; - if (this.isWorkWeekView()) { - offsetByCount = this.getTimeOffsetByColumnIndex( - columnIndex, - this.getFirstDayOfWeek(firstDayOfWeek), - columnCountBase, - intervalCount, - ); - } else if ( + if ( this.skippedDays.length > 0 - && (this.usesWeeklyDayLayout() || this.usesMonthDayLayout()) + && (this.usesWeeklyDayLayout() || this.usesMonthDayLayout() || this.isWorkWeekView()) ) { offsetByCount = this.getVisibleDayOffset( rowIndex, @@ -602,6 +595,13 @@ export class ViewDataGenerator { this.getSkippedDaysAnchorDay(firstDayOfWeek, startViewDate), cellCountInDay, ) * toMs('day'); + } else if (this.isWorkWeekView()) { + offsetByCount = this.getTimeOffsetByColumnIndex( + columnIndex, + this.getFirstDayOfWeek(firstDayOfWeek), + columnCountBase, + intervalCount, + ); } else { offsetByCount = 0; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index 1e99a186cd1e..5857bb6a815c 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -6,6 +6,10 @@ export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { public skippedDays: number[] = [0, 6]; + protected getSkippedDaysAnchorKind(): 'startViewDate' { + return 'startViewDate'; + } + protected calculateStartViewDate(options) { return workWeekUtils.calculateStartViewDate( options.currentDate, From da7160f84b4b70509815bdaf8bbd6225a6af35fb Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 13:32:40 +0200 Subject: [PATCH 18/30] refactor: revert SkippedDaysAnchorKind --- .../workspaces/view_model/m_view_data_generator.ts | 13 ++----------- .../m_view_data_generator_timeline_month.ts | 7 +++++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index bd9317bc7b8e..e422200f16d8 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -30,7 +30,6 @@ import type { } from './m_types'; const toMs = dateUtils.dateToMilliseconds; -type SkippedDaysAnchorKind = 'firstDayOfWeek' | 'startViewDate'; export class ViewDataGenerator { protected baseDaysInInterval = 1; @@ -73,18 +72,10 @@ export class ViewDataGenerator { return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); } - protected getSkippedDaysAnchorKind(): SkippedDaysAnchorKind { - return 'firstDayOfWeek'; - } - - private getSkippedDaysAnchorDay( + protected getSkippedDaysAnchorDay( firstDayOfWeekOption: number | undefined, - startViewDate: Date, + startViewDate: Date, // eslint-disable-line @typescript-eslint/no-unused-vars ): number { - if (this.getSkippedDaysAnchorKind() === 'startViewDate') { - return startViewDate.getDay(); - } - return this.getFirstDayOfWeek(firstDayOfWeekOption) ?? 0; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index 1eaebe3840a9..7357d4f3817d 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts @@ -12,8 +12,11 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return true; } - protected getSkippedDaysAnchorKind(): 'startViewDate' { - return 'startViewDate'; + protected override getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + startViewDate: Date, + ): number { + return startViewDate.getDay(); } calculateEndDate(startDate: Date, interval: number, endDayHour: number): Date { From 1bcb10331069b81cf067e0c986d7ce094362c00b Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 15:22:06 +0200 Subject: [PATCH 19/30] refactor: optimize --- .../workspaces/view_model/m_view_data_generator.ts | 9 +-------- .../view_model/m_view_data_generator_timeline_month.ts | 4 ---- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index e422200f16d8..9e51571c8647 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -60,10 +60,6 @@ export class ViewDataGenerator { ].includes(this.viewType); } - protected usesWeeklyDayLayout(): boolean { - return this.baseDaysInInterval >= 7; - } - protected usesMonthDayLayout(): boolean { return false; } @@ -576,10 +572,7 @@ export class ViewDataGenerator { const millisecondsOffset = this.getMillisecondsOffset(cellIndex, interval, cellCountInDay); let offsetByCount: number; - if ( - this.skippedDays.length > 0 - && (this.usesWeeklyDayLayout() || this.usesMonthDayLayout() || this.isWorkWeekView()) - ) { + if (this.skippedDays.length > 0) { offsetByCount = this.getVisibleDayOffset( rowIndex, columnIndex, diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index 7357d4f3817d..53fd539f8934 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts @@ -8,10 +8,6 @@ import { ViewDataGenerator } from './m_view_data_generator'; const toMs = dateUtils.dateToMilliseconds; export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { - protected usesWeeklyDayLayout(): boolean { - return true; - } - protected override getSkippedDaysAnchorDay( firstDayOfWeekOption: number | undefined, startViewDate: Date, From 2b82fa57c9082bb2d8300537e8c20e7f2b73984c Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 15:22:23 +0200 Subject: [PATCH 20/30] feat: allow hiddenWeekDays for agenda --- .../js/__internal/scheduler/utils/options/utils.test.ts | 4 ++-- .../devextreme/js/__internal/scheduler/utils/options/utils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index 7ba3f281c5c0..a82da2a4cc40 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -184,8 +184,8 @@ describe('views utils', () => { expect(getSkipped(['day'], 'day', [3])).toEqual([]); }); - it('global hiddenWeekDays on agenda → ignored (unsupported view)', () => { - expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([]); + it('global hiddenWeekDays on agenda → applied', () => { + expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([3]); }); it('per-view hiddenWeekDays dedupes duplicates', () => { diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index de3ee9655246..8a218e94d24b 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -10,7 +10,7 @@ import type { } from './types'; const VIEWS_SUPPORTING_HIDDEN_DAYS: ReadonlySet = new Set([ - 'week', 'month', 'timelineWeek', 'timelineMonth', + 'week', 'month', 'timelineWeek', 'timelineMonth', 'agenda', ]); const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ From 9c71b48907a8ab7324532e48932692911fc2559d Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 15:33:31 +0200 Subject: [PATCH 21/30] refactor: optimize --- .../workspaces/view_model/m_view_data_generator_work_week.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index 5857bb6a815c..1e99a186cd1e 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -6,10 +6,6 @@ export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { public skippedDays: number[] = [0, 6]; - protected getSkippedDaysAnchorKind(): 'startViewDate' { - return 'startViewDate'; - } - protected calculateStartViewDate(options) { return workWeekUtils.calculateStartViewDate( options.currentDate, From e32349ac16c1d7b497ed317253ba380a0fd6b47a Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 15:39:05 +0200 Subject: [PATCH 22/30] refactor: optimize --- .../view_model/m_view_data_generator_timeline_month.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index 53fd539f8934..9172371e5b30 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts @@ -46,7 +46,7 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { const daysInMonth = monthDate.getDate(); for (let day = 1; day <= daysInMonth; day += 1) { const date = new Date(monthDate.getFullYear(), monthDate.getMonth(), day); - if (!this.skippedDays.includes(date.getDay())) { + if (!this.isSkippedDate(date)) { cellCount += 1; } } From b341ca6523e12d390d0c84a685ea70fa3915dd20 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 9 Apr 2026 15:56:03 +0200 Subject: [PATCH 23/30] feat: create type WeekdayIndex for skippedDays --- .../js/__internal/scheduler/header/m_utils.ts | 5 ++-- .../js/__internal/scheduler/header/types.ts | 3 ++- .../scheduler_options_base_widget.ts | 3 ++- .../scheduler/utils/options/constants_view.ts | 5 ++-- .../scheduler/utils/options/types.ts | 6 +++-- .../scheduler/utils/options/utils.test.ts | 5 ++-- .../scheduler/utils/options/utils.ts | 17 ++++++------ .../scheduler/utils/skipped_days.ts | 26 +++++++++++++------ .../view_model/__mock__/scheduler.mock.ts | 4 ++- .../common/split_interval_by_days.ts | 3 ++- .../options/get_minutes_cell_intervals.ts | 11 +++++--- .../options/get_one_day_cell_intervals.ts | 4 ++- .../__internal/scheduler/view_model/types.ts | 3 ++- .../scheduler/workspaces/m_work_space.ts | 3 ++- .../workspaces/view_model/m_types.ts | 3 ++- .../view_model/m_view_data_generator.ts | 5 ++-- .../m_view_data_generator_work_week.ts | 4 ++- 17 files changed, 72 insertions(+), 38 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 01095e4227e9..ec52f707c9c1 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -7,6 +7,7 @@ import type { BaseFormat } from '@ts/core/localization/date'; import { camelize } from '@ts/core/utils/m_inflector'; import type { IntervalOptions, Step } from '@ts/scheduler/header/types'; import type { NormalizedView, RawViewType, ViewType } from '@ts/scheduler/utils/options/types'; +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { getDateAfterVisibleWeek, getFirstVisibleDate, @@ -110,7 +111,7 @@ const getPeriodEndDate = ( currentPeriodStartDate: Date, step: Step, agendaDuration: number, - skippedDays: number[], + skippedDays: WeekdayIndex[], ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), @@ -128,7 +129,7 @@ const getPeriodEndDate = ( const getNextPeriodStartDate = ( currentPeriodEndDate: Date, step: Step, - skippedDays: number[], + skippedDays: WeekdayIndex[], ): Date => { let date = addMS(currentPeriodEndDate); diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 061118859c4b..873f419e8c5e 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -2,6 +2,7 @@ import type { FirstDayOfWeek } from '@js/common'; import type { ValueChangedEvent } from '@js/ui/calendar'; import type { NormalizedView, SafeSchedulerOptions } from '../utils/options/types'; +import type { WeekdayIndex } from '../utils/skipped_days'; export interface HeaderOptions { currentView: NormalizedView; @@ -30,7 +31,7 @@ export interface IntervalOptions { firstDayOfWeek?: number; intervalCount: number; agendaDuration?: number; - skippedDays: number[]; + skippedDays: WeekdayIndex[]; } export interface HeaderCalendarOptions { diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 27bb628740db..181addb266b6 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -13,6 +13,7 @@ import type { } from './utils/options/types'; import { getCurrentView, getViewOption, getViews } from './utils/options/utils'; import { SchedulerOptionsValidator, SchedulerOptionsValidatorErrorsHandler } from './utils/options_validator/index'; +import type { WeekdayIndex } from './utils/skipped_days'; export class SchedulerOptionsBaseWidget extends Widget { protected views: NormalizedView[] = []; @@ -50,7 +51,7 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - const hiddenWeekDays = this.option('hiddenWeekDays'); + const hiddenWeekDays = this.option('hiddenWeekDays') as WeekdayIndex[] | undefined; this.views = getViews(views, hiddenWeekDays); this.currentView = getCurrentView( this.option('currentView') ?? '', diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts index d01f374237bd..1d19ca6df17a 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts @@ -1,3 +1,4 @@ +import type { WeekdayIndex } from '../skipped_days'; import type { AgendaView, View, ViewType } from './types'; export const VIEWS: Record = { @@ -13,11 +14,11 @@ export const VIEWS: Record = { }; export const VIEW_TYPES: ViewType[] = Object.values(VIEWS); -const WEEKENDS = [0, 6]; +const WEEKENDS: WeekdayIndex[] = [0, 6]; const getView = ( type: ViewType, groupOrientation: View['groupOrientation'], - skippedDays: number[] = [], + skippedDays: WeekdayIndex[] = [], ): View => ({ groupOrientation, intervalCount: 1, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts index eae5521500c5..07dab720f04a 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts @@ -1,6 +1,8 @@ import type { template } from '@js/common'; import type { Properties } from '@js/ui/scheduler'; +import type { WeekdayIndex } from '../skipped_days'; + export type RawViewType = Required['views'][number]; export type ViewType = Extract; export type ViewObject = Extract; @@ -9,14 +11,14 @@ export type View = ViewObject & Required> & { - skippedDays: number[]; + skippedDays: WeekdayIndex[]; }; export type AgendaView = ViewObject & Required> & { - skippedDays: number[]; + skippedDays: WeekdayIndex[]; }; export type NormalizedView = View | AgendaView; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index a82da2a4cc40..7d4919ccce28 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -3,6 +3,7 @@ import { } from '@jest/globals'; import errors from '@js/ui/widget/ui.errors'; +import type { WeekdayIndex } from '../skipped_days'; import type { RawViewType, ViewType } from './types'; import { getCurrentView, @@ -141,8 +142,8 @@ describe('views utils', () => { const getSkipped = ( views: RawViewType[], viewType: ViewType, - globalHiddenWeekDays?: number[], - ): number[] => { + globalHiddenWeekDays?: WeekdayIndex[], + ): WeekdayIndex[] => { const result = getViews(views, globalHiddenWeekDays); const view = result.find((v) => v.type === viewType); return view?.skippedDays ?? []; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 8a218e94d24b..745b1590e628 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -4,6 +4,7 @@ import { dateUtils } from '@ts/core/utils/m_date'; import { dateSerialization } from '@ts/core/utils/m_date_serialization'; import { extend } from '@ts/core/utils/m_extend'; +import { isWeekdayIndex, type WeekdayIndex } from '../skipped_days'; import { DEFAULT_VIEW_OPTIONS, VIEW_TYPES } from './constants_view'; import type { DateOption, NormalizedView, RawViewType, SafeSchedulerOptions, ViewType, @@ -19,12 +20,12 @@ const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ const normalizeHiddenWeekDays = ( days: unknown, -): number[] | undefined => { +): WeekdayIndex[] | undefined => { if (!Array.isArray(days)) { return undefined; } const valid = [...new Set(days)] - .filter((d): d is number => typeof d === 'number' && Number.isInteger(d) && d >= 0 && d <= 6) + .filter(isWeekdayIndex) .sort((a, b) => a - b); if (valid.length >= 7) { errors.log('W1029'); @@ -36,9 +37,9 @@ const normalizeHiddenWeekDays = ( const resolveSkippedDays = ( viewType: ViewType, perViewHiddenWeekDays: unknown, - globalHiddenWeekDays: number[] | undefined, - viewDefault: number[], -): number[] => { + globalHiddenWeekDays: WeekdayIndex[] | undefined, + viewDefault: WeekdayIndex[], +): WeekdayIndex[] => { const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays); if (perView !== undefined) { return perView; @@ -58,7 +59,7 @@ const isExistedView = (view: NormalizedView | undefined): view is NormalizedView const normalizeView = ( view: RawViewType, - globalHiddenWeekDays?: number[], + globalHiddenWeekDays?: WeekdayIndex[], ): NormalizedView | undefined => { if (isObject(view)) { const viewType = view.type as ViewType; @@ -93,7 +94,7 @@ const normalizeView = ( export const getViews = ( views: RawViewType[], - globalHiddenWeekDays?: number[], + globalHiddenWeekDays?: WeekdayIndex[], ): NormalizedView[] => views .filter(isKnownView) .map((v) => normalizeView(v, globalHiddenWeekDays)) @@ -102,7 +103,7 @@ export const getViews = ( export function getCurrentView( currentView: string | ViewType, views: RawViewType[], - globalHiddenWeekDays?: number[], + globalHiddenWeekDays?: WeekdayIndex[], ): NormalizedView { const viewsProps = getViews(views, globalHiddenWeekDays); const currentViewProps = viewsProps.find( diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index ecb31eaadfd4..0940e7554c52 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -1,14 +1,24 @@ -export const isDateSkipped = (date: Date, skippedDays: number[]): boolean => ( - skippedDays.includes(date.getDay()) +export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +export const isWeekdayIndex = (value: unknown): value is WeekdayIndex => ( + typeof value === 'number' + && Number.isInteger(value) + && value >= 0 + && value <= 6 +); + +export const isDateSkipped = (date: Date, skippedDays: WeekdayIndex[]): boolean => ( + skippedDays.includes(date.getDay() as WeekdayIndex) ); export const getVisibleDaysOfWeek = ( firstDayOfWeek: number, - skippedDays: number[], -): number[] => { - const result: number[] = []; + skippedDays: WeekdayIndex[], +): WeekdayIndex[] => { + const result: WeekdayIndex[] = []; for (let count = 0; count < 7; count += 1) { - const dayOfWeek = (firstDayOfWeek + count) % 7; + const raw = firstDayOfWeek + count; + const dayOfWeek = ((raw % 7) + 7) % 7 as WeekdayIndex; if (!skippedDays.includes(dayOfWeek)) { result.push(dayOfWeek); } @@ -19,7 +29,7 @@ export const getVisibleDaysOfWeek = ( export const getFirstVisibleDate = ( start: Date, - skippedDays: number[], + skippedDays: WeekdayIndex[], nextDate: (date: Date) => Date, ): Date => { let date = new Date(start); @@ -31,7 +41,7 @@ export const getFirstVisibleDate = ( export const getDateAfterVisibleWeek = ( start: Date, - skippedDays: number[], + skippedDays: WeekdayIndex[], nextDate: (date: Date) => Date, ): Date => { const visibleCount = 7 - skippedDays.length; 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 index 98483bff3457..d47b8220e647 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts @@ -1,3 +1,5 @@ +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; + import { mockAppointmentDataAccessor } from '../../__mock__/appointment_data_accessor.mock'; import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock'; import type Scheduler from '../../m_scheduler'; @@ -18,7 +20,7 @@ export const getSchedulerMock = ({ endDayHour: number; offsetMinutes: number; resourceManager?: ResourceManager; - skippedDays?: number[]; + skippedDays?: WeekdayIndex[]; dateRange?: Date[]; isVirtualScrolling?: boolean; }): Scheduler => ({ diff --git a/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts b/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts index 7d71bf194491..6615763943d4 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts @@ -1,4 +1,5 @@ import { dateUtils } from '@ts/core/utils/m_date'; +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import type { CompareOptions, DateInterval } from '../types'; @@ -26,7 +27,7 @@ export const splitIntervalByDay = ({ const result: DateInterval[] = []; while (time < maxTime) { - if (!skippedDays.includes(time.getUTCDay())) { + if (!skippedDays.includes(time.getUTCDay() as WeekdayIndex)) { const intervalMax = new Date(time); intervalMax.setUTCHours(endTime.hours, endTime.minutes, 0, 0); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts index fb2d55b50544..f9f611ab4960 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts @@ -1,3 +1,5 @@ +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; + import { splitIntervalByDay } from '../../common/split_interval_by_days'; import type { CellInterval, DateInterval } from '../../types'; @@ -6,13 +8,16 @@ interface Options { startDayHour: number; endDayHour: number; durationMinutes: number; - skippedDays: number[]; + skippedDays: WeekdayIndex[]; } const filterBySkippedDays = ( intervals: T[], - skippedDays: number[], -): T[] => intervals.filter((item) => !skippedDays.includes(new Date(item.min).getUTCDay())); + skippedDays: WeekdayIndex[], +): T[] => intervals.filter((item) => { + const weekday = new Date(item.min).getUTCDay() as WeekdayIndex; + return !skippedDays.includes(weekday); +}); export const getMinutesCellIntervals = ({ intervals, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts index 9b8146dda0d7..40ca54f5c92c 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts @@ -1,3 +1,5 @@ +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; + import { splitIntervalByDay } from '../../common/split_interval_by_days'; import type { CellInterval, DateInterval } from '../../types'; @@ -5,7 +7,7 @@ interface Options { intervals: DateInterval[]; startDayHour: number; endDayHour: number; - skippedDays: number[]; + skippedDays: WeekdayIndex[]; } export const getOneDayCellIntervals = ({ diff --git a/packages/devextreme/js/__internal/scheduler/view_model/types.ts b/packages/devextreme/js/__internal/scheduler/view_model/types.ts index 591d2be66ccc..d266d430ddd7 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/types.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/types.ts @@ -4,6 +4,7 @@ import type { AllDayPanelModeType, SafeAppointment } from '../types'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; import type { ResourceManager } from '../utils/resource_manager/resource_manager'; import type { GroupLeaf } from '../utils/resource_manager/types'; +import type { WeekdayIndex } from '../utils/skipped_days'; import type { Empty, Geometry, @@ -29,7 +30,7 @@ export interface CompareOptions { endDayHour: number; min: number; max: number; - skippedDays: number[]; + skippedDays: WeekdayIndex[]; } export interface LayoutIntervals { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index dbcfade80a11..b26da696c817 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -50,6 +50,7 @@ import { isDateAndTimeView, } from '@ts/scheduler/r1/utils/index'; import type { ViewType } from '@ts/scheduler/types'; +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import Scrollable from '@ts/ui/scroll_view/scrollable'; import type NotifyScheduler from '../base/m_widget_notify_scheduler'; @@ -204,7 +205,7 @@ type WorkspaceOptionsInternal = Omit & { hoursInterval: number; startDayHour: number; endDayHour: number; - skippedDays: number[]; + skippedDays: WeekdayIndex[]; }; class SchedulerWorkSpace extends Widget { private viewDataProviderValue: any; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts index 5f4509955bdb..996141e11302 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts @@ -8,6 +8,7 @@ import type { } from '../../types'; import type { ResourceManager } from '../../utils/resource_manager/resource_manager'; import type { GroupLeaf } from '../../utils/resource_manager/types'; +import type { WeekdayIndex } from '../../utils/skipped_days'; interface CommonOptions extends CountGenerationConfig { getResourceManager: () => ResourceManager; @@ -16,7 +17,7 @@ interface CommonOptions extends CountGenerationConfig { viewOffset: number; hoursInterval: number; viewType: ViewType; - skippedDays?: number[]; + skippedDays?: WeekdayIndex[]; cellCount: number; isProvideVirtualCellsWidth: boolean; isGenerateTimePanelData?: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 9e51571c8647..e0acf5a33a55 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -1,6 +1,7 @@ import dateUtils from '@js/core/utils/date'; import { dateUtilsTs } from '@ts/core/utils/date'; import type { GroupLeaf } from '@ts/scheduler/utils/resource_manager/types'; +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { HORIZONTAL_GROUP_ORIENTATION } from '../../constants'; import timezoneUtils from '../../m_utils_time_zone'; @@ -38,7 +39,7 @@ export class ViewDataGenerator { public hiddenInterval = 0; - public skippedDays: number[] = []; + public skippedDays: WeekdayIndex[] = []; constructor(public readonly viewType: ViewType) {} @@ -64,7 +65,7 @@ export class ViewDataGenerator { return false; } - public getVisibleDaysOfWeek(firstDayOfWeek: number): number[] { + public getVisibleDaysOfWeek(firstDayOfWeek: number): WeekdayIndex[] { return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index 1e99a186cd1e..4f730fa91165 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -1,10 +1,12 @@ +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; + import { workWeekUtils } from '../../r1/utils/index'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { protected baseDaysInInterval = 5; - public skippedDays: number[] = [0, 6]; + public skippedDays: WeekdayIndex[] = [0, 6]; protected calculateStartViewDate(options) { return workWeekUtils.calculateStartViewDate( From 6a2a314a7ff41cd6fa975ad0cd6c1abd02b2379b Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 11:37:04 +0200 Subject: [PATCH 24/30] feat: update agenda and header logic to support hiddenWeekDays --- .../scheduler/header/m_utils.test.ts | 30 ++++++++++ .../js/__internal/scheduler/header/m_utils.ts | 34 ++++++++--- .../scheduler/r1/utils/agenda.test.ts | 34 ++++++++++- .../__internal/scheduler/r1/utils/agenda.ts | 57 ++++++++++++++++--- .../js/__internal/scheduler/r1/utils/index.ts | 4 ++ .../scheduler/utils/skipped_days.ts | 11 ++-- .../scheduler/workspaces/m_agenda.ts | 30 +++++----- 7 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts new file mode 100644 index 000000000000..7345d652f5c5 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from '@jest/globals'; +import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; + +import { getCaptionInterval, getNextIntervalDate } from './m_utils'; + +describe('agenda hiddenWeekDays support in header utils', () => { + const skippedDays: WeekdayIndex[] = [0, 6]; + const options = { + date: new Date(2026, 3, 11), + step: 'agenda' as const, + intervalCount: 1, + agendaDuration: 3, + skippedDays, + }; + + it('should build caption interval by visible days', () => { + expect(getCaptionInterval(options)).toEqual({ + startDate: new Date(2026, 3, 13), + endDate: new Date(2026, 3, 15, 23, 59, 59, 999), + }); + }); + + it('should navigate to next agenda interval by visible days', () => { + expect(getNextIntervalDate(options, 1)).toEqual(new Date(2026, 3, 16)); + }); + + it('should navigate to previous agenda interval by visible days', () => { + expect(getNextIntervalDate(options, -1)).toEqual(new Date(2026, 3, 8)); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index ec52f707c9c1..c69755c6a53b 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -9,7 +9,7 @@ import type { IntervalOptions, Step } from '@ts/scheduler/header/types'; import type { NormalizedView, RawViewType, ViewType } from '@ts/scheduler/utils/options/types'; import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { - getDateAfterVisibleWeek, + getDateAfterVisibleDays, getFirstVisibleDate, } from '@ts/scheduler/utils/skipped_days'; @@ -43,6 +43,8 @@ const addMS = (date: Date): Date => addDateInterval(date, MS_DURATION, 1); const nextDay = (date: Date): Date => addDateInterval(date, DAY_DURATION, 1); +const prevDay = (date: Date): Date => addDateInterval(date, DAY_DURATION, -1); + export const nextWeek = (date: Date): Date => addDateInterval(date, WEEK_DURATION, 1); const nextMonth = (date: Date): Date => { @@ -80,7 +82,10 @@ const getDateAfterWorkWeek = (workWeekStart: Date): Date => { const nextAgendaStart = ( date: Date, agendaDuration: number, -): Date => addDateInterval(date, { days: agendaDuration }, 1); + skippedDays: WeekdayIndex[], +): Date => (skippedDays.length > 0 + ? getDateAfterVisibleDays(date, agendaDuration, skippedDays, nextDay) + : addDateInterval(date, { days: agendaDuration }, 1)); const getIntervalStartDate = (options: IntervalOptions): Date => { const { @@ -101,7 +106,9 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { case 'workWeek': return getWorkWeekStart(getWeekStart(date, firstDayOfWeek)); case 'agenda': - return new Date(date); + return skippedDays.length > 0 + ? getFirstVisibleDate(date, skippedDays, nextDay) + : new Date(date); default: return new Date(date); } @@ -116,11 +123,16 @@ const getPeriodEndDate = ( const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), week: () => (skippedDays.length > 0 - ? getDateAfterVisibleWeek(currentPeriodStartDate, skippedDays, nextDay) + ? getDateAfterVisibleDays( + currentPeriodStartDate, + 7 - skippedDays.length, + skippedDays, + nextDay, + ) : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate), - agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), + agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration, skippedDays), }; return subMS(calculators[step]()); @@ -187,7 +199,7 @@ const getNextMonthDate = (date: Date, intervalCount: number, direction: Directio export const getNextIntervalDate = (options: IntervalOptions, direction: Direction): Date => { const { - date, step, intervalCount, agendaDuration, + date, step, intervalCount, agendaDuration, skippedDays, } = options; let dayDuration = 0; @@ -201,8 +213,14 @@ export const getNextIntervalDate = (options: IntervalOptions, direction: Directi dayDuration = 7 * intervalCount; break; case 'agenda': - dayDuration = agendaDuration ?? 0; - break; + return skippedDays.length > 0 + ? getDateAfterVisibleDays( + getIntervalStartDate(options), + agendaDuration ?? 0, + skippedDays, + direction > 0 ? nextDay : prevDay, + ) + : addDateInterval(date, { days: agendaDuration ?? 0 }, direction); case 'month': return getNextMonthDate(date, intervalCount, direction); } diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts index 0f93760824ba..c50546d21ad4 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from '@jest/globals'; -import { calculateRows } from './agenda'; +import { + calculateEndViewDate, + calculateRows, + calculateStartViewDate, + getDateByIndex, +} from './agenda'; const items = [ { groupIndex: 0, startDateUTC: Date.UTC(2020, 0, 10, 5) }, @@ -27,4 +32,31 @@ describe('calculateRows', () => { [0, 2, 1, 0, 2, 0, 0], ]); }); + + it('should count only visible agenda days when skippedDays are set', () => { + expect(calculateRows(items, 3, new Date(2020, 0, 10), 2, [0, 6])).toEqual([ + [2, 0, 0], + [0, 0, 2], + ]); + }); +}); + +describe('visible agenda days', () => { + it('should shift startViewDate to the first visible date', () => { + expect(calculateStartViewDate(new Date(2020, 0, 11, 9), 9, [0, 6])).toEqual( + new Date(2020, 0, 13, 9), + ); + }); + + it('should return visible day by row index', () => { + expect(getDateByIndex(new Date(2020, 0, 10, 9), 2, [0, 6])).toEqual( + new Date(2020, 0, 14, 9), + ); + }); + + it('should calculate endViewDate by visible days', () => { + expect(calculateEndViewDate(new Date(2020, 0, 10, 9), 18, 3, [0, 6])).toEqual( + new Date(2020, 0, 14, 17, 59), + ); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts index 8bddcb45b093..76a07a627b36 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts @@ -1,24 +1,64 @@ import timeZoneUtils from '../../m_utils_time_zone'; +import type { WeekdayIndex } from '../../utils/skipped_days'; +import { + getDateAfterVisibleDays, + getFirstVisibleDate, +} from '../../utils/skipped_days'; import type { ListEntity } from '../../view_model/types'; import { setOptionHour } from './base'; -export const calculateStartViewDate = (currentDate: Date, startDayHour: number): Date => { +const nextDay = (date: Date): Date => { + const nextDate = new Date(date); + nextDate.setDate(nextDate.getDate() + 1); + return nextDate; +}; + +export const calculateStartViewDate = ( + currentDate: Date, + startDayHour: number, + skippedDays: WeekdayIndex[] = [], +): Date => { const validCurrentDate = new Date(currentDate); + const startViewDate = setOptionHour(validCurrentDate, startDayHour); - return setOptionHour(validCurrentDate, startDayHour); + return skippedDays.length > 0 + ? getFirstVisibleDate(startViewDate, skippedDays, nextDay) + : startViewDate; }; const getDayStart = (date: Date | number): number => new Date(date).setUTCHours(0, 0, 0, 0); +export const getDateByIndex = ( + startViewDate: Date, + index: number, + skippedDays: WeekdayIndex[] = [], +): Date => (index <= 0 + ? new Date(startViewDate) + : getDateAfterVisibleDays(startViewDate, index, skippedDays, nextDay)); + +export const calculateEndViewDate = ( + startViewDate: Date, + endDayHour: number, + agendaDuration: number, + skippedDays: WeekdayIndex[] = [], +): Date => { + const lastVisibleDate = getDateByIndex( + startViewDate, + Math.max(agendaDuration - 1, 0), + skippedDays, + ); + const endViewDate = setOptionHour(lastVisibleDate, endDayHour); + + return new Date(endViewDate.getTime() - 60000); +}; + export const calculateRows = ( appointments: ListEntity[], agendaDuration: number, - currentDate: Date, + startViewDate: Date, groupCount: number, + skippedDays: WeekdayIndex[] = [], ): number[][] => { - const dayMs = getDayStart( - timeZoneUtils.createUTCDateWithLocalOffset(currentDate), - ); const intervalsStartMap = new Map(); const result = Array.from( { length: groupCount || 1 }, @@ -26,8 +66,9 @@ export const calculateRows = ( ); for (let i = 0; i < agendaDuration; i += 1) { - const day = new Date(dayMs); - intervalsStartMap.set(day.setUTCDate(day.getUTCDate() + i), i); + const date = getDateByIndex(startViewDate, i, skippedDays); + const dayStart = getDayStart(timeZoneUtils.createUTCDateWithLocalOffset(date)); + intervalsStartMap.set(dayStart, i); } appointments.forEach((appointment) => { diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts index 59c306211a45..d62738a2a2cf 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts @@ -1,8 +1,10 @@ import { getThemeType } from '@ts/scheduler/r1/utils/themes'; import { + calculateEndViewDate, calculateRows, calculateStartViewDate, + getDateByIndex, } from './agenda'; import { calculateStartViewDate as dayCalculateStartViewDate, @@ -87,8 +89,10 @@ export { } from './format_weekday'; export const agendaUtils = { + calculateEndViewDate, calculateStartViewDate, calculateRows, + getDateByIndex, }; export const dayUtils = { diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index 0940e7554c52..f4a3a45cad3e 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -39,23 +39,24 @@ export const getFirstVisibleDate = ( return date; }; -export const getDateAfterVisibleWeek = ( +export const getDateAfterVisibleDays = ( start: Date, + visibleDayCount: number, skippedDays: WeekdayIndex[], nextDate: (date: Date) => Date, ): Date => { - const visibleCount = 7 - skippedDays.length; - if (visibleCount <= 0) { + if (visibleDayCount <= 0) { return new Date(start); } let date = new Date(start); let visited = 0; - while (visited < visibleCount) { + while (visited < visibleDayCount) { + date = nextDate(date); if (!isDateSkipped(date, skippedDays)) { visited += 1; } - date = nextDate(date); } + return date; }; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts index a886699802ec..964a953fa2e9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts @@ -152,7 +152,11 @@ class SchedulerAgenda extends WorkSpace { } protected override renderView() { - this.startViewDate = agendaUtils.calculateStartViewDate(this.option('currentDate') as any, this.option('startDayHour') as any); + this.startViewDate = agendaUtils.calculateStartViewDate( + this.option('currentDate'), + this.option('startDayHour'), + this.option('skippedDays'), + ); this.rows = []; } @@ -441,10 +445,11 @@ class SchedulerAgenda extends WorkSpace { } private getTimePanelStartDate(rowIndex) { - const current = new Date(this.option('currentDate') as any); - const cellDate = new Date(current.setDate(current.getDate() + rowIndex)); - - return cellDate; + return agendaUtils.getDateByIndex( + this.getStartViewDate(), + rowIndex, + this.option('skippedDays'), + ); } private getRowHeight(rowSize) { @@ -476,6 +481,7 @@ class SchedulerAgenda extends WorkSpace { this.option('agendaDuration') as number, this.getStartViewDate(), this.resourceManager.groupCount(), + this.option('skippedDays'), ); this.recalculateAgenda(rows); } @@ -485,14 +491,12 @@ class SchedulerAgenda extends WorkSpace { } getEndViewDate() { - const currentDate = new Date(this.option('currentDate') as any); - const agendaDuration: any = this.option('agendaDuration'); - - currentDate.setHours(this.option('endDayHour') as any); - - const result = currentDate.setDate(currentDate.getDate() + agendaDuration - 1) - 60000; - - return new Date(result); + return agendaUtils.calculateEndViewDate( + this.getStartViewDate(), + this.option('endDayHour') as any, + this.option('agendaDuration') as any, + this.option('skippedDays'), + ); } getEndViewDateByEndDayHour() { From 9c81b76623e7287b8ed62dc03cd17dac3692c8f0 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 12:09:39 +0200 Subject: [PATCH 25/30] feat: change agenda logic to show only calendar dates in interval minus hiddenWeekDays --- .../scheduler/header/m_utils.test.ts | 12 +++---- .../js/__internal/scheduler/header/m_utils.ts | 25 ++++--------- .../scheduler/r1/utils/agenda.test.ts | 35 +++++++++++-------- .../__internal/scheduler/r1/utils/agenda.ts | 32 ++++------------- .../scheduler/workspaces/m_agenda.ts | 4 --- 5 files changed, 40 insertions(+), 68 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts index 7345d652f5c5..f8932b7945ae 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -13,18 +13,18 @@ describe('agenda hiddenWeekDays support in header utils', () => { skippedDays, }; - it('should build caption interval by visible days', () => { + it('should build caption interval by calendar days', () => { expect(getCaptionInterval(options)).toEqual({ - startDate: new Date(2026, 3, 13), - endDate: new Date(2026, 3, 15, 23, 59, 59, 999), + startDate: new Date(2026, 3, 11), + endDate: new Date(2026, 3, 13, 23, 59, 59, 999), }); }); - it('should navigate to next agenda interval by visible days', () => { - expect(getNextIntervalDate(options, 1)).toEqual(new Date(2026, 3, 16)); + it('should navigate to next agenda interval by calendar days', () => { + expect(getNextIntervalDate(options, 1)).toEqual(new Date(2026, 3, 14)); }); - it('should navigate to previous agenda interval by visible days', () => { + it('should navigate to previous agenda interval by calendar days', () => { expect(getNextIntervalDate(options, -1)).toEqual(new Date(2026, 3, 8)); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index c69755c6a53b..9076f7815074 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -43,8 +43,6 @@ const addMS = (date: Date): Date => addDateInterval(date, MS_DURATION, 1); const nextDay = (date: Date): Date => addDateInterval(date, DAY_DURATION, 1); -const prevDay = (date: Date): Date => addDateInterval(date, DAY_DURATION, -1); - export const nextWeek = (date: Date): Date => addDateInterval(date, WEEK_DURATION, 1); const nextMonth = (date: Date): Date => { @@ -82,10 +80,7 @@ const getDateAfterWorkWeek = (workWeekStart: Date): Date => { const nextAgendaStart = ( date: Date, agendaDuration: number, - skippedDays: WeekdayIndex[], -): Date => (skippedDays.length > 0 - ? getDateAfterVisibleDays(date, agendaDuration, skippedDays, nextDay) - : addDateInterval(date, { days: agendaDuration }, 1)); +): Date => addDateInterval(date, { days: agendaDuration }, 1); const getIntervalStartDate = (options: IntervalOptions): Date => { const { @@ -106,9 +101,7 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { case 'workWeek': return getWorkWeekStart(getWeekStart(date, firstDayOfWeek)); case 'agenda': - return skippedDays.length > 0 - ? getFirstVisibleDate(date, skippedDays, nextDay) - : new Date(date); + return new Date(date); default: return new Date(date); } @@ -132,7 +125,7 @@ const getPeriodEndDate = ( : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate), - agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration, skippedDays), + agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), }; return subMS(calculators[step]()); @@ -199,7 +192,7 @@ const getNextMonthDate = (date: Date, intervalCount: number, direction: Directio export const getNextIntervalDate = (options: IntervalOptions, direction: Direction): Date => { const { - date, step, intervalCount, agendaDuration, skippedDays, + date, step, intervalCount, agendaDuration, } = options; let dayDuration = 0; @@ -213,14 +206,8 @@ export const getNextIntervalDate = (options: IntervalOptions, direction: Directi dayDuration = 7 * intervalCount; break; case 'agenda': - return skippedDays.length > 0 - ? getDateAfterVisibleDays( - getIntervalStartDate(options), - agendaDuration ?? 0, - skippedDays, - direction > 0 ? nextDay : prevDay, - ) - : addDateInterval(date, { days: agendaDuration ?? 0 }, direction); + dayDuration = agendaDuration ?? 0; + break; case 'month': return getNextMonthDate(date, intervalCount, direction); } diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts index c50546d21ad4..f9e08019185e 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.test.ts @@ -33,30 +33,37 @@ describe('calculateRows', () => { ]); }); - it('should count only visible agenda days when skippedDays are set', () => { - expect(calculateRows(items, 3, new Date(2020, 0, 10), 2, [0, 6])).toEqual([ - [2, 0, 0], - [0, 0, 2], + it('should keep calendar offsets inside agenda duration window', () => { + expect(calculateRows(items.slice(1, 2), 3, new Date(2020, 0, 10), 1)).toEqual([ + [0, 1, 0], + ]); + }); + + it('should map Monday to the third calendar day of Sat-Mon window', () => { + expect(calculateRows([ + { groupIndex: 0, startDateUTC: Date.UTC(2020, 0, 13, 5) }, + ] as any[], 3, new Date(2020, 0, 11), 1)).toEqual([ + [0, 0, 1], ]); }); }); -describe('visible agenda days', () => { - it('should shift startViewDate to the first visible date', () => { - expect(calculateStartViewDate(new Date(2020, 0, 11, 9), 9, [0, 6])).toEqual( - new Date(2020, 0, 13, 9), +describe('agenda calendar range', () => { + it('should keep startViewDate on current date', () => { + expect(calculateStartViewDate(new Date(2020, 0, 11, 9), 9)).toEqual( + new Date(2020, 0, 11, 9), ); }); - it('should return visible day by row index', () => { - expect(getDateByIndex(new Date(2020, 0, 10, 9), 2, [0, 6])).toEqual( - new Date(2020, 0, 14, 9), + it('should return calendar day by row index', () => { + expect(getDateByIndex(new Date(2020, 0, 10, 9), 2)).toEqual( + new Date(2020, 0, 12, 9), ); }); - it('should calculate endViewDate by visible days', () => { - expect(calculateEndViewDate(new Date(2020, 0, 10, 9), 18, 3, [0, 6])).toEqual( - new Date(2020, 0, 14, 17, 59), + it('should calculate endViewDate by calendar days', () => { + expect(calculateEndViewDate(new Date(2020, 0, 10, 9), 18, 3)).toEqual( + new Date(2020, 0, 12, 17, 59), ); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts index 76a07a627b36..c6d475fd17dd 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts @@ -1,29 +1,13 @@ import timeZoneUtils from '../../m_utils_time_zone'; -import type { WeekdayIndex } from '../../utils/skipped_days'; -import { - getDateAfterVisibleDays, - getFirstVisibleDate, -} from '../../utils/skipped_days'; import type { ListEntity } from '../../view_model/types'; import { setOptionHour } from './base'; -const nextDay = (date: Date): Date => { - const nextDate = new Date(date); - nextDate.setDate(nextDate.getDate() + 1); - return nextDate; -}; - export const calculateStartViewDate = ( currentDate: Date, startDayHour: number, - skippedDays: WeekdayIndex[] = [], ): Date => { const validCurrentDate = new Date(currentDate); - const startViewDate = setOptionHour(validCurrentDate, startDayHour); - - return skippedDays.length > 0 - ? getFirstVisibleDate(startViewDate, skippedDays, nextDay) - : startViewDate; + return setOptionHour(validCurrentDate, startDayHour); }; const getDayStart = (date: Date | number): number => new Date(date).setUTCHours(0, 0, 0, 0); @@ -31,21 +15,20 @@ const getDayStart = (date: Date | number): number => new Date(date).setUTCHours( export const getDateByIndex = ( startViewDate: Date, index: number, - skippedDays: WeekdayIndex[] = [], -): Date => (index <= 0 - ? new Date(startViewDate) - : getDateAfterVisibleDays(startViewDate, index, skippedDays, nextDay)); +): Date => { + const date = new Date(startViewDate); + date.setDate(date.getDate() + index); + return date; +}; export const calculateEndViewDate = ( startViewDate: Date, endDayHour: number, agendaDuration: number, - skippedDays: WeekdayIndex[] = [], ): Date => { const lastVisibleDate = getDateByIndex( startViewDate, Math.max(agendaDuration - 1, 0), - skippedDays, ); const endViewDate = setOptionHour(lastVisibleDate, endDayHour); @@ -57,7 +40,6 @@ export const calculateRows = ( agendaDuration: number, startViewDate: Date, groupCount: number, - skippedDays: WeekdayIndex[] = [], ): number[][] => { const intervalsStartMap = new Map(); const result = Array.from( @@ -66,7 +48,7 @@ export const calculateRows = ( ); for (let i = 0; i < agendaDuration; i += 1) { - const date = getDateByIndex(startViewDate, i, skippedDays); + const date = getDateByIndex(startViewDate, i); const dayStart = getDayStart(timeZoneUtils.createUTCDateWithLocalOffset(date)); intervalsStartMap.set(dayStart, i); } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts index 964a953fa2e9..969eeda71374 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts @@ -155,7 +155,6 @@ class SchedulerAgenda extends WorkSpace { this.startViewDate = agendaUtils.calculateStartViewDate( this.option('currentDate'), this.option('startDayHour'), - this.option('skippedDays'), ); this.rows = []; } @@ -448,7 +447,6 @@ class SchedulerAgenda extends WorkSpace { return agendaUtils.getDateByIndex( this.getStartViewDate(), rowIndex, - this.option('skippedDays'), ); } @@ -481,7 +479,6 @@ class SchedulerAgenda extends WorkSpace { this.option('agendaDuration') as number, this.getStartViewDate(), this.resourceManager.groupCount(), - this.option('skippedDays'), ); this.recalculateAgenda(rows); } @@ -495,7 +492,6 @@ class SchedulerAgenda extends WorkSpace { this.getStartViewDate(), this.option('endDayHour') as any, this.option('agendaDuration') as any, - this.option('skippedDays'), ); } From e30a7bd1bd8f8511b1c47ae4cf6a96ac8229b009 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 12:51:49 +0200 Subject: [PATCH 26/30] fix: fix anchor day for workWeek --- .../view_model/m_view_data_generator.test.ts | 24 +++++++++++++++++++ .../m_view_data_generator_work_week.ts | 24 ++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index ae4f4469e4ab..4d253a29e6b8 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -248,4 +248,28 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { expect(date.getDate()).toBe(4); }); }); + + describe('WorkWeek hiddenWeekDays support', () => { + it('keeps first visible column on Monday when startViewDate is already Monday', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + gen.skippedDays = [0, 6]; + + const options = { + startViewDate: new Date(2026, 2, 30, 0, 0), // Monday + startDayHour: 0, + endDayHour: 24, + hoursInterval: 1, + interval: 24 * 60 * 60 * 1000, + firstDayOfWeek: 0, // Sunday + intervalCount: 1, + viewOffset: 0, + currentDate: new Date(2026, 3, 1), + viewType: 'workWeek' as ViewType, + }; + + const date = gen.getDateByCellIndices(options, 0, 0); + expect(date.getDay()).toBe(1); + expect(date.getDate()).toBe(30); + }); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index 4f730fa91165..3a7c31caaba5 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -3,12 +3,20 @@ import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { workWeekUtils } from '../../r1/utils/index'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; +interface WorkWeekStartViewDateOptions { + currentDate: Date; + startDayHour: number; + startDate: Date; + intervalCount: number; + firstDayOfWeek: number; +} + export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { protected baseDaysInInterval = 5; public skippedDays: WeekdayIndex[] = [0, 6]; - protected calculateStartViewDate(options) { + protected override calculateStartViewDate(options: WorkWeekStartViewDateOptions): Date { return workWeekUtils.calculateStartViewDate( options.currentDate, options.startDayHour, @@ -18,7 +26,17 @@ export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { ); } - getFirstDayOfWeek(firstDayOfWeekOption) { - return firstDayOfWeekOption || 0; + // eslint-disable-next-line class-methods-use-this + public override getFirstDayOfWeek(firstDayOfWeekOption: number | undefined): number { + return firstDayOfWeekOption ?? 0; + } + + protected override getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + startViewDate: Date, + ): number { + return this.skippedDays.length > 0 + ? startViewDate.getDay() + : this.getFirstDayOfWeek(firstDayOfWeekOption); } } From 54283e76eecbb32c101934e1fe797c2af3d57042 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 13:03:41 +0200 Subject: [PATCH 27/30] feat: remove WeekDayIndex interface because it requires too much maintance --- .../scheduler/header/m_utils.test.ts | 3 +-- .../js/__internal/scheduler/header/m_utils.ts | 5 ++--- .../js/__internal/scheduler/header/types.ts | 3 +-- .../scheduler_options_base_widget.ts | 6 ++---- .../scheduler/utils/options/constants_view.ts | 5 ++--- .../scheduler/utils/options/types.ts | 6 ++---- .../scheduler/utils/options/utils.test.ts | 5 ++--- .../scheduler/utils/options/utils.ts | 18 ++++++++--------- .../scheduler/utils/skipped_days.ts | 20 +++++++++---------- .../view_model/__mock__/scheduler.mock.ts | 4 +--- .../common/split_interval_by_days.ts | 3 +-- .../options/get_minutes_cell_intervals.ts | 8 +++----- .../options/get_one_day_cell_intervals.ts | 4 +--- .../__internal/scheduler/view_model/types.ts | 3 +-- .../scheduler/workspaces/m_work_space.ts | 3 +-- .../workspaces/view_model/m_types.ts | 3 +-- .../view_model/m_view_data_generator.ts | 5 ++--- .../m_view_data_generator_work_week.ts | 14 ++----------- 18 files changed, 43 insertions(+), 75 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts index f8932b7945ae..169d12606b93 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from '@jest/globals'; -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { getCaptionInterval, getNextIntervalDate } from './m_utils'; describe('agenda hiddenWeekDays support in header utils', () => { - const skippedDays: WeekdayIndex[] = [0, 6]; + const skippedDays: number[] = [0, 6]; const options = { date: new Date(2026, 3, 11), step: 'agenda' as const, diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 9076f7815074..648d759c701d 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -7,7 +7,6 @@ import type { BaseFormat } from '@ts/core/localization/date'; import { camelize } from '@ts/core/utils/m_inflector'; import type { IntervalOptions, Step } from '@ts/scheduler/header/types'; import type { NormalizedView, RawViewType, ViewType } from '@ts/scheduler/utils/options/types'; -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { getDateAfterVisibleDays, getFirstVisibleDate, @@ -111,7 +110,7 @@ const getPeriodEndDate = ( currentPeriodStartDate: Date, step: Step, agendaDuration: number, - skippedDays: WeekdayIndex[], + skippedDays: number[], ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), @@ -134,7 +133,7 @@ const getPeriodEndDate = ( const getNextPeriodStartDate = ( currentPeriodEndDate: Date, step: Step, - skippedDays: WeekdayIndex[], + skippedDays: number[], ): Date => { let date = addMS(currentPeriodEndDate); diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 873f419e8c5e..061118859c4b 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -2,7 +2,6 @@ import type { FirstDayOfWeek } from '@js/common'; import type { ValueChangedEvent } from '@js/ui/calendar'; import type { NormalizedView, SafeSchedulerOptions } from '../utils/options/types'; -import type { WeekdayIndex } from '../utils/skipped_days'; export interface HeaderOptions { currentView: NormalizedView; @@ -31,7 +30,7 @@ export interface IntervalOptions { firstDayOfWeek?: number; intervalCount: number; agendaDuration?: number; - skippedDays: WeekdayIndex[]; + skippedDays: number[]; } export interface HeaderCalendarOptions { diff --git a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 181addb266b6..67affdb4733f 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -13,7 +13,6 @@ import type { } from './utils/options/types'; import { getCurrentView, getViewOption, getViews } from './utils/options/utils'; import { SchedulerOptionsValidator, SchedulerOptionsValidatorErrorsHandler } from './utils/options_validator/index'; -import type { WeekdayIndex } from './utils/skipped_days'; export class SchedulerOptionsBaseWidget extends Widget { protected views: NormalizedView[] = []; @@ -51,12 +50,11 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - const hiddenWeekDays = this.option('hiddenWeekDays') as WeekdayIndex[] | undefined; - this.views = getViews(views, hiddenWeekDays); + this.views = getViews(views, this.option('hiddenWeekDays')); this.currentView = getCurrentView( this.option('currentView') ?? '', views, - hiddenWeekDays, + this.option('hiddenWeekDays'), ); } diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts index 1d19ca6df17a..6585e5bcc9a4 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts @@ -1,4 +1,3 @@ -import type { WeekdayIndex } from '../skipped_days'; import type { AgendaView, View, ViewType } from './types'; export const VIEWS: Record = { @@ -14,11 +13,11 @@ export const VIEWS: Record = { }; export const VIEW_TYPES: ViewType[] = Object.values(VIEWS); -const WEEKENDS: WeekdayIndex[] = [0, 6]; +const WEEKENDS: number[] = [0, 6]; const getView = ( type: ViewType, groupOrientation: View['groupOrientation'], - skippedDays: WeekdayIndex[] = [], + skippedDays: number[] = [], ): View => ({ groupOrientation, intervalCount: 1, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts index 07dab720f04a..eae5521500c5 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts @@ -1,8 +1,6 @@ import type { template } from '@js/common'; import type { Properties } from '@js/ui/scheduler'; -import type { WeekdayIndex } from '../skipped_days'; - export type RawViewType = Required['views'][number]; export type ViewType = Extract; export type ViewObject = Extract; @@ -11,14 +9,14 @@ export type View = ViewObject & Required> & { - skippedDays: WeekdayIndex[]; + skippedDays: number[]; }; export type AgendaView = ViewObject & Required> & { - skippedDays: WeekdayIndex[]; + skippedDays: number[]; }; export type NormalizedView = View | AgendaView; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts index 7d4919ccce28..a82da2a4cc40 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -3,7 +3,6 @@ import { } from '@jest/globals'; import errors from '@js/ui/widget/ui.errors'; -import type { WeekdayIndex } from '../skipped_days'; import type { RawViewType, ViewType } from './types'; import { getCurrentView, @@ -142,8 +141,8 @@ describe('views utils', () => { const getSkipped = ( views: RawViewType[], viewType: ViewType, - globalHiddenWeekDays?: WeekdayIndex[], - ): WeekdayIndex[] => { + globalHiddenWeekDays?: number[], + ): number[] => { const result = getViews(views, globalHiddenWeekDays); const view = result.find((v) => v.type === viewType); return view?.skippedDays ?? []; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 745b1590e628..9a3e1e75cb99 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -4,7 +4,7 @@ import { dateUtils } from '@ts/core/utils/m_date'; import { dateSerialization } from '@ts/core/utils/m_date_serialization'; import { extend } from '@ts/core/utils/m_extend'; -import { isWeekdayIndex, type WeekdayIndex } from '../skipped_days'; +import { isValidWeekday } from '../skipped_days'; import { DEFAULT_VIEW_OPTIONS, VIEW_TYPES } from './constants_view'; import type { DateOption, NormalizedView, RawViewType, SafeSchedulerOptions, ViewType, @@ -20,12 +20,12 @@ const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ const normalizeHiddenWeekDays = ( days: unknown, -): WeekdayIndex[] | undefined => { +): number[] | undefined => { if (!Array.isArray(days)) { return undefined; } const valid = [...new Set(days)] - .filter(isWeekdayIndex) + .filter(isValidWeekday) .sort((a, b) => a - b); if (valid.length >= 7) { errors.log('W1029'); @@ -37,9 +37,9 @@ const normalizeHiddenWeekDays = ( const resolveSkippedDays = ( viewType: ViewType, perViewHiddenWeekDays: unknown, - globalHiddenWeekDays: WeekdayIndex[] | undefined, - viewDefault: WeekdayIndex[], -): WeekdayIndex[] => { + globalHiddenWeekDays: number[] | undefined, + viewDefault: number[], +): number[] => { const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays); if (perView !== undefined) { return perView; @@ -59,7 +59,7 @@ const isExistedView = (view: NormalizedView | undefined): view is NormalizedView const normalizeView = ( view: RawViewType, - globalHiddenWeekDays?: WeekdayIndex[], + globalHiddenWeekDays?: number[], ): NormalizedView | undefined => { if (isObject(view)) { const viewType = view.type as ViewType; @@ -94,7 +94,7 @@ const normalizeView = ( export const getViews = ( views: RawViewType[], - globalHiddenWeekDays?: WeekdayIndex[], + globalHiddenWeekDays?: number[], ): NormalizedView[] => views .filter(isKnownView) .map((v) => normalizeView(v, globalHiddenWeekDays)) @@ -103,7 +103,7 @@ export const getViews = ( export function getCurrentView( currentView: string | ViewType, views: RawViewType[], - globalHiddenWeekDays?: WeekdayIndex[], + globalHiddenWeekDays?: number[], ): NormalizedView { const viewsProps = getViews(views, globalHiddenWeekDays); const currentViewProps = viewsProps.find( diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index f4a3a45cad3e..d4393c9ddc33 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -1,24 +1,22 @@ -export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; - -export const isWeekdayIndex = (value: unknown): value is WeekdayIndex => ( +export const isValidWeekday = (value: unknown): value is number => ( typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 6 ); -export const isDateSkipped = (date: Date, skippedDays: WeekdayIndex[]): boolean => ( - skippedDays.includes(date.getDay() as WeekdayIndex) +export const isDateSkipped = (date: Date, skippedDays: number[]): boolean => ( + skippedDays.includes(date.getDay()) ); export const getVisibleDaysOfWeek = ( firstDayOfWeek: number, - skippedDays: WeekdayIndex[], -): WeekdayIndex[] => { - const result: WeekdayIndex[] = []; + skippedDays: number[], +): number[] => { + const result: number[] = []; for (let count = 0; count < 7; count += 1) { const raw = firstDayOfWeek + count; - const dayOfWeek = ((raw % 7) + 7) % 7 as WeekdayIndex; + const dayOfWeek = ((raw % 7) + 7) % 7; if (!skippedDays.includes(dayOfWeek)) { result.push(dayOfWeek); } @@ -29,7 +27,7 @@ export const getVisibleDaysOfWeek = ( export const getFirstVisibleDate = ( start: Date, - skippedDays: WeekdayIndex[], + skippedDays: number[], nextDate: (date: Date) => Date, ): Date => { let date = new Date(start); @@ -42,7 +40,7 @@ export const getFirstVisibleDate = ( export const getDateAfterVisibleDays = ( start: Date, visibleDayCount: number, - skippedDays: WeekdayIndex[], + skippedDays: number[], nextDate: (date: Date) => Date, ): Date => { if (visibleDayCount <= 0) { 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 index d47b8220e647..98483bff3457 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts @@ -1,5 +1,3 @@ -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; - import { mockAppointmentDataAccessor } from '../../__mock__/appointment_data_accessor.mock'; import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock'; import type Scheduler from '../../m_scheduler'; @@ -20,7 +18,7 @@ export const getSchedulerMock = ({ endDayHour: number; offsetMinutes: number; resourceManager?: ResourceManager; - skippedDays?: WeekdayIndex[]; + skippedDays?: number[]; dateRange?: Date[]; isVirtualScrolling?: boolean; }): Scheduler => ({ diff --git a/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts b/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts index 6615763943d4..7d71bf194491 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/common/split_interval_by_days.ts @@ -1,5 +1,4 @@ import { dateUtils } from '@ts/core/utils/m_date'; -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import type { CompareOptions, DateInterval } from '../types'; @@ -27,7 +26,7 @@ export const splitIntervalByDay = ({ const result: DateInterval[] = []; while (time < maxTime) { - if (!skippedDays.includes(time.getUTCDay() as WeekdayIndex)) { + if (!skippedDays.includes(time.getUTCDay())) { const intervalMax = new Date(time); intervalMax.setUTCHours(endTime.hours, endTime.minutes, 0, 0); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts index f9f611ab4960..57ad9082a499 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_minutes_cell_intervals.ts @@ -1,5 +1,3 @@ -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; - import { splitIntervalByDay } from '../../common/split_interval_by_days'; import type { CellInterval, DateInterval } from '../../types'; @@ -8,14 +6,14 @@ interface Options { startDayHour: number; endDayHour: number; durationMinutes: number; - skippedDays: WeekdayIndex[]; + skippedDays: number[]; } const filterBySkippedDays = ( intervals: T[], - skippedDays: WeekdayIndex[], + skippedDays: number[], ): T[] => intervals.filter((item) => { - const weekday = new Date(item.min).getUTCDay() as WeekdayIndex; + const weekday = new Date(item.min).getUTCDay(); return !skippedDays.includes(weekday); }); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts index 40ca54f5c92c..9b8146dda0d7 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_one_day_cell_intervals.ts @@ -1,5 +1,3 @@ -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; - import { splitIntervalByDay } from '../../common/split_interval_by_days'; import type { CellInterval, DateInterval } from '../../types'; @@ -7,7 +5,7 @@ interface Options { intervals: DateInterval[]; startDayHour: number; endDayHour: number; - skippedDays: WeekdayIndex[]; + skippedDays: number[]; } export const getOneDayCellIntervals = ({ diff --git a/packages/devextreme/js/__internal/scheduler/view_model/types.ts b/packages/devextreme/js/__internal/scheduler/view_model/types.ts index d266d430ddd7..591d2be66ccc 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/types.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/types.ts @@ -4,7 +4,6 @@ import type { AllDayPanelModeType, SafeAppointment } from '../types'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; import type { ResourceManager } from '../utils/resource_manager/resource_manager'; import type { GroupLeaf } from '../utils/resource_manager/types'; -import type { WeekdayIndex } from '../utils/skipped_days'; import type { Empty, Geometry, @@ -30,7 +29,7 @@ export interface CompareOptions { endDayHour: number; min: number; max: number; - skippedDays: WeekdayIndex[]; + skippedDays: number[]; } export interface LayoutIntervals { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index b26da696c817..dbcfade80a11 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -50,7 +50,6 @@ import { isDateAndTimeView, } from '@ts/scheduler/r1/utils/index'; import type { ViewType } from '@ts/scheduler/types'; -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import Scrollable from '@ts/ui/scroll_view/scrollable'; import type NotifyScheduler from '../base/m_widget_notify_scheduler'; @@ -205,7 +204,7 @@ type WorkspaceOptionsInternal = Omit & { hoursInterval: number; startDayHour: number; endDayHour: number; - skippedDays: WeekdayIndex[]; + skippedDays: number[]; }; class SchedulerWorkSpace extends Widget { private viewDataProviderValue: any; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts index 996141e11302..5f4509955bdb 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_types.ts @@ -8,7 +8,6 @@ import type { } from '../../types'; import type { ResourceManager } from '../../utils/resource_manager/resource_manager'; import type { GroupLeaf } from '../../utils/resource_manager/types'; -import type { WeekdayIndex } from '../../utils/skipped_days'; interface CommonOptions extends CountGenerationConfig { getResourceManager: () => ResourceManager; @@ -17,7 +16,7 @@ interface CommonOptions extends CountGenerationConfig { viewOffset: number; hoursInterval: number; viewType: ViewType; - skippedDays?: WeekdayIndex[]; + skippedDays?: number[]; cellCount: number; isProvideVirtualCellsWidth: boolean; isGenerateTimePanelData?: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index e0acf5a33a55..9e51571c8647 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -1,7 +1,6 @@ import dateUtils from '@js/core/utils/date'; import { dateUtilsTs } from '@ts/core/utils/date'; import type { GroupLeaf } from '@ts/scheduler/utils/resource_manager/types'; -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; import { HORIZONTAL_GROUP_ORIENTATION } from '../../constants'; import timezoneUtils from '../../m_utils_time_zone'; @@ -39,7 +38,7 @@ export class ViewDataGenerator { public hiddenInterval = 0; - public skippedDays: WeekdayIndex[] = []; + public skippedDays: number[] = []; constructor(public readonly viewType: ViewType) {} @@ -65,7 +64,7 @@ export class ViewDataGenerator { return false; } - public getVisibleDaysOfWeek(firstDayOfWeek: number): WeekdayIndex[] { + public getVisibleDaysOfWeek(firstDayOfWeek: number): number[] { return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts index 3a7c31caaba5..fd4fefe42d49 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_work_week.ts @@ -1,22 +1,12 @@ -import type { WeekdayIndex } from '@ts/scheduler/utils/skipped_days'; - import { workWeekUtils } from '../../r1/utils/index'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; -interface WorkWeekStartViewDateOptions { - currentDate: Date; - startDayHour: number; - startDate: Date; - intervalCount: number; - firstDayOfWeek: number; -} - export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { protected baseDaysInInterval = 5; - public skippedDays: WeekdayIndex[] = [0, 6]; + public skippedDays: number[] = [0, 6]; - protected override calculateStartViewDate(options: WorkWeekStartViewDateOptions): Date { + protected override calculateStartViewDate(options: any): Date { return workWeekUtils.calculateStartViewDate( options.currentDate, options.startDayHour, From 2ccc5548a44ee4c585ace4cc8fe4f4c13ad80b66 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 13:27:40 +0200 Subject: [PATCH 28/30] fix: fix tests --- .../__internal/scheduler/workspaces/m_work_space.ts | 5 +++++ .../view_model/m_view_data_generator.test.ts | 13 +++++++++++++ .../workspaces/view_model/m_view_data_generator.ts | 4 +++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index dbcfade80a11..ec88f77e17a3 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1304,9 +1304,14 @@ class SchedulerWorkSpace extends Widget { return { startDayHour: this.option('startDayHour'), endDayHour: this.option('endDayHour'), + hoursInterval: this.option('hoursInterval'), interval: this.viewDataProvider.viewDataGenerator?.getInterval(this.option('hoursInterval')), + intervalCount: this.option('intervalCount'), startViewDate: this.getStartViewDate(), firstDayOfWeek: this.firstDayOfWeek(), + skippedDays: this.option('skippedDays'), + viewOffset: 0, + viewType: this.type, }; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index 4d253a29e6b8..422d3893ea56 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -161,6 +161,19 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { // After 5 visible days (10 cells), the next day jumps over weekend (+2 days). expect(callGetVisibleDayOffset(gen, 0, 10, 1, 2)).toBe(2); }); + + it('vertical workWeek layout uses column index as day index', () => { + const workWeekGen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + workWeekGen.skippedDays = [0, 6]; + + const verticalWorkWeek = workWeekGen as unknown as ViewDataGeneratorWeek; + + expect(callGetVisibleDayOffset(verticalWorkWeek, 0, 0, 3, 24)).toBe(0); + expect(callGetVisibleDayOffset(verticalWorkWeek, 0, 1, 3, 24)).toBe(0); + expect(callGetVisibleDayOffset(verticalWorkWeek, 0, 2, 3, 24)).toBe(0); + expect(callGetVisibleDayOffset(verticalWorkWeek, 0, 3, 3, 24)).toBe(2); + expect(callGetVisibleDayOffset(verticalWorkWeek, 0, 4, 3, 24)).toBe(2); + }); }); describe('getVisibleDayOffset for month-style layout', () => { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index 9e51571c8647..e56ad7abff4c 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts @@ -93,7 +93,9 @@ export class ViewDataGenerator { + ((targetDayOfWeek - anchorDay + 7) % 7); return actualDayOffset - naiveDayOffset; } - const dayIndex = Math.floor(columnIndex / cellCountInDay); + const dayIndex = isHorizontalView(this.viewType) + ? Math.floor(columnIndex / cellCountInDay) + : columnIndex; const week = Math.floor(dayIndex / visibleCount); const idxInWeek = dayIndex % visibleCount; const targetDayOfWeek = rotated[idxInWeek]; From faf52d412f0a25d4d98dc93ce423bea62fa2c51c Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 14:07:47 +0200 Subject: [PATCH 29/30] test: update snapshot for santiago timezone for workWeek --- .../__tests__/__snapshots__/santiago_timezone.test.ts.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__snapshots__/santiago_timezone.test.ts.snap b/packages/devextreme/js/__internal/scheduler/__tests__/__snapshots__/santiago_timezone.test.ts.snap index 2190e15e6280..7c2c8817b56c 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__snapshots__/santiago_timezone.test.ts.snap +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__snapshots__/santiago_timezone.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`scheduler should render correct workspace in Santiago DST for view: Day DST 1`] = ` [ @@ -307,7 +307,7 @@ exports[`scheduler should render correct workspace in Santiago DST for view: Tim "Wed 4", "Thu 5", "Fri 6", - "Sat 7", + "Mon 9", "12:00 AM", "6:00 AM", "12:00 PM", From c1b49692231a0bba1d2f2297423ceb328de31b99 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Fri, 10 Apr 2026 17:00:28 +0200 Subject: [PATCH 30/30] test: fix test --- .../view_model/m_view_data_generator.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts index 422d3893ea56..567179ed8237 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -150,16 +150,20 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { expect(callGetVisibleDayOffset(gen, 0, 4, 0, 1)).toBe(3); }); - it('timeline-like layout with multiple cells in day uses day index', () => { - gen.skippedDays = [0, 6]; + it('timelineWorkWeek with multiple cells in day uses day index', () => { + const timelineWorkWeekGen = new ViewDataGeneratorWorkWeek('timelineWorkWeek' as ViewType); + timelineWorkWeekGen.skippedDays = [0, 6]; + + const timelineWorkWeek = timelineWorkWeekGen as unknown as ViewDataGeneratorWeek; + // 2 cells per day, first visible week day is Monday (firstDayOfWeek=1) // Both cells of the first day must have the same offset. - expect(callGetVisibleDayOffset(gen, 0, 0, 1, 2)).toBe(0); - expect(callGetVisibleDayOffset(gen, 0, 1, 1, 2)).toBe(0); + expect(callGetVisibleDayOffset(timelineWorkWeek, 0, 0, 1, 2)).toBe(0); + expect(callGetVisibleDayOffset(timelineWorkWeek, 0, 1, 1, 2)).toBe(0); // The first cell of next visible day still has zero offset. - expect(callGetVisibleDayOffset(gen, 0, 2, 1, 2)).toBe(0); + expect(callGetVisibleDayOffset(timelineWorkWeek, 0, 2, 1, 2)).toBe(0); // After 5 visible days (10 cells), the next day jumps over weekend (+2 days). - expect(callGetVisibleDayOffset(gen, 0, 10, 1, 2)).toBe(2); + expect(callGetVisibleDayOffset(timelineWorkWeek, 0, 10, 1, 2)).toBe(2); }); it('vertical workWeek layout uses column index as day index', () => {