From 0016a3236752e0f42f8d2cdc5bfdd5334b900f00 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Tue, 7 Apr 2026 12:02:56 +0000 Subject: [PATCH 01/65] =?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/65] =?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/65] =?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/65] =?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/65] =?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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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/65] 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', () => { From 1fd1356e9ebf4ef9f472220a713e07cefcf7072a Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 08:54:06 +0200 Subject: [PATCH 31/65] feat: update W1029 error text --- packages/devextreme/js/ui/widget/ui.errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index dce5034ed7a1..a6c3f0dfd145 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: '"hiddenWeekDays" must leave at least one weekday visible.', }); From f58d384e15cca0ae7208eac2575537ca7deaa74e Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 10:21:56 +0200 Subject: [PATCH 32/65] refactor: remove weekends logic from workspace --- .../__tests__/workspace.base.test.ts | 38 +++++++++++ .../scheduler/header/m_utils.test.ts | 41 +++++++++++ .../js/__internal/scheduler/header/m_utils.ts | 68 +++++++++---------- .../scheduler/r1/utils/__tests__/base.test.ts | 11 +-- .../js/__internal/scheduler/r1/utils/base.ts | 9 --- .../js/__internal/scheduler/r1/utils/index.ts | 2 - .../scheduler/r1/utils/work_week.ts | 34 ++++------ .../scheduler/utils/skipped_days.ts | 22 ++++++ .../scheduler/workspaces/m_timeline.ts | 17 +++-- .../scheduler/workspaces/m_timeline_week.ts | 4 -- .../workspaces/m_timeline_work_week.ts | 19 ------ .../scheduler/workspaces/m_work_space.ts | 18 +++-- .../workspaces/m_work_space_indicator.ts | 6 +- .../workspaces/m_work_space_work_week.ts | 10 --- .../view_model/m_view_data_generator.test.ts | 32 +++++++++ .../view_model/m_view_data_generator.ts | 16 ----- .../m_view_data_generator_work_week.ts | 1 + 17 files changed, 213 insertions(+), 135 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index 17098be9585a..5f25a2b67abc 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -44,6 +44,7 @@ type WorkspaceConstructor = new (container: Element, options?: any) => T; const createWorkspace = ( WorkSpace: WorkspaceConstructor, currentView: string, + options?: any, ): { workspace: T; container: Element } => { const container = document.createElement('div'); const workspace = new WorkSpace(container, { @@ -52,6 +53,7 @@ const createWorkspace = ( currentDate: new Date(2017, 4, 25), firstDayOfWeek: 0, getResourceManager: () => getResourceManagerMock([]), + ...options, }); (workspace as any)._isVisible = () => true; expect(container.classList).toContain('dx-scheduler-work-space'); @@ -193,3 +195,39 @@ describe('scheduler workspace scrollTo', () => { expect(scrollableContainer.scrollLeft).toBeCloseTo(-11125); }); }); + +describe('scheduler workspace skipped days support', () => { + beforeEach(() => { + setupSchedulerTestEnvironment(); + }); + + it('should count configured skipped days in week workspace interval math', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWeek, 'week', { + skippedDays: [1, 3], + }); + + expect((workspace as any).getSkippedDaysCount(7, new Date(2026, 3, 5))).toBe(2); + }); + + it('should skip configured hidden days when incrementing timeline header dates', () => { + const { workspace } = createWorkspace(SchedulerTimelineWeek, 'timelineWeek', { + skippedDays: [3], + }); + const date = new Date(2026, 3, 7); // Tuesday + + (workspace as any).incrementDate(date); + + expect(date).toEqual(new Date(2026, 3, 9)); // Thursday + }); + + it('should respect empty skippedDays override in timeline work week', () => { + const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek', { + skippedDays: [], + }); + const date = new Date(2026, 3, 10); // Friday + + (workspace as any).incrementDate(date); + + expect(date).toEqual(new Date(2026, 3, 11)); // Saturday + }); +}); 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 169d12606b93..f77bc36a03fa 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -27,3 +27,44 @@ describe('agenda hiddenWeekDays support in header utils', () => { expect(getNextIntervalDate(options, -1)).toEqual(new Date(2026, 3, 8)); }); }); + +describe('workWeek hiddenWeekDays support in header utils', () => { + it('should keep Mon-Fri caption for default skippedDays', () => { + expect(getCaptionInterval({ + date: new Date(2026, 3, 8), // Wednesday + step: 'workWeek', + intervalCount: 1, + skippedDays: [0, 6], + firstDayOfWeek: 0, + })).toEqual({ + startDate: new Date(2026, 3, 6), + endDate: new Date(2026, 3, 10, 23, 59, 59, 999), + }); + }); + + it('should use full week caption when skippedDays override is empty', () => { + expect(getCaptionInterval({ + date: new Date(2026, 3, 8), // Wednesday + step: 'workWeek', + intervalCount: 1, + skippedDays: [], + firstDayOfWeek: 0, + })).toEqual({ + startDate: new Date(2026, 3, 5), + endDate: new Date(2026, 3, 11, 23, 59, 59, 999), + }); + }); + + it('should use first and last visible days for custom skippedDays', () => { + expect(getCaptionInterval({ + date: new Date(2026, 3, 8), // Wednesday + step: 'workWeek', + intervalCount: 1, + skippedDays: [1, 2], + firstDayOfWeek: 0, + })).toEqual({ + startDate: new Date(2026, 3, 5), + endDate: new Date(2026, 3, 11, 23, 59, 59, 999), + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 648d759c701d..6d19e74eca6d 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -10,14 +10,13 @@ import type { NormalizedView, RawViewType, ViewType } from '@ts/scheduler/utils/ import { getDateAfterVisibleDays, getFirstVisibleDate, + isDateSkipped, } from '@ts/scheduler/utils/skipped_days'; import type { Direction } from './constants'; const DAY_FORMAT = 'd'; -const DAYS_IN_WORK_WEEK = 5; - const { correctDateWithUnitBeginning: getPeriodStart, getFirstWeekDate: getWeekStart, @@ -33,9 +32,6 @@ const MS_DURATION = { milliseconds: 1 }; const DAY_DURATION = { days: 1 }; const WEEK_DURATION = { days: 7 }; -const SATURDAY_INDEX = 6; -const SUNDAY_INDEX = 0; - const subMS = (date: Date): Date => addDateInterval(date, MS_DURATION, -1); const addMS = (date: Date): Date => addDateInterval(date, MS_DURATION, 1); @@ -50,30 +46,29 @@ const nextMonth = (date: Date): Date => { return addDateInterval(date, { days }, 1); }; -const isWeekend = (date: Date): boolean => [SATURDAY_INDEX, SUNDAY_INDEX].includes(date.getDay()); - -const getWorkWeekStart = (firstDayOfWeek: Date): Date => { - let date = new Date(firstDayOfWeek); - while (isWeekend(date)) { - date = nextDay(date); - } - - return date; -}; - -const getDateAfterWorkWeek = (workWeekStart: Date): Date => { - let date = new Date(workWeekStart); - - let workDaysCount = 0; - while (workDaysCount < DAYS_IN_WORK_WEEK) { - if (!isWeekend(date)) { - workDaysCount += 1; - } +const getWorkWeekStart = ( + date: Date, + firstDayOfWeek: number | undefined, + skippedDays: number[], +): Date => getFirstVisibleDate( + getWeekStart(date, firstDayOfWeek), + skippedDays, + nextDay, +); + +const getDateAfterWorkWeek = ( + workWeekStart: Date, + firstDayOfWeek: number | undefined, + skippedDays: number[], +): Date => { + const weekStart = getWeekStart(workWeekStart, firstDayOfWeek); + let lastVisibleDate = addDateInterval(weekStart, WEEK_DURATION, 1); - date = nextDay(date); - } + do { + lastVisibleDate = addDateInterval(lastVisibleDate, DAY_DURATION, -1); + } while (isDateSkipped(lastVisibleDate, skippedDays)); - return date; + return nextDay(lastVisibleDate); }; const nextAgendaStart = ( @@ -98,7 +93,7 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { return weekStart; } case 'workWeek': - return getWorkWeekStart(getWeekStart(date, firstDayOfWeek)); + return getWorkWeekStart(date, firstDayOfWeek, skippedDays); case 'agenda': return new Date(date); default: @@ -111,6 +106,7 @@ const getPeriodEndDate = ( step: Step, agendaDuration: number, skippedDays: number[], + firstDayOfWeek: number | undefined, ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), @@ -123,7 +119,7 @@ const getPeriodEndDate = ( ) : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), - workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate), + workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate, firstDayOfWeek, skippedDays), agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), }; @@ -138,9 +134,7 @@ const getNextPeriodStartDate = ( let date = addMS(currentPeriodEndDate); if (step === 'workWeek') { - while (isWeekend(date)) { - date = nextDay(date); - } + date = getFirstVisibleDate(date, skippedDays, nextDay); } else if (step === 'week' && skippedDays.length > 0) { date = getFirstVisibleDate(date, skippedDays, nextDay); } @@ -150,7 +144,7 @@ const getNextPeriodStartDate = ( const getIntervalEndDate = (startDate: Date, options: IntervalOptions): Date => { const { - intervalCount, step, agendaDuration, skippedDays, + intervalCount, step, agendaDuration, skippedDays, firstDayOfWeek, } = options; let periodStartDate = new Date(startDate); @@ -160,7 +154,13 @@ const getIntervalEndDate = (startDate: Date, options: IntervalOptions): Date => for (let i = 0; i < intervalCount; i += 1) { periodStartDate = nextPeriodStartDate; - periodEndDate = getPeriodEndDate(periodStartDate, step, agendaDuration ?? 0, skippedDays); + periodEndDate = getPeriodEndDate( + periodStartDate, + step, + agendaDuration ?? 0, + skippedDays, + firstDayOfWeek, + ); nextPeriodStartDate = getNextPeriodStartDate(periodEndDate, step, skippedDays); } diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts index 2fed0b1a6b69..272c91a9b31a 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts @@ -13,12 +13,13 @@ import { getKeyByGroup, getSkippedHoursInRange, isAppointmentTakesAllDay, - isDataOnWeekend, isGroupingByDate, isHorizontalGroupingApplied, isVerticalGroupingApplied, } from '../index'; +const isWeekend = (date: Date): boolean => [0, 6].includes(date.getDay()); + describe('base utils', () => { describe('getDatesWithoutTime', () => { it('should trim dates correctly', () => { @@ -221,7 +222,7 @@ describe('base utils', () => { it('should skip 2 weekend days if startDate and endDate inside weekend', () => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isDataOnWeekend(date), + isSkippedDate: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour: 0, endDayHour: 24, @@ -243,7 +244,7 @@ describe('base utils', () => { describe('border conditions', () => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isDataOnWeekend(date), + isSkippedDate: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour: 0, endDayHour: 24, @@ -374,7 +375,7 @@ describe('base utils', () => { endDayHour, }) => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isDataOnWeekend(date), + isSkippedDate: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour, endDayHour, @@ -413,7 +414,7 @@ describe('base utils', () => { expectedHours, }) => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isDataOnWeekend(date), + isSkippedDate: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour: 11, endDayHour: 19, diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts index 2487bac3a6d9..13d7bf92c771 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts @@ -28,8 +28,6 @@ import { VIEWS } from '../../utils/options/constants_view'; const toMs = dateUtils.dateToMilliseconds; const DAY_HOURS = 24; const HOUR_IN_MS = 1000 * 60 * 60; -const SATURDAY_INDEX = 6; -const SUNDAY_INDEX = 0; const getDurationInHours = ( startDate: Date, @@ -431,13 +429,6 @@ export const getSkippedHoursInRange = ( return result; }; -export const isDataOnWeekend = (date: Date): boolean => { - const day = date.getDay(); - return day === SATURDAY_INDEX || day === SUNDAY_INDEX; -}; - -export const getWeekendsCount = (days: number): number => 2 * Math.floor(days / 7); - export const extendGroupItemsForGroupingByDate = ( groupRenderItems: GroupRenderItem[][], columnCountPerGroup: number, diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts index d62738a2a2cf..744de1c99b8a 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/index.ts @@ -66,9 +66,7 @@ export { getValidCellDateForLocalTimeFormat, getVerticalGroupCountClass, getViewStartByOptions, - getWeekendsCount, isAppointmentTakesAllDay, - isDataOnWeekend, isDateAndTimeView, isDateInRange, isFirstCellInMonthWithIntervalCount, diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/work_week.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/work_week.ts index db381e2eb616..7067e4689238 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/work_week.ts @@ -1,19 +1,17 @@ import dateUtils from '@js/core/utils/date'; -import type { CalculateStartViewDate } from '../../types'; -import { getViewStartByOptions, isDataOnWeekend, setOptionHour } from './base'; +import { getFirstVisibleDate } from '../../utils/skipped_days'; +import { getViewStartByOptions, setOptionHour } from './base'; import { getValidStartDate } from './week'; -const MONDAY_INDEX = 1; -const DAYS_IN_WEEK = 7; - -export const calculateStartViewDate: CalculateStartViewDate = ( - currentDate, - startDayHour, - startDate, - intervalDuration, - firstDayOfWeek, -) => { +export const calculateStartViewDate = ( + currentDate: Date, + startDayHour: number, + startDate: Date, + intervalDuration: number, + firstDayOfWeek: number | undefined, + skippedDays: number[] = [0, 6], +): Date => { const viewStart = getViewStartByOptions( startDate, currentDate, @@ -21,13 +19,11 @@ export const calculateStartViewDate: CalculateStartViewDate = ( getValidStartDate(startDate, firstDayOfWeek), ); - const firstViewDate = dateUtils.getFirstWeekDate(viewStart, firstDayOfWeek); - if (isDataOnWeekend(firstViewDate)) { - const currentDay = firstViewDate.getDay(); - const distance = (MONDAY_INDEX + DAYS_IN_WEEK - currentDay) % 7; - - firstViewDate.setDate(firstViewDate.getDate() + distance); - } + const firstViewDate = getFirstVisibleDate( + dateUtils.getFirstWeekDate(viewStart, firstDayOfWeek), + skippedDays, + (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1), + ); return setOptionHour(firstViewDate, startDayHour); }; diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index d4393c9ddc33..c0d3cc12fac7 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -58,3 +58,25 @@ export const getDateAfterVisibleDays = ( return date; }; + +export const getSkippedDaysCount = ( + start: Date, + dayCount: number, + skippedDays: number[], +): number => { + if (dayCount <= 0 || skippedDays.length === 0) { + return 0; + } + + const date = new Date(start); + let skippedCount = 0; + + for (let i = 0; i < dayCount; i += 1) { + if (isDateSkipped(date, skippedDays)) { + skippedCount += 1; + } + date.setDate(date.getDate() + 1); + } + + return skippedCount; +}; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index e66d081086ff..9b20a96b6358 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -109,6 +109,9 @@ class SchedulerTimeline extends SchedulerWorkSpace { protected incrementDate(date) { date.setDate(date.getDate() + 1); + while (this.option('skippedDays').includes(date.getDay())) { + date.setDate(date.getDate() + 1); + } } getIndicationCellCount() { @@ -133,6 +136,10 @@ class SchedulerTimeline extends SchedulerWorkSpace { protected calculateDurationInCells(timeDiff) { const today = this.getToday(); const differenceInDays = Math.floor(timeDiff / toMs('day')); + const skippedDaysCount = this.getSkippedDaysCount( + differenceInDays, + this.getIndicationFirstViewDate(), + ); let duration = (timeDiff - differenceInDays * toMs('day') - (this.option('startDayHour') as any) * toMs('hour')) / this.getCellDuration(); if (today.getHours() > (this.option('endDayHour') as any)) { @@ -142,7 +149,7 @@ class SchedulerTimeline extends SchedulerWorkSpace { if (duration < 0) { duration = 0; } - return differenceInDays * this.getCellCountInDay() + duration; + return (differenceInDays - skippedDaysCount) * this.getCellCountInDay() + duration; } getIndicationWidth() { @@ -223,7 +230,8 @@ class SchedulerTimeline extends SchedulerWorkSpace { const fullDays = Math.floor(fullInterval / toMs('day')); const tailDuration = fullInterval - (fullDays * toMs('day')); let tailDelta = 0; - const cellCount = this.getCellCountInDay() * (fullDays - this.getWeekendsCount(fullDays)); + const skippedDaysCount = this.getSkippedDaysCount(fullDays, firstViewDate); + const cellCount = this.getCellCountInDay() * (fullDays - skippedDaysCount); const gapBeforeAppt = apptStart - dateUtils.trimTime(new Date(currentDate)).getTime(); let result = cellCount * (this.option('hoursInterval') as any) * toMs('hour'); @@ -253,11 +261,6 @@ class SchedulerTimeline extends SchedulerWorkSpace { return result; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected override getWeekendsCount(argument?: any) { - return 0; - } - getAllDayContainer() { return null; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_week.ts index 0f38e0b3cdd5..96851eb816b5 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_week.ts @@ -15,10 +15,6 @@ export default class SchedulerTimelineWeek extends SchedulerTimeline { protected override needRenderWeekHeader() { return true; } - - protected override incrementDate(date) { - date.setDate(date.getDate() + 1); - } } registerComponent('dxSchedulerTimelineWeek', SchedulerTimelineWeek as any); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts index f4fe25697191..155d5868a52f 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts @@ -1,35 +1,16 @@ import registerComponent from '@js/core/component_registrator'; -import { - getWeekendsCount, -} from '@ts/scheduler/r1/utils/index'; import { VIEWS } from '../utils/options/constants_view'; import SchedulerTimelineWeek from './m_timeline_week'; const TIMELINE_CLASS = 'dx-scheduler-timeline-work-week'; -const LAST_DAY_WEEK_INDEX = 5; class SchedulerTimelineWorkWeek extends SchedulerTimelineWeek { get type() { return VIEWS.TIMELINE_WORK_WEEK; } - constructor(...args: any[]) { - // @ts-expect-error - super(...args); - - this.getWeekendsCount = getWeekendsCount; - } - protected override getElementClass() { return TIMELINE_CLASS; } - - protected override incrementDate(date) { - const day = date.getDay(); - if (day === LAST_DAY_WEEK_INDEX) { - date.setDate(date.getDate() + 2); - } - super.incrementDate(date); - } } registerComponent('dxSchedulerTimelineWorkWeek', SchedulerTimelineWorkWeek as any); 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 ec88f77e17a3..119ff1ed3745 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -80,6 +80,7 @@ import { import { getLeafGroupValues } from '../utils/resource_manager/group_utils'; import type { ResourceManager } from '../utils/resource_manager/resource_manager'; import type { GroupValues, RawGroupValues } from '../utils/resource_manager/types'; +import { getSkippedDaysCount as countSkippedDays } from '../utils/skipped_days'; import { getAllDayHeight, getCellHeight, @@ -1319,26 +1320,29 @@ class SchedulerWorkSpace extends Widget { protected getIntervalBetween(currentDate, allDay) { const firstViewDate = this.getStartViewDate(); - const startDayTime = (this.option('startDayHour') as any) * HOUR_MS; + const startDayTime = this.option('startDayHour') * HOUR_MS; const timeZoneOffset = dateUtils.getTimezonesDifference(firstViewDate, currentDate); const fullInterval = currentDate.getTime() - firstViewDate.getTime() - timeZoneOffset; const days = this.getDaysOfInterval(fullInterval, startDayTime); - const weekendsCount = this.getWeekendsCount(days); - let result = (days - weekendsCount) * DAY_MS; + const skippedDaysCount = this.getSkippedDaysCount(days, firstViewDate); + let result = (days - skippedDaysCount) * DAY_MS; if (!allDay) { const { hiddenInterval } = this.viewDataProvider; const visibleDayDuration = this.getVisibleDayDuration(); - result = fullInterval - days * hiddenInterval - weekendsCount * visibleDayDuration; + result = fullInterval - days * hiddenInterval - skippedDaysCount * visibleDayDuration; } return result; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getWeekendsCount(argument?: any) { - return 0; + protected getSkippedDaysCount(days: number, startDate: Date = this.getStartViewDate()) { + return countSkippedDays( + startDate, + days, + this.option('skippedDays'), + ); } private getDaysOfInterval(fullInterval, startDayTime) { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts index ce2e3250776e..2bad32821cb9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts @@ -127,9 +127,9 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { const viewStartTime = this.getStartViewDate().getTime(); let timeDiff = today.getTime() - viewStartTime; - if (this.option('type') === 'workWeek') { - const weekendDays = this.getWeekendsCount(Math.round(timeDiff / toMs('day'))) * toMs('day'); - timeDiff -= weekendDays; + if ((this.option('skippedDays').length > 0)) { + const skippedDaysDuration = this.getSkippedDaysCount(Math.round(timeDiff / toMs('day'))) * toMs('day'); + timeDiff -= skippedDaysDuration; } return Math.ceil((timeDiff + 1) / toMs('day')); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts index c0b43a1bc5aa..57c27f341df9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts @@ -1,7 +1,4 @@ import registerComponent from '@js/core/component_registrator'; -import { - getWeekendsCount, -} from '@ts/scheduler/r1/utils/index'; import { VIEWS } from '../utils/options/constants_view'; import SchedulerWorkSpaceWeek from './m_work_space_week'; @@ -10,13 +7,6 @@ const WORK_WEEK_CLASS = 'dx-scheduler-work-space-work-week'; class SchedulerWorkSpaceWorkWeek extends SchedulerWorkSpaceWeek { get type() { return VIEWS.WORK_WEEK; } - constructor(...args: any[]) { - // @ts-expect-error - super(...args); - - this.getWeekendsCount = getWeekendsCount; - } - protected override getElementClass() { return WORK_WEEK_CLASS; } 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 567179ed8237..394733be965d 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 @@ -267,6 +267,38 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { }); describe('WorkWeek hiddenWeekDays support', () => { + it('uses week start when skippedDays override is empty', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + gen.skippedDays = []; + + const startViewDate = gen.getStartViewDate({ + currentDate: new Date(2026, 3, 1), // Wednesday + startDayHour: 0, + startDate: undefined, + intervalCount: 1, + firstDayOfWeek: 0, // Sunday + }); + + expect(startViewDate.getDay()).toBe(0); + expect(startViewDate.getDate()).toBe(29); + }); + + it('uses first visible day of week for custom skippedDays', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); + gen.skippedDays = [1, 2]; + + const startViewDate = gen.getStartViewDate({ + currentDate: new Date(2026, 3, 1), // Wednesday + startDayHour: 0, + startDate: undefined, + intervalCount: 1, + firstDayOfWeek: 0, // Sunday + }); + + expect(startViewDate.getDay()).toBe(0); + expect(startViewDate.getDate()).toBe(29); + }); + it('keeps first visible column on Monday when startViewDate is already Monday', () => { const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); gen.skippedDays = [0, 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 e56ad7abff4c..2cacf4006534 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 @@ -563,7 +563,6 @@ export class ViewDataGenerator { hoursInterval, interval, firstDayOfWeek, - intervalCount, viewOffset, } = options; const cellCountInDay = this.getCellCountInDay(startDayHour, endDayHour, hoursInterval); @@ -581,13 +580,6 @@ 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; } @@ -631,14 +623,6 @@ export class ViewDataGenerator { return interval * cellIndex + realHiddenInterval; } - getTimeOffsetByColumnIndex(columnIndex, firstDayOfWeek, columnCount, intervalCount) { - const firstDayOfWeekDiff = Math.max(0, firstDayOfWeek - 1); - const columnsInWeek = columnCount / intervalCount; - const weekendCount = Math.floor((columnIndex + firstDayOfWeekDiff) / columnsInWeek); - - return weekendCount * 2 * toMs('day'); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public calculateEndDate(startDate: Date, interval: number, endDayHour?: any): Date { return this.getCellEndDate(startDate, { interval }); 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 fd4fefe42d49..0c3b6afe1616 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 @@ -13,6 +13,7 @@ export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { options.startDate, this._getIntervalDuration(options.intervalCount), this.getFirstDayOfWeek(options.firstDayOfWeek), + this.skippedDays, ); } From 433c33f664ed04bd8d12ac508a17fd9d04d55724 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 10:51:39 +0200 Subject: [PATCH 33/65] fix: fix defaults for skippedDays in workWeek --- .../__tests__/workspace.base.test.ts | 21 +++++++++++++++++++ .../js/__internal/scheduler/header/m_utils.ts | 7 +++---- .../workspaces/m_timeline_work_week.ts | 7 +++++++ .../workspaces/m_work_space_work_week.ts | 7 +++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index 5f25a2b67abc..b4883c11978e 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -201,6 +201,18 @@ describe('scheduler workspace skipped days support', () => { setupSchedulerTestEnvironment(); }); + it('should use weekend skippedDays by default in work week workspace', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek'); + + expect(workspace.option('skippedDays')).toEqual([0, 6]); + }); + + it('should use weekend skippedDays by default in timeline work week workspace', () => { + const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek'); + + expect(workspace.option('skippedDays')).toEqual([0, 6]); + }); + it('should count configured skipped days in week workspace interval math', () => { const { workspace } = createWorkspace(SchedulerWorkSpaceWeek, 'week', { skippedDays: [1, 3], @@ -230,4 +242,13 @@ describe('scheduler workspace skipped days support', () => { expect(date).toEqual(new Date(2026, 3, 11)); // Saturday }); + + it('should skip weekend by default in timeline work week', () => { + const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek'); + const date = new Date(2026, 3, 10); // Friday + + (workspace as any).incrementDate(date); + + expect(date).toEqual(new Date(2026, 3, 13)); // Monday + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 6d19e74eca6d..acb988c23b44 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -62,11 +62,10 @@ const getDateAfterWorkWeek = ( skippedDays: number[], ): Date => { const weekStart = getWeekStart(workWeekStart, firstDayOfWeek); - let lastVisibleDate = addDateInterval(weekStart, WEEK_DURATION, 1); - - do { + let lastVisibleDate = addDateInterval(weekStart, { days: 6 }, 1); + while (isDateSkipped(lastVisibleDate, skippedDays)) { lastVisibleDate = addDateInterval(lastVisibleDate, DAY_DURATION, -1); - } while (isDateSkipped(lastVisibleDate, skippedDays)); + } return nextDay(lastVisibleDate); }; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts index 155d5868a52f..bfc486eb75db 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts @@ -1,4 +1,5 @@ import registerComponent from '@js/core/component_registrator'; +import { extend } from '@js/core/utils/extend'; import { VIEWS } from '../utils/options/constants_view'; import SchedulerTimelineWeek from './m_timeline_week'; @@ -8,6 +9,12 @@ const TIMELINE_CLASS = 'dx-scheduler-timeline-work-week'; class SchedulerTimelineWorkWeek extends SchedulerTimelineWeek { get type() { return VIEWS.TIMELINE_WORK_WEEK; } + _getDefaultOptions() { + return extend(super._getDefaultOptions(), { + skippedDays: [0, 6], + }); + } + protected override getElementClass() { return TIMELINE_CLASS; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts index 57c27f341df9..02fb2ae2c9a1 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts @@ -1,4 +1,5 @@ import registerComponent from '@js/core/component_registrator'; +import { extend } from '@js/core/utils/extend'; import { VIEWS } from '../utils/options/constants_view'; import SchedulerWorkSpaceWeek from './m_work_space_week'; @@ -7,6 +8,12 @@ const WORK_WEEK_CLASS = 'dx-scheduler-work-space-work-week'; class SchedulerWorkSpaceWorkWeek extends SchedulerWorkSpaceWeek { get type() { return VIEWS.WORK_WEEK; } + _getDefaultOptions() { + return extend(super._getDefaultOptions(), { + skippedDays: [0, 6], + }); + } + protected override getElementClass() { return WORK_WEEK_CLASS; } From 418467c731a3b37f7fb2f80e2d3e05f983d0bb75 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 11:00:06 +0200 Subject: [PATCH 34/65] fix: undo default value for skipWeekDays --- .../devextreme/js/__internal/scheduler/utils/skipped_days.ts | 4 ++-- .../js/__internal/scheduler/workspaces/m_timeline.ts | 2 +- .../js/__internal/scheduler/workspaces/m_work_space.ts | 4 ++-- .../__internal/scheduler/workspaces/m_work_space_indicator.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index c0d3cc12fac7..604d26d6ebf9 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -62,9 +62,9 @@ export const getDateAfterVisibleDays = ( export const getSkippedDaysCount = ( start: Date, dayCount: number, - skippedDays: number[], + skippedDays?: number[], ): number => { - if (dayCount <= 0 || skippedDays.length === 0) { + if (dayCount <= 0 || !skippedDays || skippedDays.length === 0) { return 0; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index 9b20a96b6358..9164077d3ea6 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -109,7 +109,7 @@ class SchedulerTimeline extends SchedulerWorkSpace { protected incrementDate(date) { date.setDate(date.getDate() + 1); - while (this.option('skippedDays').includes(date.getDay())) { + while ((this.option('skippedDays') ?? []).includes(date.getDay())) { date.setDate(date.getDate() + 1); } } 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 119ff1ed3745..98a53b6ae209 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -205,7 +205,7 @@ type WorkspaceOptionsInternal = Omit & { hoursInterval: number; startDayHour: number; endDayHour: number; - skippedDays: number[]; + skippedDays?: number[]; }; class SchedulerWorkSpace extends Widget { private viewDataProviderValue: any; @@ -2302,7 +2302,7 @@ class SchedulerWorkSpace extends Widget { groupOrientation: 'horizontal', selectedCellData: [], groupByDate: false, - skippedDays: [], + skippedDays: undefined, scrolling: { mode: 'standard', }, diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts index 2bad32821cb9..90084b414233 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts @@ -127,7 +127,7 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { const viewStartTime = this.getStartViewDate().getTime(); let timeDiff = today.getTime() - viewStartTime; - if ((this.option('skippedDays').length > 0)) { + if (((this.option('skippedDays')) ?? []).length > 0) { const skippedDaysDuration = this.getSkippedDaysCount(Math.round(timeDiff / toMs('day'))) * toMs('day'); timeDiff -= skippedDaysDuration; } From 11f6b4f0ea87f29bf962aefe98163bc4ef93a1e0 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 13:06:28 +0200 Subject: [PATCH 35/65] fix: fix column rendering for workWeek --- .../workspaces/view_model/m_view_data_generator_work_week.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0c3b6afe1616..4cda019c8419 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 @@ -2,7 +2,7 @@ import { workWeekUtils } from '../../r1/utils/index'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { - protected baseDaysInInterval = 5; + protected baseDaysInInterval = 7; public skippedDays: number[] = [0, 6]; From 3c255b3775bb538269742b5222d8c2e320d49bd0 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 13:50:41 +0200 Subject: [PATCH 36/65] fix: fix caption in navigator for week view --- .../scheduler/header/m_utils.test.ts | 15 +++++++++++++ .../js/__internal/scheduler/header/m_utils.ts | 22 +++++++++++-------- .../scheduler/utils/skipped_days.ts | 22 ------------------- 3 files changed, 28 insertions(+), 31 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 f77bc36a03fa..fdb94577a8a9 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -68,3 +68,18 @@ describe('workWeek hiddenWeekDays support in header utils', () => { }); }); }); + +describe('week hiddenWeekDays support in header utils', () => { + it('should use first and last visible day for week caption', () => { + expect(getCaptionInterval({ + date: new Date(2026, 3, 8), // Wednesday + step: 'week', + intervalCount: 1, + skippedDays: [0, 1], + firstDayOfWeek: 1, // Monday + })).toEqual({ + startDate: new Date(2026, 3, 7), + endDate: new Date(2026, 3, 11, 23, 59, 59, 999), + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index acb988c23b44..67fb6e822bf6 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -8,7 +8,6 @@ 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 { - getDateAfterVisibleDays, getFirstVisibleDate, isDateSkipped, } from '@ts/scheduler/utils/skipped_days'; @@ -70,6 +69,18 @@ const getDateAfterWorkWeek = ( return nextDay(lastVisibleDate); }; +const getDateAfterWeek = ( + weekStartDate: Date, + firstDayOfWeek: number | undefined, + skippedDays: number[], +): Date => { + if (skippedDays.length === 0) { + return nextWeek(weekStartDate); + } + + return getDateAfterWorkWeek(weekStartDate, firstDayOfWeek, skippedDays); +}; + const nextAgendaStart = ( date: Date, agendaDuration: number, @@ -109,14 +120,7 @@ const getPeriodEndDate = ( ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), - week: () => (skippedDays.length > 0 - ? getDateAfterVisibleDays( - currentPeriodStartDate, - 7 - skippedDays.length, - skippedDays, - nextDay, - ) - : nextWeek(currentPeriodStartDate)), + week: () => getDateAfterWeek(currentPeriodStartDate, firstDayOfWeek, skippedDays), month: () => nextMonth(currentPeriodStartDate), workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate, firstDayOfWeek, skippedDays), agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index 604d26d6ebf9..a312f9d855cc 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -37,28 +37,6 @@ export const getFirstVisibleDate = ( return date; }; -export const getDateAfterVisibleDays = ( - start: Date, - visibleDayCount: number, - skippedDays: number[], - nextDate: (date: Date) => Date, -): Date => { - if (visibleDayCount <= 0) { - return new Date(start); - } - - let date = new Date(start); - let visited = 0; - while (visited < visibleDayCount) { - date = nextDate(date); - if (!isDateSkipped(date, skippedDays)) { - visited += 1; - } - } - - return date; -}; - export const getSkippedDaysCount = ( start: Date, dayCount: number, From 5e87ccc60ca8361a54360859e55f032a464f2124 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 14:01:42 +0200 Subject: [PATCH 37/65] test: optimize tests --- .../__tests__/workspace.base.test.ts | 12 ---- .../view_model/m_view_data_generator.test.ts | 64 +------------------ 2 files changed, 3 insertions(+), 73 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index b4883c11978e..bb526f913121 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -201,18 +201,6 @@ describe('scheduler workspace skipped days support', () => { setupSchedulerTestEnvironment(); }); - it('should use weekend skippedDays by default in work week workspace', () => { - const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek'); - - expect(workspace.option('skippedDays')).toEqual([0, 6]); - }); - - it('should use weekend skippedDays by default in timeline work week workspace', () => { - const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek'); - - expect(workspace.option('skippedDays')).toEqual([0, 6]); - }); - it('should count configured skipped days in week workspace interval math', () => { const { workspace } = createWorkspace(SchedulerWorkSpaceWeek, 'week', { skippedDays: [1, 3], 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 394733be965d..6dd178be4410 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 @@ -1,65 +1,13 @@ 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'; describe('ViewDataGenerator hiddenWeekDays 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); - }); - - 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', () => { - 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]; @@ -72,10 +20,10 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { expect(gen.daysInInterval).toBe(5); }); - it('day view: 1 (unaffected by skippedDays)', () => { - const gen = new ViewDataGenerator('day' as ViewType); + it('workWeek view: 7 with empty skippedDays override', () => { + const gen = new ViewDataGeneratorWorkWeek('workWeek' as ViewType); gen.skippedDays = []; - expect(gen.daysInInterval).toBe(1); + expect(gen.daysInInterval).toBe(7); }); }); @@ -222,12 +170,6 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { }); 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]; From 862fffea1978a1229d7691aac857e84d25191d2e Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 14:05:55 +0200 Subject: [PATCH 38/65] refactor: undo typing --- .../js/__internal/scheduler/utils/options/constants_view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6585e5bcc9a4..d01f374237bd 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts @@ -13,7 +13,7 @@ export const VIEWS: Record = { }; export const VIEW_TYPES: ViewType[] = Object.values(VIEWS); -const WEEKENDS: number[] = [0, 6]; +const WEEKENDS = [0, 6]; const getView = ( type: ViewType, groupOrientation: View['groupOrientation'], From 33e2a6b85b7e4ed5fb2cc57376f94949eb768643 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 14:28:32 +0200 Subject: [PATCH 39/65] test: add tests for work week layout with skippedDays scenarios --- .../__tests__/workspace.base.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index bb526f913121..173a04c34ea3 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -209,6 +209,28 @@ describe('scheduler workspace skipped days support', () => { expect((workspace as any).getSkippedDaysCount(7, new Date(2026, 3, 5))).toBe(2); }); + it('should use full week layout for work week when skippedDays override is empty', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek', { + currentDate: new Date(2026, 3, 1), // Wednesday + firstDayOfWeek: 0, // Sunday + skippedDays: [], + }); + + expect(workspace.getStartViewDate()).toEqual(new Date(2026, 2, 29)); + expect((workspace as any)._getCellCount()).toBe(7); + }); + + it('should use custom skippedDays in work week runtime layout', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek', { + currentDate: new Date(2026, 3, 1), // Wednesday + firstDayOfWeek: 0, // Sunday + skippedDays: [3], // Wednesday + }); + + expect(workspace.getStartViewDate()).toEqual(new Date(2026, 2, 29)); + expect((workspace as any)._getCellCount()).toBe(6); + }); + it('should skip configured hidden days when incrementing timeline header dates', () => { const { workspace } = createWorkspace(SchedulerTimelineWeek, 'timelineWeek', { skippedDays: [3], From 8a9989c96213e7a5e4c5ef9cdd463aa22f6d5488 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 14:47:25 +0200 Subject: [PATCH 40/65] feat: make global hiddenWeekDays affect timelineWorkWeek and workweek --- .../scheduler/utils/options/utils.test.ts | 44 +++++++++++-------- .../scheduler/utils/options/utils.ts | 9 +--- 2 files changed, 27 insertions(+), 26 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 a82da2a4cc40..310677cbe28a 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -148,55 +148,59 @@ describe('views utils', () => { return view?.skippedDays ?? []; }; - it('per-view hiddenWeekDays on week → uses per-view value', () => { + it('uses per-view hiddenWeekDays for week', () => { expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); }); - it('per-view hiddenWeekDays: [] on workWeek → overrides built-in default', () => { + it('lets workWeek override the default weekends with an empty list', () => { expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); }); - it('per-view hiddenWeekDays on workWeek → overrides built-in default', () => { + it('lets workWeek override the default weekends with custom days', () => { expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek')).toEqual([3]); }); - it('global hiddenWeekDays on workWeek → ignored, built-in default wins', () => { - expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([0, 6]); + it('applies global hiddenWeekDays to workWeek', () => { + expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([3]); }); - it('global hiddenWeekDays on week → applied', () => { + it('applies global hiddenWeekDays to timelineWorkWeek', () => { + expect(getSkipped(['timelineWorkWeek'], 'timelineWorkWeek', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to week', () => { expect(getSkipped(['week'], 'week', [3])).toEqual([3]); }); - it('global hiddenWeekDays on month → applied', () => { + it('applies global hiddenWeekDays to month', () => { expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); }); - it('global hiddenWeekDays on timelineWeek → applied', () => { + it('applies global hiddenWeekDays to timelineWeek', () => { expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); }); - it('global hiddenWeekDays on timelineMonth → applied', () => { + it('applies global hiddenWeekDays to timelineMonth', () => { expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); }); - it('global hiddenWeekDays on day → ignored (unsupported view)', () => { + it('ignores global hiddenWeekDays for day', () => { expect(getSkipped(['day'], 'day', [3])).toEqual([]); }); - it('global hiddenWeekDays on agenda → applied', () => { + it('applies global hiddenWeekDays to agenda', () => { expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([3]); }); - it('per-view hiddenWeekDays dedupes duplicates', () => { + it('removes duplicates from per-view hiddenWeekDays', () => { expect(getSkipped([{ type: 'week', hiddenWeekDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); }); - it('per-view hiddenWeekDays sorts ascending', () => { + it('sorts per-view hiddenWeekDays', () => { expect(getSkipped([{ type: 'week', hiddenWeekDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); }); - it('per-view hiddenWeekDays filters out invalid values', () => { + it('filters out invalid per-view hiddenWeekDays values', () => { expect( getSkipped([ // @ts-expect-error intentionally pass invalid values to verify runtime filtering @@ -205,7 +209,7 @@ describe('views utils', () => { ).toEqual([3]); }); - it('hiddenWeekDays covering all 7 days → falls back to [] and logs W1029', () => { + it('falls back to an empty list and logs error when all days are hidden', () => { const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); try { expect( @@ -217,15 +221,19 @@ describe('views utils', () => { } }); - it('global hiddenWeekDays + per-view undefined on week → uses global', () => { + it('uses global hiddenWeekDays when week does not define its own value', () => { expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); }); - it('global hiddenWeekDays + per-view [3] on week → per-view wins', () => { + it('keeps the per-view value for week when both global and per-view hiddenWeekDays are set', () => { expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week', [0, 6])).toEqual([3]); }); - it('no hiddenWeekDays anywhere on week → []', () => { + it('keeps the per-view value for workWeek when both global and per-view hiddenWeekDays are set', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek', [0, 6])).toEqual([3]); + }); + + it('returns an empty list for week when hiddenWeekDays is not set anywhere', () => { 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 9a3e1e75cb99..033c09b39908 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -11,11 +11,7 @@ import type { } from './types'; const VIEWS_SUPPORTING_HIDDEN_DAYS: ReadonlySet = new Set([ - 'week', 'month', 'timelineWeek', 'timelineMonth', 'agenda', -]); - -const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ - 'workWeek', 'timelineWorkWeek', + 'week', 'workWeek', 'month', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth', 'agenda', ]); const normalizeHiddenWeekDays = ( @@ -44,9 +40,6 @@ const resolveSkippedDays = ( if (perView !== undefined) { return perView; } - if (VIEWS_WITH_BUILTIN_SKIPPED.has(viewType)) { - return viewDefault; - } if (globalHiddenWeekDays !== undefined && VIEWS_SUPPORTING_HIDDEN_DAYS.has(viewType)) { return normalizeHiddenWeekDays(globalHiddenWeekDays) ?? []; } From daa6e6cf999ee9c320918b9ef43a79b191d1b8c6 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 13 Apr 2026 15:39:00 +0200 Subject: [PATCH 41/65] fix: fix test --- .../workspaces/view_model/m_view_data_generator_work_week.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4cda019c8419..fe424a1e2541 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 @@ -13,7 +13,7 @@ export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { options.startDate, this._getIntervalDuration(options.intervalCount), this.getFirstDayOfWeek(options.firstDayOfWeek), - this.skippedDays, + options.skippedDays ?? this.skippedDays, ); } From 9d82f1ccee271139b00f43729b689510b19c0eed Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 14 Apr 2026 15:12:26 +0200 Subject: [PATCH 42/65] fix: remove infinite loop risk from util functions --- .../js/__internal/scheduler/utils/skipped_days.ts | 4 ++++ .../js/__internal/scheduler/workspaces/m_timeline.ts | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts index a312f9d855cc..937ffa8f4526 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -30,6 +30,10 @@ export const getFirstVisibleDate = ( skippedDays: number[], nextDate: (date: Date) => Date, ): Date => { + if (skippedDays.length >= 7) { + return new Date(start); + } + let date = new Date(start); while (isDateSkipped(date, skippedDays)) { date = nextDate(date); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index 9164077d3ea6..7e696a8ef77b 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -108,8 +108,14 @@ class SchedulerTimeline extends SchedulerWorkSpace { } protected incrementDate(date) { + const skippedDays = this.option('skippedDays') ?? []; + + if (skippedDays.length >= 7) { + return; + } + date.setDate(date.getDate() + 1); - while ((this.option('skippedDays') ?? []).includes(date.getDay())) { + while (skippedDays.includes(date.getDay())) { date.setDate(date.getDate() + 1); } } From 39507163c8804f314847da2f4bd88969df4c126a Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 15 Apr 2026 12:05:26 +0200 Subject: [PATCH 43/65] Apply suggestion from @arman-boyakhchyan Co-authored-by: Arman Boyakhchyan Signed-off-by: Sergei Burkatskii --- packages/devextreme/js/ui/widget/ui.errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index a6c3f0dfd145..9027c1cba511 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: '"hiddenWeekDays" must leave at least one weekday visible.', + W1029: '\'hiddenWeekDays'\ must leave at least one weekday visible.', }); From 60b6e24b5ac3b399d290322d7bfde50fdb8ed320 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 12:08:49 +0200 Subject: [PATCH 44/65] feat: update .d.ts for DayOfWeek type --- .../devextreme-angular/src/ui/scheduler/index.ts | 14 +++++++------- .../src/ui/scheduler/nested/view-dxi.ts | 6 +++--- packages/devextreme-react/src/scheduler.ts | 4 ++-- packages/devextreme-vue/src/scheduler.ts | 5 +++-- packages/devextreme/js/ui/scheduler.d.ts | 6 ++++-- packages/devextreme/js/ui/scheduler_types.d.ts | 1 + packages/devextreme/ts/dx.all.d.ts | 5 +++-- 7 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/devextreme-angular/src/ui/scheduler/index.ts b/packages/devextreme-angular/src/ui/scheduler/index.ts index adbcc7672314..9d326c24d2f8 100644 --- a/packages/devextreme-angular/src/ui/scheduler/index.ts +++ b/packages/devextreme-angular/src/ui/scheduler/index.ts @@ -24,7 +24,7 @@ import { import type dxSortable from 'devextreme/ui/sortable'; import type dxDraggable from 'devextreme/ui/draggable'; -import type { default as dxScheduler, AllDayPanelMode, ViewType, dxSchedulerAppointment, AppointmentFormProperties, CellAppointmentsLimit, AppointmentAddedEvent, AppointmentAddingEvent, AppointmentClickEvent, AppointmentContextMenuEvent, AppointmentDblClickEvent, AppointmentDeletedEvent, AppointmentDeletingEvent, AppointmentFormOpeningEvent, AppointmentRenderedEvent, AppointmentTooltipShowingEvent, AppointmentUpdatedEvent, AppointmentUpdatingEvent, CellClickEvent, CellContextMenuEvent, ContentReadyEvent, DisposingEvent, InitializedEvent, OptionChangedEvent, RecurrenceEditMode, dxSchedulerScrolling, SnapToCellsMode, dxSchedulerToolbar } from 'devextreme/ui/scheduler'; +import type { default as dxScheduler, AllDayPanelMode, ViewType, dxSchedulerAppointment, AppointmentFormProperties, DayOfWeek, CellAppointmentsLimit, AppointmentAddedEvent, AppointmentAddingEvent, AppointmentClickEvent, AppointmentContextMenuEvent, AppointmentDblClickEvent, AppointmentDeletedEvent, AppointmentDeletingEvent, AppointmentFormOpeningEvent, AppointmentRenderedEvent, AppointmentTooltipShowingEvent, AppointmentUpdatedEvent, AppointmentUpdatingEvent, CellClickEvent, CellContextMenuEvent, ContentReadyEvent, DisposingEvent, InitializedEvent, OptionChangedEvent, RecurrenceEditMode, dxSchedulerScrolling, SnapToCellsMode, dxSchedulerToolbar } from 'devextreme/ui/scheduler'; import type { event } from 'devextreme/events/events.types'; import type { default as DataSource, DataSourceOptions } from 'devextreme/data/data_source'; import type { Store } from 'devextreme/data/store'; @@ -517,10 +517,10 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh @Input() - get hiddenWeekDays(): Array { + get hiddenWeekDays(): Array { return this._getOption('hiddenWeekDays'); } - set hiddenWeekDays(value: Array) { + set hiddenWeekDays(value: Array) { this._setOption('hiddenWeekDays', value); } @@ -904,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, 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 }[] { + 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, 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 }[]) { + 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); } @@ -1289,7 +1289,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() hiddenWeekDaysChange: EventEmitter>; + @Output() hiddenWeekDaysChange: EventEmitter>; /** @@ -1499,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, 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 }[]>; + @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 }[]>; /** 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 a4c1fd0fe77d..ac6bc16347a1 100644 --- a/packages/devextreme-angular/src/ui/scheduler/nested/view-dxi.ts +++ b/packages/devextreme-angular/src/ui/scheduler/nested/view-dxi.ts @@ -12,7 +12,7 @@ import { -import type { AllDayPanelMode, CellAppointmentsLimit, dxSchedulerScrolling, SnapToCellsMode, ViewType } from 'devextreme/ui/scheduler'; +import type { AllDayPanelMode, DayOfWeek, CellAppointmentsLimit, dxSchedulerScrolling, SnapToCellsMode, ViewType } from 'devextreme/ui/scheduler'; import type { FirstDayOfWeek, Orientation } from 'devextreme/common'; import { @@ -143,10 +143,10 @@ export class DxiSchedulerViewComponent extends CollectionNestedOption { } @Input() - get hiddenWeekDays(): Array { + get hiddenWeekDays(): Array { return this._getOption('hiddenWeekDays'); } - set hiddenWeekDays(value: Array) { + set hiddenWeekDays(value: Array) { this._setOption('hiddenWeekDays', value); } diff --git a/packages/devextreme-react/src/scheduler.ts b/packages/devextreme-react/src/scheduler.ts index f5ab778820b6..94046b0c9bab 100644 --- a/packages/devextreme-react/src/scheduler.ts +++ b/packages/devextreme-react/src/scheduler.ts @@ -8,7 +8,7 @@ import dxScheduler, { import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component"; import NestedOption from "./core/nested-option"; -import type { ViewType, AppointmentAddedEvent, AppointmentAddingEvent, AppointmentClickEvent, AppointmentContextMenuEvent, AppointmentDblClickEvent, AppointmentDeletedEvent, AppointmentDeletingEvent, AppointmentFormOpeningEvent, AppointmentRenderedEvent, AppointmentTooltipShowingEvent, AppointmentUpdatedEvent, AppointmentUpdatingEvent, CellClickEvent, CellContextMenuEvent, ContentReadyEvent, DisposingEvent, InitializedEvent, AppointmentFormProperties, AppointmentFormIconsShowMode, SchedulerPredefinedToolbarItem, DateNavigatorItemProperties, SchedulerPredefinedDateNavigatorItem, dxSchedulerToolbarItem, AllDayPanelMode, AppointmentCollectorTemplateData, AppointmentTemplateData, AppointmentTooltipTemplateData, CellAppointmentsLimit, dxSchedulerScrolling, SnapToCellsMode } from "devextreme/ui/scheduler"; +import type { ViewType, AppointmentAddedEvent, AppointmentAddingEvent, AppointmentClickEvent, AppointmentContextMenuEvent, AppointmentDblClickEvent, AppointmentDeletedEvent, AppointmentDeletingEvent, AppointmentFormOpeningEvent, AppointmentRenderedEvent, AppointmentTooltipShowingEvent, AppointmentUpdatedEvent, AppointmentUpdatingEvent, CellClickEvent, CellContextMenuEvent, ContentReadyEvent, DisposingEvent, InitializedEvent, AppointmentFormProperties, AppointmentFormIconsShowMode, SchedulerPredefinedToolbarItem, DateNavigatorItemProperties, SchedulerPredefinedDateNavigatorItem, dxSchedulerToolbarItem, AllDayPanelMode, AppointmentCollectorTemplateData, AppointmentTemplateData, AppointmentTooltipTemplateData, DayOfWeek, CellAppointmentsLimit, dxSchedulerScrolling, SnapToCellsMode } from "devextreme/ui/scheduler"; import type { ContentReadyEvent as ButtonContentReadyEvent, DisposingEvent as ButtonDisposingEvent, InitializedEvent as ButtonInitializedEvent, dxButtonOptions, ClickEvent, OptionChangedEvent } from "devextreme/ui/button"; import type { ContentReadyEvent as FormContentReadyEvent, DisposingEvent as FormDisposingEvent, InitializedEvent as FormInitializedEvent, FormItemType, FormPredefinedButtonItem, OptionChangedEvent as FormOptionChangedEvent, dxFormSimpleItem, dxFormGroupItem, dxFormTabbedItem, dxFormEmptyItem, dxFormButtonItem, LabelLocation, FormLabelMode, EditorEnterKeyEvent, FieldDataChangedEvent, SmartPastedEvent, SmartPastingEvent, FormItemComponent } from "devextreme/ui/form"; import type { ContentReadyEvent as ButtonGroupContentReadyEvent, DisposingEvent as ButtonGroupDisposingEvent, InitializedEvent as ButtonGroupInitializedEvent, OptionChangedEvent as ButtonGroupOptionChangedEvent, dxButtonGroupItem, ItemClickEvent, SelectionChangedEvent } from "devextreme/ui/button_group"; @@ -1454,7 +1454,7 @@ type IViewProps = React.PropsWithChildren<{ groupByDate?: boolean; groupOrientation?: Orientation; groups?: Array; - hiddenWeekDays?: 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 548f4d2da72f..c2cd393d4439 100644 --- a/packages/devextreme-vue/src/scheduler.ts +++ b/packages/devextreme-vue/src/scheduler.ts @@ -10,6 +10,7 @@ import { AllDayPanelMode, ViewType, dxSchedulerAppointment, + DayOfWeek, CellAppointmentsLimit, AppointmentAddedEvent, AppointmentAddingEvent, @@ -249,7 +250,7 @@ const componentConfig = { groupByDate: Boolean, groups: Array as PropType>, height: [Number, String], - hiddenWeekDays: Array as PropType>, + hiddenWeekDays: Array as PropType>, hint: String, indicatorUpdateInterval: Number, max: [Date, Number, String], @@ -1804,7 +1805,7 @@ const DxViewConfig = { groupByDate: Boolean, groupOrientation: String as PropType, groups: Array as PropType>, - hiddenWeekDays: Array as PropType>, + hiddenWeekDays: Array as PropType>, intervalCount: Number, maxAppointmentsPerCell: [String, Number] as PropType, name: String, diff --git a/packages/devextreme/js/ui/scheduler.d.ts b/packages/devextreme/js/ui/scheduler.d.ts index 95c46bda15dc..8e1a71df45bf 100644 --- a/packages/devextreme/js/ui/scheduler.d.ts +++ b/packages/devextreme/js/ui/scheduler.d.ts @@ -81,6 +81,8 @@ export type SnapToCellsMode = 'always' | 'auto' | 'never'; export type RecurrenceEditMode = 'dialog' | 'occurrence' | 'series'; /** @public */ export type AppointmentFormIconsShowMode = 'both' | 'main' | 'recurrence' | 'none'; +/** @public */ +export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6; /** * @docid @@ -690,7 +692,7 @@ export interface dxSchedulerOptions extends WidgetOptions { * @default undefined * @public */ - hiddenWeekDays?: Array; + hiddenWeekDays?: Array; /** * @docid * @default true &for(desktop) @@ -1097,7 +1099,7 @@ export interface dxSchedulerOptions extends WidgetOptions { * @docid * @default undefined */ - hiddenWeekDays?: Array; + hiddenWeekDays?: Array; /** * @docid * @default false diff --git a/packages/devextreme/js/ui/scheduler_types.d.ts b/packages/devextreme/js/ui/scheduler_types.d.ts index a213dd60cb1a..d6571eb09c40 100644 --- a/packages/devextreme/js/ui/scheduler_types.d.ts +++ b/packages/devextreme/js/ui/scheduler_types.d.ts @@ -7,6 +7,7 @@ export { SnapToCellsMode, RecurrenceEditMode, AppointmentFormIconsShowMode, + DayOfWeek, AppointmentFormProperties, ViewType, SchedulerScrollToAlign, diff --git a/packages/devextreme/ts/dx.all.d.ts b/packages/devextreme/ts/dx.all.d.ts index eafdaa7fb4f6..37480da4c36c 100644 --- a/packages/devextreme/ts/dx.all.d.ts +++ b/packages/devextreme/ts/dx.all.d.ts @@ -26243,6 +26243,7 @@ declare module DevExpress.ui { readonly endDate: Date; readonly text: string; }; + export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6; /** * [descr:_ui_scheduler_DisposingEvent] */ @@ -26581,7 +26582,7 @@ declare module DevExpress.ui { /** * [descr:dxSchedulerOptions.hiddenWeekDays] */ - hiddenWeekDays?: Array; + hiddenWeekDays?: Array; /** * [descr:dxSchedulerOptions.focusStateEnabled] */ @@ -26916,7 +26917,7 @@ declare module DevExpress.ui { /** * [descr:dxSchedulerOptions.views.hiddenWeekDays] */ - hiddenWeekDays?: Array; + hiddenWeekDays?: Array; /** * [descr:dxSchedulerOptions.views.groupByDate] */ From 00ee28b61ea03ed0816dbc9d0b33663e0423e005 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:03:33 +0200 Subject: [PATCH 45/65] fix: fix singlequote error --- packages/devextreme/js/ui/widget/ui.errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/ui/widget/ui.errors.js b/packages/devextreme/js/ui/widget/ui.errors.js index 9027c1cba511..033a93901892 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: '\'hiddenWeekDays'\ must leave at least one weekday visible.', + W1029: '\'hiddenWeekDays\' must leave at least one weekday visible.', }); From dd3a8ab84d00b4a1ffaf82991ebb9934ce5d9e3c Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:05:05 +0200 Subject: [PATCH 46/65] refactor: change visibility of getVisibleDayOffset method to private --- .../scheduler/workspaces/view_model/m_view_data_generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2cacf4006534..1ba2ac39a9f8 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 @@ -75,7 +75,7 @@ export class ViewDataGenerator { return this.getFirstDayOfWeek(firstDayOfWeekOption) ?? 0; } - protected getVisibleDayOffset( + private getVisibleDayOffset( rowIndex: number, columnIndex: number, anchorDay: number, From 47e7828aa4987b85fc6072ca3ab64ce9a3a1ce52 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:09:27 +0200 Subject: [PATCH 47/65] refactor: rename isSkippedDate to isDateSkipped --- .../scheduler/r1/utils/__tests__/base.test.ts | 10 +++++----- .../js/__internal/scheduler/r1/utils/base.ts | 6 +++--- packages/devextreme/js/__internal/scheduler/types.ts | 2 +- .../workspaces/view_model/m_view_data_generator.ts | 2 +- .../view_model/m_view_data_generator_timeline_month.ts | 2 +- .../workspaces/view_model/m_view_data_provider.ts | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts index 272c91a9b31a..4f53f96fdf21 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/__tests__/base.test.ts @@ -201,7 +201,7 @@ describe('base utils', () => { describe('default', () => { it('should skip large interval', () => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => date.getDay() >= 6, + isDateSkipped: (date: Date) => date.getDay() >= 6, getViewOptions: () => ({ startDayHour: 0, endDayHour: 24, @@ -222,7 +222,7 @@ describe('base utils', () => { it('should skip 2 weekend days if startDate and endDate inside weekend', () => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isWeekend(date), + isDateSkipped: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour: 0, endDayHour: 24, @@ -244,7 +244,7 @@ describe('base utils', () => { describe('border conditions', () => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isWeekend(date), + isDateSkipped: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour: 0, endDayHour: 24, @@ -375,7 +375,7 @@ describe('base utils', () => { endDayHour, }) => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isWeekend(date), + isDateSkipped: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour, endDayHour, @@ -414,7 +414,7 @@ describe('base utils', () => { expectedHours, }) => { const mockViewDataProvider = { - isSkippedDate: (date: Date) => isWeekend(date), + isDateSkipped: (date: Date) => isWeekend(date), getViewOptions: () => ({ startDayHour: 11, endDayHour: 19, diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts index 13d7bf92c771..fab274a20b3d 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/base.ts @@ -384,7 +384,7 @@ export const getSkippedHoursInRange = ( const dayHours = isAllDay ? DAY_HOURS : endDayHour - startDayHour; while (currentDate < endDateWithStartHour) { - if (viewDataProvider.isSkippedDate(currentDate)) { + if (viewDataProvider.isDateSkipped(currentDate)) { result += dayHours; } @@ -394,7 +394,7 @@ export const getSkippedHoursInRange = ( const startDateHours = startDate.getHours(); const endDateHours = endDate.getHours() + (endDate.getTime() % HOUR_IN_MS) / HOUR_IN_MS; - if (viewDataProvider.isSkippedDate(startDate)) { + if (viewDataProvider.isDateSkipped(startDate)) { switch (true) { case isAllDay: result += DAY_HOURS; @@ -410,7 +410,7 @@ export const getSkippedHoursInRange = ( } } - if (viewDataProvider.isSkippedDate(endDate)) { + if (viewDataProvider.isDateSkipped(endDate)) { switch (true) { case isAllDay: result += DAY_HOURS; diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts index 2b4df4b92ec7..c6f84c390050 100644 --- a/packages/devextreme/js/__internal/scheduler/types.ts +++ b/packages/devextreme/js/__internal/scheduler/types.ts @@ -242,7 +242,7 @@ export interface ViewDataProviderType { getViewOptions: () => ViewOptions; setViewOptions: (options: ViewDataProviderOptions) => void; createGroupedDataMapProvider: () => void; - isSkippedDate: (date: Date) => boolean; + isDateSkipped: (date: Date) => boolean; getCellsByGroupIndexAndAllDay: (groupIndex: number, isAllDay: boolean) => ViewCellData[][]; getCellsBetween: (first: ViewCellData, last: ViewCellData) => ViewCellData[]; viewType: ViewType; 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 1ba2ac39a9f8..6e98ecf3dc9e 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 @@ -104,7 +104,7 @@ export class ViewDataGenerator { return actualDayOffset - naiveDayOffset; } - public isSkippedDate(date: Date): boolean { + public isDateSkipped(date: Date): boolean { return isDateSkipped(date, this.skippedDays); } 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 9172371e5b30..56a45bd5fd49 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.isSkippedDate(date)) { + if (!this.isDateSkipped(date)) { cellCount += 1; } } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts index 6b0458ebe787..1189212f074b 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_provider.ts @@ -65,7 +65,7 @@ export default class ViewDataProvider { get hiddenInterval() { return this.viewDataGenerator.hiddenInterval; } - isSkippedDate(date: Date): boolean { return this.viewDataGenerator.isSkippedDate(date); } + isDateSkipped(date: Date): boolean { return this.viewDataGenerator.isDateSkipped(date); } update(options: ViewDataProviderOptions, isGenerateNewViewData: boolean): void { this.viewDataGenerator = getViewDataGeneratorByViewType(options.viewType); From 3c408f629adbcdbdd512630b91470ae513d0a332 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:21:55 +0200 Subject: [PATCH 48/65] refactor: simplify date increment logic using getFirstVisibleDate utility --- .../scheduler/workspaces/m_timeline.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index 7e696a8ef77b..17da1f0ccdf9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -18,6 +18,7 @@ import { import tableCreatorModule from '../m_table_creator'; import timezoneUtils from '../m_utils_time_zone'; import HorizontalShader from '../shaders/current_time_shader_horizontal'; +import { getFirstVisibleDate } from '../utils/skipped_days'; import SchedulerWorkSpace from './m_work_space_indicator'; const { tableCreator } = tableCreatorModule; @@ -109,15 +110,17 @@ class SchedulerTimeline extends SchedulerWorkSpace { protected incrementDate(date) { const skippedDays = this.option('skippedDays') ?? []; + const nextVisibleDate = getFirstVisibleDate( + new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1), + skippedDays, + (currentDate) => { + const result = new Date(currentDate); + result.setDate(result.getDate() + 1); + return result; + }, + ); - if (skippedDays.length >= 7) { - return; - } - - date.setDate(date.getDate() + 1); - while (skippedDays.includes(date.getDay())) { - date.setDate(date.getDate() + 1); - } + date.setTime(nextVisibleDate.getTime()); } getIndicationCellCount() { From 8ccbb06dad15ff8d33c918d195f88668365a612d Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:34:22 +0200 Subject: [PATCH 49/65] refactor: consolidate date handling for week and workWeek steps --- packages/devextreme/js/__internal/scheduler/header/m_utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 67fb6e822bf6..a559ff7eb9f5 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -136,9 +136,7 @@ const getNextPeriodStartDate = ( ): Date => { let date = addMS(currentPeriodEndDate); - if (step === 'workWeek') { - date = getFirstVisibleDate(date, skippedDays, nextDay); - } else if (step === 'week' && skippedDays.length > 0) { + if (step === 'week' || step === 'workWeek') { date = getFirstVisibleDate(date, skippedDays, nextDay); } From 58861724b38115d733f3c8e5218083fa1ab298b9 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:37:29 +0200 Subject: [PATCH 50/65] refactor: unify date handling functions for week and workWeek calculations --- .../js/__internal/scheduler/header/m_utils.ts | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index a559ff7eb9f5..a4172be4eb1a 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -55,13 +55,14 @@ const getWorkWeekStart = ( nextDay, ); -const getDateAfterWorkWeek = ( - workWeekStart: Date, +const getDateAfterWeek = ( + startDate: Date, firstDayOfWeek: number | undefined, skippedDays: number[], ): Date => { - const weekStart = getWeekStart(workWeekStart, firstDayOfWeek); + const weekStart = getWeekStart(startDate, firstDayOfWeek); let lastVisibleDate = addDateInterval(weekStart, { days: 6 }, 1); + while (isDateSkipped(lastVisibleDate, skippedDays)) { lastVisibleDate = addDateInterval(lastVisibleDate, DAY_DURATION, -1); } @@ -69,18 +70,6 @@ const getDateAfterWorkWeek = ( return nextDay(lastVisibleDate); }; -const getDateAfterWeek = ( - weekStartDate: Date, - firstDayOfWeek: number | undefined, - skippedDays: number[], -): Date => { - if (skippedDays.length === 0) { - return nextWeek(weekStartDate); - } - - return getDateAfterWorkWeek(weekStartDate, firstDayOfWeek, skippedDays); -}; - const nextAgendaStart = ( date: Date, agendaDuration: number, @@ -122,7 +111,7 @@ const getPeriodEndDate = ( day: () => nextDay(currentPeriodStartDate), week: () => getDateAfterWeek(currentPeriodStartDate, firstDayOfWeek, skippedDays), month: () => nextMonth(currentPeriodStartDate), - workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate, firstDayOfWeek, skippedDays), + workWeek: () => getDateAfterWeek(currentPeriodStartDate, firstDayOfWeek, skippedDays), agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), }; From 0f5a3292e03b0e234084a1ea2fd805f4d5a8ebf5 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:46:34 +0200 Subject: [PATCH 51/65] refactor: update getSkippedDaysCount parameter order for consistency across scheduler components --- .../js/__internal/scheduler/__tests__/workspace.base.test.ts | 2 +- .../js/__internal/scheduler/workspaces/m_timeline.ts | 4 ++-- .../js/__internal/scheduler/workspaces/m_work_space.ts | 4 ++-- .../scheduler/workspaces/m_work_space_indicator.ts | 5 ++++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index 173a04c34ea3..5c028effcf14 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -206,7 +206,7 @@ describe('scheduler workspace skipped days support', () => { skippedDays: [1, 3], }); - expect((workspace as any).getSkippedDaysCount(7, new Date(2026, 3, 5))).toBe(2); + expect((workspace as any).getSkippedDaysCount(new Date(2026, 3, 5), 7)).toBe(2); }); it('should use full week layout for work week when skippedDays override is empty', () => { diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index 17da1f0ccdf9..0f91d0ab30d7 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -146,8 +146,8 @@ class SchedulerTimeline extends SchedulerWorkSpace { const today = this.getToday(); const differenceInDays = Math.floor(timeDiff / toMs('day')); const skippedDaysCount = this.getSkippedDaysCount( - differenceInDays, this.getIndicationFirstViewDate(), + differenceInDays, ); let duration = (timeDiff - differenceInDays * toMs('day') - (this.option('startDayHour') as any) * toMs('hour')) / this.getCellDuration(); @@ -239,7 +239,7 @@ class SchedulerTimeline extends SchedulerWorkSpace { const fullDays = Math.floor(fullInterval / toMs('day')); const tailDuration = fullInterval - (fullDays * toMs('day')); let tailDelta = 0; - const skippedDaysCount = this.getSkippedDaysCount(fullDays, firstViewDate); + const skippedDaysCount = this.getSkippedDaysCount(firstViewDate, fullDays); const cellCount = this.getCellCountInDay() * (fullDays - skippedDaysCount); const gapBeforeAppt = apptStart - dateUtils.trimTime(new Date(currentDate)).getTime(); let result = cellCount * (this.option('hoursInterval') as any) * toMs('hour'); 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 98a53b6ae209..2a0008d79057 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -1324,7 +1324,7 @@ class SchedulerWorkSpace extends Widget { const timeZoneOffset = dateUtils.getTimezonesDifference(firstViewDate, currentDate); const fullInterval = currentDate.getTime() - firstViewDate.getTime() - timeZoneOffset; const days = this.getDaysOfInterval(fullInterval, startDayTime); - const skippedDaysCount = this.getSkippedDaysCount(days, firstViewDate); + const skippedDaysCount = this.getSkippedDaysCount(firstViewDate, days); let result = (days - skippedDaysCount) * DAY_MS; if (!allDay) { @@ -1337,7 +1337,7 @@ class SchedulerWorkSpace extends Widget { return result; } - protected getSkippedDaysCount(days: number, startDate: Date = this.getStartViewDate()) { + protected getSkippedDaysCount(startDate: Date, days: number) { return countSkippedDays( startDate, days, diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts index 90084b414233..361e5ab59195 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts @@ -128,7 +128,10 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { let timeDiff = today.getTime() - viewStartTime; if (((this.option('skippedDays')) ?? []).length > 0) { - const skippedDaysDuration = this.getSkippedDaysCount(Math.round(timeDiff / toMs('day'))) * toMs('day'); + const skippedDaysDuration = this.getSkippedDaysCount( + this.getStartViewDate(), + Math.round(timeDiff / toMs('day')), + ) * toMs('day'); timeDiff -= skippedDaysDuration; } From a7c1825850398357008362da35397f8b7b776f41 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 13:48:10 +0200 Subject: [PATCH 52/65] refactor: streamline daysInInterval calculation for improved readability --- .../workspaces/view_model/m_view_data_generator.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 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 6e98ecf3dc9e..0d16b074f91f 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 @@ -43,14 +43,11 @@ export class ViewDataGenerator { 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; + const isWeekLikeView = this.baseDaysInInterval >= 7; + + return isWeekLikeView + ? 7 - this.skippedDays.length + : this.baseDaysInInterval; } public isWorkWeekView(): boolean { From fd13c8a084a5950b395999c9110f3d3b37f31320 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 14:18:40 +0200 Subject: [PATCH 53/65] test: fix test --- .../view_data_provider.tests.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js index eeef176f2bff..a7ed83460ef6 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js @@ -960,7 +960,7 @@ module('View Data Provider', { }); }); - module('isSkippedDate', () => { + module('isDateSkipped', () => { test('it should return correct value for the weekend', async function(assert) { [ { viewType: 'day', expected: false }, @@ -973,18 +973,18 @@ module('View Data Provider', { { viewType: 'timelineMonth', expected: false }, ].forEach(({ viewType, expected }) => { const viewDataProvider = new ViewDataProvider(viewType); - const result = viewDataProvider.isSkippedDate(new Date(2021, 8, 4)); + const result = viewDataProvider.isDateSkipped(new Date(2021, 8, 4)); - assert.equal(result, expected, `isSkippedDate is correct for the ${viewType} view type`); + assert.equal(result, expected, `isDateSkipped is correct for the ${viewType} view type`); }); }); test('it should return correct value for the week day', async function(assert) { supportedViews.forEach((viewType) => { const viewDataProvider = new ViewDataProvider(viewType); - const result = viewDataProvider.isSkippedDate(new Date(2021, 8, 3)); + const result = viewDataProvider.isDateSkipped(new Date(2021, 8, 3)); - assert.notOk(result, `isSkippedDate is correct for the ${viewType} view type`); + assert.notOk(result, `isDateSkipped is correct for the ${viewType} view type`); }); }); }); From 1b666f2ba852d590e2200279bf9f8daa7398648f Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 14:24:37 +0200 Subject: [PATCH 54/65] feat: add support for day and timelineday --- .../__tests__/workspace.base.test.ts | 11 +++++ .../scheduler/header/m_utils.test.ts | 32 ++++++++++++++ .../js/__internal/scheduler/header/m_utils.ts | 35 +++++++++++++-- .../scheduler/utils/options/utils.test.ts | 8 +++- .../scheduler/utils/options/utils.ts | 6 +-- .../view_model/m_view_data_generator.test.ts | 44 +++++++++++++++++++ .../view_model/m_view_data_generator_day.ts | 23 +++++++++- 7 files changed, 146 insertions(+), 13 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index 5c028effcf14..11ec6a59740d 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -242,6 +242,17 @@ describe('scheduler workspace skipped days support', () => { expect(date).toEqual(new Date(2026, 3, 9)); // Thursday }); + it('should skip hidden days when incrementing timeline day dates', () => { + const { workspace } = createWorkspace(SchedulerTimelineDay, 'timelineDay', { + skippedDays: [0, 6], + }); + const date = new Date(2026, 3, 10); // Friday + + (workspace as any).incrementDate(date); + + expect(date).toEqual(new Date(2026, 3, 13)); // Monday + }); + it('should respect empty skippedDays override in timeline work week', () => { const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek', { skippedDays: [], 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 fdb94577a8a9..4d08bac989c5 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -28,6 +28,38 @@ describe('agenda hiddenWeekDays support in header utils', () => { }); }); +describe('day hiddenWeekDays support in header utils', () => { + it('should shift day caption to the next visible day', () => { + expect(getCaptionInterval({ + date: new Date(2026, 3, 11), // Saturday + step: 'day', + intervalCount: 1, + skippedDays: [0, 6], + })).toEqual({ + startDate: new Date(2026, 3, 13), + endDate: new Date(2026, 3, 13, 23, 59, 59, 999), + }); + }); + + it('should navigate to the next visible day interval', () => { + expect(getNextIntervalDate({ + date: new Date(2026, 3, 10), // Friday + step: 'day', + intervalCount: 3, + skippedDays: [0, 6], + }, 1)).toEqual(new Date(2026, 3, 15)); + }); + + it('should navigate from a hidden day based on the visible interval', () => { + expect(getNextIntervalDate({ + date: new Date(2026, 3, 11), // Saturday + step: 'day', + intervalCount: 1, + skippedDays: [0, 6], + }, 1)).toEqual(new Date(2026, 3, 14)); + }); +}); + describe('workWeek hiddenWeekDays support in header utils', () => { it('should keep Mon-Fri caption for default skippedDays', () => { expect(getCaptionInterval({ diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index a4172be4eb1a..0d1f97b8029d 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -37,6 +37,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 => { @@ -75,6 +77,22 @@ const nextAgendaStart = ( agendaDuration: number, ): Date => addDateInterval(date, { days: agendaDuration }, 1); +const getDateAfterVisibleDays = ( + startDate: Date, + dayCount: number, + skippedDays: number[], + direction: Direction, +): Date => { + const dateStep = direction === 1 ? nextDay : prevDay; + let date = new Date(startDate); + + for (let i = 0; i < dayCount; i += 1) { + date = getFirstVisibleDate(dateStep(date), skippedDays, dateStep); + } + + return date; +}; + const getIntervalStartDate = (options: IntervalOptions): Date => { const { date, step, firstDayOfWeek, skippedDays, @@ -82,6 +100,11 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { switch (step) { case 'day': + return getFirstVisibleDate( + getPeriodStart(date, step, false, firstDayOfWeek) as Date, + skippedDays, + nextDay, + ); case 'month': return getPeriodStart(date, step, false, firstDayOfWeek) as Date; case 'week': { @@ -125,7 +148,7 @@ const getNextPeriodStartDate = ( ): Date => { let date = addMS(currentPeriodEndDate); - if (step === 'week' || step === 'workWeek') { + if (step === 'day' || step === 'week' || step === 'workWeek') { date = getFirstVisibleDate(date, skippedDays, nextDay); } @@ -181,15 +204,19 @@ 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; // eslint-disable-next-line default-case switch (step) { case 'day': - dayDuration = Number(intervalCount); - break; + return getDateAfterVisibleDays( + getIntervalStartDate(options), + intervalCount, + skippedDays, + direction, + ); case 'week': case 'workWeek': dayDuration = 7 * intervalCount; 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 310677cbe28a..7ead61bf5557 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,12 @@ describe('views utils', () => { expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); }); - it('ignores global hiddenWeekDays for day', () => { - expect(getSkipped(['day'], 'day', [3])).toEqual([]); + it('applies global hiddenWeekDays to day', () => { + expect(getSkipped(['day'], 'day', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to timelineDay', () => { + expect(getSkipped(['timelineDay'], 'timelineDay', [3])).toEqual([3]); }); it('applies global hiddenWeekDays to agenda', () => { diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 033c09b39908..4c247db8b8c8 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -10,10 +10,6 @@ import type { DateOption, NormalizedView, RawViewType, SafeSchedulerOptions, ViewType, } from './types'; -const VIEWS_SUPPORTING_HIDDEN_DAYS: ReadonlySet = new Set([ - 'week', 'workWeek', 'month', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth', 'agenda', -]); - const normalizeHiddenWeekDays = ( days: unknown, ): number[] | undefined => { @@ -40,7 +36,7 @@ const resolveSkippedDays = ( if (perView !== undefined) { return perView; } - if (globalHiddenWeekDays !== undefined && VIEWS_SUPPORTING_HIDDEN_DAYS.has(viewType)) { + if (globalHiddenWeekDays !== undefined) { return normalizeHiddenWeekDays(globalHiddenWeekDays) ?? []; } return viewDefault; 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 6dd178be4410..ce69888eb7f7 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 @@ -1,6 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import type { ViewType } from '../../types'; +import { ViewDataGeneratorDay } from './m_view_data_generator_day'; 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'; @@ -263,4 +264,47 @@ describe('ViewDataGenerator hiddenWeekDays support', () => { expect(date.getDate()).toBe(30); }); }); + + describe('Day hiddenWeekDays support', () => { + it('shifts startViewDate to the next visible day', () => { + const gen = new ViewDataGeneratorDay('day' as ViewType); + gen.skippedDays = [0, 6]; + + const startViewDate = gen.getStartViewDate({ + currentDate: new Date(2026, 3, 11), // Saturday + startDayHour: 0, + startDate: undefined, + intervalCount: 1, + }); + + expect(startViewDate.getDay()).toBe(1); + expect(startViewDate.getDate()).toBe(13); + }); + + it('maps multi-day timeline columns to visible days only', () => { + const gen = new ViewDataGeneratorDay('timelineDay' as ViewType); + gen.skippedDays = [0, 6]; + + const options = { + startViewDate: new Date(2026, 3, 10, 0, 0), // Friday + startDayHour: 0, + endDayHour: 24, + hoursInterval: 24, + interval: 24 * 60 * 60 * 1000, + firstDayOfWeek: 0, + intervalCount: 3, + viewOffset: 0, + currentDate: new Date(2026, 3, 10), + viewType: 'timelineDay' as ViewType, + }; + + const Monday = gen.getDateByCellIndices(options, 0, 1); + const Tuesday = gen.getDateByCellIndices(options, 0, 2); + + expect(Monday.getDay()).toBe(1); + expect(Monday.getDate()).toBe(13); + expect(Tuesday.getDay()).toBe(2); + expect(Tuesday.getDate()).toBe(14); + }); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_day.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_day.ts index f29deb8ec768..038519991dce 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_day.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_day.ts @@ -1,13 +1,32 @@ import { dayUtils } from '../../r1/utils/index'; +import { getFirstVisibleDate } from '../../utils/skipped_days'; import { ViewDataGenerator } from './m_view_data_generator'; export class ViewDataGeneratorDay extends ViewDataGenerator { - protected calculateStartViewDate(options) { - return dayUtils.calculateStartViewDate( + // eslint-disable-next-line class-methods-use-this + protected override getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + startViewDate: Date, + ): number { + return startViewDate.getDay(); + } + + protected override calculateStartViewDate(options: any): Date { + const startViewDate = dayUtils.calculateStartViewDate( options.currentDate, options.startDayHour, options.startDate, this._getIntervalDuration(options.intervalCount), ); + + return getFirstVisibleDate( + startViewDate, + options.skippedDays ?? this.skippedDays, + (date) => { + const nextDate = new Date(date); + nextDate.setDate(nextDate.getDate() + 1); + return nextDate; + }, + ); } } From 1ed0d644bec5c9a664dc2f1e5789b9795368e774 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 14:39:36 +0200 Subject: [PATCH 55/65] fix: fix tests --- .../devextreme/js/__internal/scheduler/header/m_utils.ts | 4 ++-- .../js/__internal/scheduler/workspaces/m_timeline.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index 0d1f97b8029d..f38ca69f2693 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -84,7 +84,7 @@ const getDateAfterVisibleDays = ( direction: Direction, ): Date => { const dateStep = direction === 1 ? nextDay : prevDay; - let date = new Date(startDate); + let date = getFirstVisibleDate(new Date(startDate), skippedDays, dateStep); for (let i = 0; i < dayCount; i += 1) { date = getFirstVisibleDate(dateStep(date), skippedDays, dateStep); @@ -212,7 +212,7 @@ export const getNextIntervalDate = (options: IntervalOptions, direction: Directi switch (step) { case 'day': return getDateAfterVisibleDays( - getIntervalStartDate(options), + date, intervalCount, skippedDays, direction, diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index 0f91d0ab30d7..cb5a4bca1c3b 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -110,8 +110,11 @@ class SchedulerTimeline extends SchedulerWorkSpace { protected incrementDate(date) { const skippedDays = this.option('skippedDays') ?? []; + const nextDate = new Date(date); + nextDate.setDate(nextDate.getDate() + 1); + const nextVisibleDate = getFirstVisibleDate( - new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1), + nextDate, skippedDays, (currentDate) => { const result = new Date(currentDate); From 73130531523e260a8170d7a4045b47560b8658e2 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 15 Apr 2026 15:13:54 +0200 Subject: [PATCH 56/65] refactor: unify week and workWeek date handling in getIntervalStartDate function --- .../js/__internal/scheduler/header/m_utils.ts | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts index f38ca69f2693..32ba82f1f869 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.ts @@ -47,16 +47,6 @@ const nextMonth = (date: Date): Date => { return addDateInterval(date, { days }, 1); }; -const getWorkWeekStart = ( - date: Date, - firstDayOfWeek: number | undefined, - skippedDays: number[], -): Date => getFirstVisibleDate( - getWeekStart(date, firstDayOfWeek), - skippedDays, - nextDay, -); - const getDateAfterWeek = ( startDate: Date, firstDayOfWeek: number | undefined, @@ -107,15 +97,11 @@ const getIntervalStartDate = (options: IntervalOptions): Date => { ); case 'month': return getPeriodStart(date, step, false, firstDayOfWeek) as Date; - case 'week': { - const weekStart = getPeriodStart(date, step, false, firstDayOfWeek) as Date; - if (skippedDays.length > 0) { - return getFirstVisibleDate(weekStart, skippedDays, nextDay); - } - return weekStart; + case 'week': + case 'workWeek': { + const weekStart = getPeriodStart(date, 'week', false, firstDayOfWeek) as Date; + return getFirstVisibleDate(weekStart, skippedDays, nextDay); } - case 'workWeek': - return getWorkWeekStart(date, firstDayOfWeek, skippedDays); case 'agenda': return new Date(date); default: From e3f8d673775bb5b65dbde915d9ee933c14dafcd0 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 10:53:53 +0200 Subject: [PATCH 57/65] fix: remove skippedDays tests from workspace --- .../__tests__/workspace.base.test.ts | 78 ------------------- .../workspaces/m_timeline_work_week.ts | 7 -- .../workspaces/m_work_space_work_week.ts | 7 -- 3 files changed, 92 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index 11ec6a59740d..fc9107a89926 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -195,81 +195,3 @@ describe('scheduler workspace scrollTo', () => { expect(scrollableContainer.scrollLeft).toBeCloseTo(-11125); }); }); - -describe('scheduler workspace skipped days support', () => { - beforeEach(() => { - setupSchedulerTestEnvironment(); - }); - - it('should count configured skipped days in week workspace interval math', () => { - const { workspace } = createWorkspace(SchedulerWorkSpaceWeek, 'week', { - skippedDays: [1, 3], - }); - - expect((workspace as any).getSkippedDaysCount(new Date(2026, 3, 5), 7)).toBe(2); - }); - - it('should use full week layout for work week when skippedDays override is empty', () => { - const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek', { - currentDate: new Date(2026, 3, 1), // Wednesday - firstDayOfWeek: 0, // Sunday - skippedDays: [], - }); - - expect(workspace.getStartViewDate()).toEqual(new Date(2026, 2, 29)); - expect((workspace as any)._getCellCount()).toBe(7); - }); - - it('should use custom skippedDays in work week runtime layout', () => { - const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek', { - currentDate: new Date(2026, 3, 1), // Wednesday - firstDayOfWeek: 0, // Sunday - skippedDays: [3], // Wednesday - }); - - expect(workspace.getStartViewDate()).toEqual(new Date(2026, 2, 29)); - expect((workspace as any)._getCellCount()).toBe(6); - }); - - it('should skip configured hidden days when incrementing timeline header dates', () => { - const { workspace } = createWorkspace(SchedulerTimelineWeek, 'timelineWeek', { - skippedDays: [3], - }); - const date = new Date(2026, 3, 7); // Tuesday - - (workspace as any).incrementDate(date); - - expect(date).toEqual(new Date(2026, 3, 9)); // Thursday - }); - - it('should skip hidden days when incrementing timeline day dates', () => { - const { workspace } = createWorkspace(SchedulerTimelineDay, 'timelineDay', { - skippedDays: [0, 6], - }); - const date = new Date(2026, 3, 10); // Friday - - (workspace as any).incrementDate(date); - - expect(date).toEqual(new Date(2026, 3, 13)); // Monday - }); - - it('should respect empty skippedDays override in timeline work week', () => { - const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek', { - skippedDays: [], - }); - const date = new Date(2026, 3, 10); // Friday - - (workspace as any).incrementDate(date); - - expect(date).toEqual(new Date(2026, 3, 11)); // Saturday - }); - - it('should skip weekend by default in timeline work week', () => { - const { workspace } = createWorkspace(SchedulerTimelineWorkWeek, 'timelineWorkWeek'); - const date = new Date(2026, 3, 10); // Friday - - (workspace as any).incrementDate(date); - - expect(date).toEqual(new Date(2026, 3, 13)); // Monday - }); -}); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts index bfc486eb75db..155d5868a52f 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline_work_week.ts @@ -1,5 +1,4 @@ import registerComponent from '@js/core/component_registrator'; -import { extend } from '@js/core/utils/extend'; import { VIEWS } from '../utils/options/constants_view'; import SchedulerTimelineWeek from './m_timeline_week'; @@ -9,12 +8,6 @@ const TIMELINE_CLASS = 'dx-scheduler-timeline-work-week'; class SchedulerTimelineWorkWeek extends SchedulerTimelineWeek { get type() { return VIEWS.TIMELINE_WORK_WEEK; } - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - skippedDays: [0, 6], - }); - } - protected override getElementClass() { return TIMELINE_CLASS; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts index 02fb2ae2c9a1..57c27f341df9 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_work_week.ts @@ -1,5 +1,4 @@ import registerComponent from '@js/core/component_registrator'; -import { extend } from '@js/core/utils/extend'; import { VIEWS } from '../utils/options/constants_view'; import SchedulerWorkSpaceWeek from './m_work_space_week'; @@ -8,12 +7,6 @@ const WORK_WEEK_CLASS = 'dx-scheduler-work-space-work-week'; class SchedulerWorkSpaceWorkWeek extends SchedulerWorkSpaceWeek { get type() { return VIEWS.WORK_WEEK; } - _getDefaultOptions() { - return extend(super._getDefaultOptions(), { - skippedDays: [0, 6], - }); - } - protected override getElementClass() { return WORK_WEEK_CLASS; } From 6a7a2b020b3181fe2da98525334796f69f68dea9 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 10:58:19 +0200 Subject: [PATCH 58/65] refactor: remove baseDaysInInterval --- .../workspaces/view_model/m_view_data_generator.ts | 11 +++++++---- .../view_model/m_view_data_generator_week.ts | 2 -- .../view_model/m_view_data_generator_work_week.ts | 2 -- 3 files changed, 7 insertions(+), 8 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 0d16b074f91f..9c5161a4060a 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 @@ -32,8 +32,6 @@ import type { const toMs = dateUtils.dateToMilliseconds; export class ViewDataGenerator { - protected baseDaysInInterval = 1; - protected tableAllDay = false; public hiddenInterval = 0; @@ -43,11 +41,16 @@ export class ViewDataGenerator { constructor(public readonly viewType: ViewType) {} get daysInInterval(): number { - const isWeekLikeView = this.baseDaysInInterval >= 7; + const isWeekLikeView = [ + VIEWS.WEEK, + VIEWS.TIMELINE_WEEK, + VIEWS.WORK_WEEK, + VIEWS.TIMELINE_WORK_WEEK, + ].includes(this.viewType); return isWeekLikeView ? 7 - this.skippedDays.length - : this.baseDaysInInterval; + : 1; } public isWorkWeekView(): boolean { 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 e2c8262a2993..eb69dc885954 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,8 +2,6 @@ import { weekUtils } from '../../r1/utils/index'; import { ViewDataGenerator } from './m_view_data_generator'; export class ViewDataGeneratorWeek extends ViewDataGenerator { - 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 fe424a1e2541..0be766ea5383 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 @@ -2,8 +2,6 @@ import { workWeekUtils } from '../../r1/utils/index'; import { ViewDataGeneratorWeek } from './m_view_data_generator_week'; export class ViewDataGeneratorWorkWeek extends ViewDataGeneratorWeek { - protected baseDaysInInterval = 7; - public skippedDays: number[] = [0, 6]; protected override calculateStartViewDate(options: any): Date { From 0cffa6f78792c80b9ae257f5a77283e80e45898d Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 11:45:14 +0200 Subject: [PATCH 59/65] feat: move skippedDays to getviewoption --- .../__internal/scheduler/header/m_header.ts | 4 +- .../js/__internal/scheduler/header/types.ts | 1 + .../js/__internal/scheduler/m_scheduler.ts | 3 + .../scheduler_options_base_widget.ts | 10 +++- .../scheduler/utils/options/constants.ts | 1 + .../scheduler/utils/options/types.ts | 1 + .../scheduler/utils/options/utils.test.ts | 11 +++- .../scheduler/utils/options/utils.ts | 58 ++++++------------- .../view_model/common/get_compare_options.ts | 2 +- 9 files changed, 43 insertions(+), 48 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/header/m_header.ts b/packages/devextreme/js/__internal/scheduler/header/m_header.ts index 927546baf8bf..4c2267dc6c77 100644 --- a/packages/devextreme/js/__internal/scheduler/header/m_header.ts +++ b/packages/devextreme/js/__internal/scheduler/header/m_header.ts @@ -57,7 +57,7 @@ export class SchedulerHeader extends Widget { } public getIntervalOptions(date: Date): IntervalOptions { - const { currentView, firstDayOfWeek } = this.option(); + const { currentView, firstDayOfWeek, skippedDays } = this.option(); const step = getStep(currentView.type); return { @@ -66,7 +66,7 @@ export class SchedulerHeader extends Widget { firstDayOfWeek, intervalCount: currentView.intervalCount, agendaDuration: currentView.agendaDuration, - skippedDays: currentView.skippedDays, + skippedDays, }; } diff --git a/packages/devextreme/js/__internal/scheduler/header/types.ts b/packages/devextreme/js/__internal/scheduler/header/types.ts index 061118859c4b..7fdb106fa6be 100644 --- a/packages/devextreme/js/__internal/scheduler/header/types.ts +++ b/packages/devextreme/js/__internal/scheduler/header/types.ts @@ -5,6 +5,7 @@ import type { NormalizedView, SafeSchedulerOptions } from '../utils/options/type export interface HeaderOptions { currentView: NormalizedView; + skippedDays: number[]; views: NormalizedView[]; currentDate: Date; min?: Date; diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index f2ee28d53680..38e542f395bd 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1281,6 +1281,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { private headerConfig(): HeaderOptions { return { currentView: this.currentView, + skippedDays: this.getViewOption('skippedDays'), views: this.views, currentDate: this.getViewOption('currentDate'), min: this.getViewOption('min'), @@ -1437,6 +1438,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.option('selectedCellData', args.selectedCellData); }, groupByDate: this.getViewOption('groupByDate'), + skippedDays: this.getViewOption('skippedDays'), scrolling, draggingMode: this.option('_draggingMode'), timeZoneCalculator: this.timeZoneCalculator, @@ -1458,6 +1460,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { result.onCellClick = this._createActionByOption('onCellClick'); result.onCellContextMenu = this._createActionByOption('onCellContextMenu'); result.currentDate = this.getViewOption('currentDate'); + result.skippedDays = this.getViewOption('skippedDays'); result.hoursInterval = result.cellDuration / 60; result.allDayExpanded = false; result.dataCellTemplate = result.dataCellTemplate ? this._getTemplate(result.dataCellTemplate) : null; 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 67affdb4733f..75723ee1289b 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -50,11 +50,10 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - this.views = getViews(views, this.option('hiddenWeekDays')); + this.views = getViews(views); this.currentView = getCurrentView( this.option('currentView') ?? '', views, - this.option('hiddenWeekDays'), ); } @@ -104,7 +103,12 @@ export class SchedulerOptionsBaseWidget extends Widget { const viewOptionValue = this.currentView?.[optionName as keyof View]; const optionValue = (viewOptionValue ?? this.option(optionName)) as SafeSchedulerOptions[K]; - return getViewOption(optionName, optionValue); + return getViewOption( + optionName, + optionValue, + this.currentView, + this.option('hiddenWeekDays'), + ); } hasAgendaView(): boolean { diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index c0aa187f34bb..a332ab87881b 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -108,6 +108,7 @@ export const DEFAULT_SCHEDULER_INTERNAL_OPTIONS: SchedulerInternalOptions = { ...DEFAULT_SCHEDULER_OPTIONS.editing, popup: {}, }, + skippedDays: [], // TODO: legacy option property name _draggingMode: 'outlook', // TODO: legacy option property name diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts index eae5521500c5..caa5994a0cf2 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts @@ -24,6 +24,7 @@ export interface SchedulerInternalOptions { indicatorTime?: Date; renovateRender: boolean; editing: Properties['editing']; + skippedDays: number[]; _draggingMode: 'outlook' | 'default'; // TODO: legacy option property name _appointmentTooltipOffset: { x: number; y: number }; 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 7ead61bf5557..aa24f151dad1 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -143,9 +143,14 @@ describe('views utils', () => { viewType: ViewType, globalHiddenWeekDays?: number[], ): number[] => { - const result = getViews(views, globalHiddenWeekDays); - const view = result.find((v) => v.type === viewType); - return view?.skippedDays ?? []; + const currentView = getCurrentView(viewType, views); + + return getViewOption( + 'skippedDays', + currentView.skippedDays, + currentView, + globalHiddenWeekDays, + ); }; it('uses per-view hiddenWeekDays for week', () => { diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 4c247db8b8c8..2dafb1658a4a 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -27,7 +27,6 @@ const normalizeHiddenWeekDays = ( }; const resolveSkippedDays = ( - viewType: ViewType, perViewHiddenWeekDays: unknown, globalHiddenWeekDays: number[] | undefined, viewDefault: number[], @@ -46,55 +45,22 @@ 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, - globalHiddenWeekDays?: 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.hiddenWeekDays, - globalHiddenWeekDays, - viewDefault.skippedDays, - ); - return merged; - } - const defaultView = DEFAULT_VIEW_OPTIONS[view]; - if (!defaultView) { - return undefined; - } - const skippedDays = resolveSkippedDays( - view as ViewType, - undefined, - globalHiddenWeekDays, - defaultView.skippedDays, - ); - if (skippedDays === defaultView.skippedDays) { - return defaultView; - } - return { ...defaultView, skippedDays }; -}; +const normalizeView = (view: RawViewType): NormalizedView | undefined => (isObject(view) + ? extend({}, DEFAULT_VIEW_OPTIONS[view.type as string], view) as NormalizedView + : DEFAULT_VIEW_OPTIONS[view]); export const getViews = ( views: RawViewType[], - globalHiddenWeekDays?: number[], ): NormalizedView[] => views .filter(isKnownView) - .map((v) => normalizeView(v, globalHiddenWeekDays)) + .map((v) => normalizeView(v)) .filter(isExistedView); export function getCurrentView( currentView: string | ViewType, views: RawViewType[], - globalHiddenWeekDays?: number[], ): NormalizedView { - const viewsProps = getViews(views, globalHiddenWeekDays); + const viewsProps = getViews(views); const currentViewProps = viewsProps.find( (view) => [view.name, view.type].includes(currentView), ); @@ -121,7 +87,21 @@ const isDateOption = ( export const getViewOption = ( optionName: K, currentOptionValue: SafeSchedulerOptions[K], + currentView?: NormalizedView, + globalHiddenWeekDays?: number[], ): SafeSchedulerOptions[K] => { + if (optionName === 'skippedDays') { + if (!currentView) { + return currentOptionValue; + } + + return resolveSkippedDays( + currentView.hiddenWeekDays, + globalHiddenWeekDays, + DEFAULT_VIEW_OPTIONS[currentView.type].skippedDays, + ) as SafeSchedulerOptions[K]; + } + if (!isDateOption(optionName)) { return currentOptionValue; } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts index cc33db47c1ba..baee90588928 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts @@ -12,7 +12,7 @@ export const getCompareOptions = ( endDayHour: schedulerStore.getViewOption('endDayHour'), min: timeZoneUtils.createUTCDateWithLocalOffset(dateRange[0]).getTime(), max: timeZoneUtils.createUTCDateWithLocalOffset(dateRange[1]).getTime(), - skippedDays: schedulerStore.currentView.skippedDays, + skippedDays: schedulerStore.getViewOption('skippedDays'), }; return compareOptions; From b15188c4c74d41f5f67e454c6a7007e6fe6c09a0 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 12:08:57 +0200 Subject: [PATCH 60/65] fix: fix tests --- packages/devextreme/js/__internal/scheduler/m_scheduler.ts | 2 ++ .../__internal/scheduler/view_model/__mock__/scheduler.mock.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 38e542f395bd..10bc843253c2 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -514,6 +514,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateOption('workSpace', name, value); this.repaint(); break; + case 'skippedDays': + break; case 'indicatorTime': this.updateOption('workSpace', name, value); this.updateOption('header', name, value); 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..bceae06e732e 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 @@ -34,6 +34,7 @@ export const getSchedulerMock = ({ getViewOption: (name: string) => ({ startDayHour, endDayHour, + skippedDays: skippedDays ?? [], allDayPanelMode: 'allDay', cellDuration: 30, }[name]), From 12f577d2f87bba4c53514eb10746a0f1ad7beba5 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 14:00:34 +0200 Subject: [PATCH 61/65] feat: remove internal skippedOptions and create normalize_skipped_days.ts --- .../js/__internal/scheduler/m_scheduler.ts | 6 +-- .../scheduler_options_base_widget.ts | 16 ++++++- .../scheduler/utils/options/constants.ts | 1 - .../utils/options/normalize_skipped_days.ts | 34 ++++++++++++++ .../scheduler/utils/options/types.ts | 1 - .../scheduler/utils/options/utils.test.ts | 9 ++-- .../scheduler/utils/options/utils.ts | 47 ------------------- .../view_model/common/get_compare_options.ts | 2 +- 8 files changed, 57 insertions(+), 59 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/utils/options/normalize_skipped_days.ts diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 10bc843253c2..ce2c478a4817 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1283,7 +1283,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { private headerConfig(): HeaderOptions { return { currentView: this.currentView, - skippedDays: this.getViewOption('skippedDays'), + skippedDays: this.getViewOption('hiddenWeekDays') as number[], views: this.views, currentDate: this.getViewOption('currentDate'), min: this.getViewOption('min'), @@ -1440,7 +1440,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.option('selectedCellData', args.selectedCellData); }, groupByDate: this.getViewOption('groupByDate'), - skippedDays: this.getViewOption('skippedDays'), + skippedDays: this.getViewOption('hiddenWeekDays') as number[], scrolling, draggingMode: this.option('_draggingMode'), timeZoneCalculator: this.timeZoneCalculator, @@ -1462,7 +1462,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { result.onCellClick = this._createActionByOption('onCellClick'); result.onCellContextMenu = this._createActionByOption('onCellContextMenu'); result.currentDate = this.getViewOption('currentDate'); - result.skippedDays = this.getViewOption('skippedDays'); + result.skippedDays = this.getViewOption('hiddenWeekDays') as number[]; result.hoursInterval = result.cellDuration / 60; result.allDayExpanded = false; result.dataCellTemplate = result.dataCellTemplate ? this._getTemplate(result.dataCellTemplate) : null; 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 75723ee1289b..b320c151447a 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -8,6 +8,8 @@ import { DEFAULT_SCHEDULER_OPTIONS, DEFAULT_SCHEDULER_OPTIONS_RULES, } from './utils/options/constants'; +import { DEFAULT_VIEW_OPTIONS } from './utils/options/constants_view'; +import { resolveSkippedDays } from './utils/options/normalize_skipped_days'; import type { NormalizedView, SafeSchedulerOptions, SchedulerOptionsRule, View, } from './utils/options/types'; @@ -103,11 +105,21 @@ export class SchedulerOptionsBaseWidget extends Widget { const viewOptionValue = this.currentView?.[optionName as keyof View]; const optionValue = (viewOptionValue ?? this.option(optionName)) as SafeSchedulerOptions[K]; + if (optionName === 'hiddenWeekDays') { + if (!this.currentView) { + return optionValue; + } + + return resolveSkippedDays( + this.currentView.hiddenWeekDays, + this.option('hiddenWeekDays'), + DEFAULT_VIEW_OPTIONS[this.currentView.type].skippedDays, + ) as SafeSchedulerOptions[K]; + } + return getViewOption( optionName, optionValue, - this.currentView, - this.option('hiddenWeekDays'), ); } diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index a332ab87881b..c0aa187f34bb 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -108,7 +108,6 @@ export const DEFAULT_SCHEDULER_INTERNAL_OPTIONS: SchedulerInternalOptions = { ...DEFAULT_SCHEDULER_OPTIONS.editing, popup: {}, }, - skippedDays: [], // TODO: legacy option property name _draggingMode: 'outlook', // TODO: legacy option property name diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/normalize_skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/options/normalize_skipped_days.ts new file mode 100644 index 000000000000..4f59b77749a3 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/options/normalize_skipped_days.ts @@ -0,0 +1,34 @@ +import errors from '@js/ui/widget/ui.errors'; + +import { isValidWeekday } from '../skipped_days'; + +const normalizeHiddenWeekDays = ( + days: unknown, +): number[] | undefined => { + if (!Array.isArray(days)) { + return undefined; + } + const valid = [...new Set(days)] + .filter(isValidWeekday) + .sort((a, b) => a - b); + if (valid.length >= 7) { + errors.log('W1029'); + return []; + } + return valid; +}; + +export const resolveSkippedDays = ( + perViewHiddenWeekDays: unknown, + globalHiddenWeekDays: number[] | undefined, + viewDefault: number[], +): number[] => { + const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays); + if (perView !== undefined) { + return perView; + } + if (globalHiddenWeekDays !== undefined) { + return normalizeHiddenWeekDays(globalHiddenWeekDays) ?? []; + } + return viewDefault; +}; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts index caa5994a0cf2..eae5521500c5 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts @@ -24,7 +24,6 @@ export interface SchedulerInternalOptions { indicatorTime?: Date; renovateRender: boolean; editing: Properties['editing']; - skippedDays: number[]; _draggingMode: 'outlook' | 'default'; // TODO: legacy option property name _appointmentTooltipOffset: { x: number; y: number }; 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 aa24f151dad1..eec88ffc2ab8 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,8 @@ import { } from '@jest/globals'; import errors from '@js/ui/widget/ui.errors'; +import { DEFAULT_VIEW_OPTIONS } from './constants_view'; +import { resolveSkippedDays } from './normalize_skipped_days'; import type { RawViewType, ViewType } from './types'; import { getCurrentView, @@ -145,11 +147,10 @@ describe('views utils', () => { ): number[] => { const currentView = getCurrentView(viewType, views); - return getViewOption( - 'skippedDays', - currentView.skippedDays, - currentView, + return resolveSkippedDays( + currentView.hiddenWeekDays, globalHiddenWeekDays, + DEFAULT_VIEW_OPTIONS[currentView.type].skippedDays, ); }; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts index 2dafb1658a4a..7b3876df3cac 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -1,46 +1,13 @@ 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'; -import { isValidWeekday } from '../skipped_days'; import { DEFAULT_VIEW_OPTIONS, VIEW_TYPES } from './constants_view'; import type { DateOption, NormalizedView, RawViewType, SafeSchedulerOptions, ViewType, } from './types'; -const normalizeHiddenWeekDays = ( - days: unknown, -): number[] | undefined => { - if (!Array.isArray(days)) { - return undefined; - } - const valid = [...new Set(days)] - .filter(isValidWeekday) - .sort((a, b) => a - b); - if (valid.length >= 7) { - errors.log('W1029'); - return []; - } - return valid; -}; - -const resolveSkippedDays = ( - perViewHiddenWeekDays: unknown, - globalHiddenWeekDays: number[] | undefined, - viewDefault: number[], -): number[] => { - const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays); - if (perView !== undefined) { - return perView; - } - if (globalHiddenWeekDays !== undefined) { - return normalizeHiddenWeekDays(globalHiddenWeekDays) ?? []; - } - 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); @@ -87,21 +54,7 @@ const isDateOption = ( export const getViewOption = ( optionName: K, currentOptionValue: SafeSchedulerOptions[K], - currentView?: NormalizedView, - globalHiddenWeekDays?: number[], ): SafeSchedulerOptions[K] => { - if (optionName === 'skippedDays') { - if (!currentView) { - return currentOptionValue; - } - - return resolveSkippedDays( - currentView.hiddenWeekDays, - globalHiddenWeekDays, - DEFAULT_VIEW_OPTIONS[currentView.type].skippedDays, - ) as SafeSchedulerOptions[K]; - } - if (!isDateOption(optionName)) { return currentOptionValue; } diff --git a/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts index baee90588928..203db8f64802 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/common/get_compare_options.ts @@ -12,7 +12,7 @@ export const getCompareOptions = ( endDayHour: schedulerStore.getViewOption('endDayHour'), min: timeZoneUtils.createUTCDateWithLocalOffset(dateRange[0]).getTime(), max: timeZoneUtils.createUTCDateWithLocalOffset(dateRange[1]).getTime(), - skippedDays: schedulerStore.getViewOption('skippedDays'), + skippedDays: schedulerStore.getViewOption('hiddenWeekDays') as number[], }; return compareOptions; From d55e2a92b71460cf0a17f76090cdd9b0f9aa5209 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 14:01:35 +0200 Subject: [PATCH 62/65] test: return deleted tests --- .../__tests__/workspace.base.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts index fc9107a89926..03c0cad2e4dc 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/workspace.base.test.ts @@ -195,3 +195,61 @@ describe('scheduler workspace scrollTo', () => { expect(scrollableContainer.scrollLeft).toBeCloseTo(-11125); }); }); + +describe('scheduler workspace skipped days support', () => { + beforeEach(() => { + setupSchedulerTestEnvironment(); + }); + + it('should count configured skipped days in week workspace interval math', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWeek, 'week', { + skippedDays: [1, 3], + }); + + expect((workspace as any).getSkippedDaysCount(new Date(2026, 3, 5), 7)).toBe(2); + }); + + it('should use full week layout for work week when skippedDays override is empty', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek', { + currentDate: new Date(2026, 3, 1), // Wednesday + firstDayOfWeek: 0, // Sunday + skippedDays: [], + }); + + expect(workspace.getStartViewDate()).toEqual(new Date(2026, 2, 29)); + expect((workspace as any)._getCellCount()).toBe(7); + }); + + it('should use custom skippedDays in work week runtime layout', () => { + const { workspace } = createWorkspace(SchedulerWorkSpaceWorkWeek, 'workWeek', { + currentDate: new Date(2026, 3, 1), // Wednesday + firstDayOfWeek: 0, // Sunday + skippedDays: [3], // Wednesday + }); + + expect(workspace.getStartViewDate()).toEqual(new Date(2026, 2, 29)); + expect((workspace as any)._getCellCount()).toBe(6); + }); + + it('should skip configured hidden days when incrementing timeline header dates', () => { + const { workspace } = createWorkspace(SchedulerTimelineWeek, 'timelineWeek', { + skippedDays: [3], + }); + const date = new Date(2026, 3, 7); // Tuesday + + (workspace as any).incrementDate(date); + + expect(date).toEqual(new Date(2026, 3, 9)); // Thursday + }); + + it('should skip hidden days when incrementing timeline day dates', () => { + const { workspace } = createWorkspace(SchedulerTimelineDay, 'timelineDay', { + skippedDays: [0, 6], + }); + const date = new Date(2026, 3, 10); // Friday + + (workspace as any).incrementDate(date); + + expect(date).toEqual(new Date(2026, 3, 13)); // Monday + }); +}); From 163cb3c78534af04bde37b146f5cbbc9adfa6f2e Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 14:05:26 +0200 Subject: [PATCH 63/65] refactor: rename file --- .../js/__internal/scheduler/scheduler_options_base_widget.ts | 2 +- .../{normalize_skipped_days.ts => normalize_hidden_days.ts} | 0 .../js/__internal/scheduler/utils/options/utils.test.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/devextreme/js/__internal/scheduler/utils/options/{normalize_skipped_days.ts => normalize_hidden_days.ts} (100%) 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 b320c151447a..ca50ec943665 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -9,7 +9,7 @@ import { DEFAULT_SCHEDULER_OPTIONS_RULES, } from './utils/options/constants'; import { DEFAULT_VIEW_OPTIONS } from './utils/options/constants_view'; -import { resolveSkippedDays } from './utils/options/normalize_skipped_days'; +import { resolveSkippedDays } from './utils/options/normalize_hidden_days'; import type { NormalizedView, SafeSchedulerOptions, SchedulerOptionsRule, View, } from './utils/options/types'; diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/normalize_skipped_days.ts b/packages/devextreme/js/__internal/scheduler/utils/options/normalize_hidden_days.ts similarity index 100% rename from packages/devextreme/js/__internal/scheduler/utils/options/normalize_skipped_days.ts rename to packages/devextreme/js/__internal/scheduler/utils/options/normalize_hidden_days.ts 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 eec88ffc2ab8..c7fe88846ecc 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -4,7 +4,7 @@ import { import errors from '@js/ui/widget/ui.errors'; import { DEFAULT_VIEW_OPTIONS } from './constants_view'; -import { resolveSkippedDays } from './normalize_skipped_days'; +import { resolveSkippedDays } from './normalize_hidden_days'; import type { RawViewType, ViewType } from './types'; import { getCurrentView, From 78ea5c30c19220b3340ea315e70ff28b7a39ea86 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 14:13:09 +0200 Subject: [PATCH 64/65] test: fix test --- .../scheduler/view_model/__mock__/scheduler.mock.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 bceae06e732e..59a57c045d4f 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 @@ -23,7 +23,7 @@ export const getSchedulerMock = ({ isVirtualScrolling?: boolean; }): Scheduler => ({ timeZoneCalculator: mockTimeZoneCalculator, - currentView: { type, skippedDays: skippedDays ?? [] }, + currentView: { type, hiddenWeekDays: skippedDays }, getWorkSpace: () => ({ getDateRange: () => dateRange ?? [ new Date(2000, 0, 10, startDayHour), @@ -34,7 +34,7 @@ export const getSchedulerMock = ({ getViewOption: (name: string) => ({ startDayHour, endDayHour, - skippedDays: skippedDays ?? [], + hiddenWeekDays: skippedDays ?? [], allDayPanelMode: 'allDay', cellDuration: 30, }[name]), From ab97b1520ba579acdf8e99510cfda43f6a9acdd6 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 16 Apr 2026 14:39:36 +0200 Subject: [PATCH 65/65] refactor: apply review --- .../scheduler_options_base_widget.ts | 1 - .../options/normalize_hidden_days.test.ts | 118 ++++++++++++++++++ .../scheduler/utils/options/utils.test.ts | 116 +---------------- 3 files changed, 120 insertions(+), 115 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/utils/options/normalize_hidden_days.test.ts 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 ca50ec943665..50300301227e 100644 --- a/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts +++ b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts @@ -73,7 +73,6 @@ export class SchedulerOptionsBaseWidget extends Widget { switch (args.name) { case 'currentView': case 'views': - case 'hiddenWeekDays': this.updateViews(); break; default: diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/normalize_hidden_days.test.ts b/packages/devextreme/js/__internal/scheduler/utils/options/normalize_hidden_days.test.ts new file mode 100644 index 000000000000..4e844679978e --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/options/normalize_hidden_days.test.ts @@ -0,0 +1,118 @@ +import { + describe, expect, it, jest, +} from '@jest/globals'; +import errors from '@js/ui/widget/ui.errors'; + +import { DEFAULT_VIEW_OPTIONS } from './constants_view'; +import { resolveSkippedDays } from './normalize_hidden_days'; +import type { RawViewType, ViewType } from './types'; +import { getCurrentView } from './utils'; + +describe('hiddenWeekDays', () => { + const getSkipped = ( + views: RawViewType[], + viewType: ViewType, + globalHiddenWeekDays?: number[], + ): number[] => { + const currentView = getCurrentView(viewType, views); + + return resolveSkippedDays( + currentView.hiddenWeekDays, + globalHiddenWeekDays, + DEFAULT_VIEW_OPTIONS[currentView.type].skippedDays, + ); + }; + + it('uses per-view hiddenWeekDays for week', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); + }); + + it('lets workWeek override the default weekends with an empty list', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); + }); + + it('lets workWeek override the default weekends with custom days', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek')).toEqual([3]); + }); + + it('applies global hiddenWeekDays to workWeek', () => { + expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to timelineWorkWeek', () => { + expect(getSkipped(['timelineWorkWeek'], 'timelineWorkWeek', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to week', () => { + expect(getSkipped(['week'], 'week', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to month', () => { + expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); + }); + + it('applies global hiddenWeekDays to timelineWeek', () => { + expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to timelineMonth', () => { + expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to day', () => { + expect(getSkipped(['day'], 'day', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to timelineDay', () => { + expect(getSkipped(['timelineDay'], 'timelineDay', [3])).toEqual([3]); + }); + + it('applies global hiddenWeekDays to agenda', () => { + expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([3]); + }); + + it('removes duplicates from per-view hiddenWeekDays', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); + }); + + it('sorts per-view hiddenWeekDays', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); + }); + + it('filters out invalid per-view hiddenWeekDays values', () => { + expect( + 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]); + }); + + it('falls back to an empty list and logs error when all days are hidden', () => { + const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); + try { + expect( + getSkipped([{ type: 'week', hiddenWeekDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), + ).toEqual([]); + expect(logSpy).toHaveBeenCalledWith('W1029'); + } finally { + logSpy.mockRestore(); + } + }); + + it('uses global hiddenWeekDays when week does not define its own value', () => { + expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); + }); + + it('keeps the per-view value for week when both global and per-view hiddenWeekDays are set', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week', [0, 6])).toEqual([3]); + }); + + it('keeps the per-view value for workWeek when both global and per-view hiddenWeekDays are set', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek', [0, 6])).toEqual([3]); + }); + + it('returns an empty list for week when hiddenWeekDays is not set anywhere', () => { + expect(getSkipped(['week'], 'week')).toEqual([]); + }); +}); 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 c7fe88846ecc..ab8c6ef8170a 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -1,11 +1,8 @@ import { - describe, expect, it, jest, + describe, expect, it, } from '@jest/globals'; -import errors from '@js/ui/widget/ui.errors'; -import { DEFAULT_VIEW_OPTIONS } from './constants_view'; -import { resolveSkippedDays } from './normalize_hidden_days'; -import type { RawViewType, ViewType } from './types'; +import type { RawViewType } from './types'; import { getCurrentView, getViewOption, @@ -138,115 +135,6 @@ describe('views utils', () => { ])('should return normalized $input.type view', ({ input, output }) => { expect(getViews([input as RawViewType])).toEqual([{ ...output, skippedDays: [0, 6] }]); }); - - describe('hiddenWeekDays', () => { - const getSkipped = ( - views: RawViewType[], - viewType: ViewType, - globalHiddenWeekDays?: number[], - ): number[] => { - const currentView = getCurrentView(viewType, views); - - return resolveSkippedDays( - currentView.hiddenWeekDays, - globalHiddenWeekDays, - DEFAULT_VIEW_OPTIONS[currentView.type].skippedDays, - ); - }; - - it('uses per-view hiddenWeekDays for week', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); - }); - - it('lets workWeek override the default weekends with an empty list', () => { - expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); - }); - - it('lets workWeek override the default weekends with custom days', () => { - expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek')).toEqual([3]); - }); - - it('applies global hiddenWeekDays to workWeek', () => { - expect(getSkipped(['workWeek'], 'workWeek', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to timelineWorkWeek', () => { - expect(getSkipped(['timelineWorkWeek'], 'timelineWorkWeek', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to week', () => { - expect(getSkipped(['week'], 'week', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to month', () => { - expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); - }); - - it('applies global hiddenWeekDays to timelineWeek', () => { - expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to timelineMonth', () => { - expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to day', () => { - expect(getSkipped(['day'], 'day', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to timelineDay', () => { - expect(getSkipped(['timelineDay'], 'timelineDay', [3])).toEqual([3]); - }); - - it('applies global hiddenWeekDays to agenda', () => { - expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([3]); - }); - - it('removes duplicates from per-view hiddenWeekDays', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [0, 0, 1, 1] }], 'week')).toEqual([0, 1]); - }); - - it('sorts per-view hiddenWeekDays', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [6, 0, 3] }], 'week')).toEqual([0, 3, 6]); - }); - - it('filters out invalid per-view hiddenWeekDays values', () => { - expect( - 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]); - }); - - it('falls back to an empty list and logs error when all days are hidden', () => { - const logSpy = jest.spyOn(errors, 'log').mockImplementation(() => undefined); - try { - expect( - getSkipped([{ type: 'week', hiddenWeekDays: [0, 1, 2, 3, 4, 5, 6] }], 'week'), - ).toEqual([]); - expect(logSpy).toHaveBeenCalledWith('W1029'); - } finally { - logSpy.mockRestore(); - } - }); - - it('uses global hiddenWeekDays when week does not define its own value', () => { - expect(getSkipped([{ type: 'week' }], 'week', [3])).toEqual([3]); - }); - - it('keeps the per-view value for week when both global and per-view hiddenWeekDays are set', () => { - expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week', [0, 6])).toEqual([3]); - }); - - it('keeps the per-view value for workWeek when both global and per-view hiddenWeekDays are set', () => { - expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [3] }], 'workWeek', [0, 6])).toEqual([3]); - }); - - it('returns an empty list for week when hiddenWeekDays is not set anywhere', () => { - expect(getSkipped(['week'], 'week')).toEqual([]); - }); - }); }); describe('getCurrentView', () => {