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 }, + }, +}; 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/__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", diff --git a/packages/devextreme/js/__internal/scheduler/header/m_header.ts b/packages/devextreme/js/__internal/scheduler/header/m_header.ts index b9b5b19e14fe..927546baf8bf 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.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 new file mode 100644 index 000000000000..169d12606b93 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/header/m_utils.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from '@jest/globals'; + +import { getCaptionInterval, getNextIntervalDate } from './m_utils'; + +describe('agenda hiddenWeekDays support in header utils', () => { + const skippedDays: number[] = [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 calendar days', () => { + expect(getCaptionInterval(options)).toEqual({ + startDate: new Date(2026, 3, 11), + endDate: new Date(2026, 3, 13, 23, 59, 59, 999), + }); + }); + + 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 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 f5d981394439..648d759c701d 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 { + getDateAfterVisibleDays, + getFirstVisibleDate, +} from '@ts/scheduler/utils/skipped_days'; import type { Direction } from './constants'; @@ -78,13 +82,21 @@ const nextAgendaStart = ( ): 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.length > 0) { + return getFirstVisibleDate(weekStart, skippedDays, nextDay); + } + return weekStart; + } case 'workWeek': return getWorkWeekStart(getWeekStart(date, firstDayOfWeek)); case 'agenda': @@ -98,10 +110,18 @@ const getPeriodEndDate = ( currentPeriodStartDate: Date, step: Step, agendaDuration: number, + skippedDays: number[], ): Date => { const calculators: Record Date> = { day: () => nextDay(currentPeriodStartDate), - week: () => nextWeek(currentPeriodStartDate), + week: () => (skippedDays.length > 0 + ? getDateAfterVisibleDays( + currentPeriodStartDate, + 7 - skippedDays.length, + skippedDays, + nextDay, + ) + : nextWeek(currentPeriodStartDate)), month: () => nextMonth(currentPeriodStartDate), workWeek: () => getDateAfterWorkWeek(currentPeriodStartDate), agenda: () => nextAgendaStart(currentPeriodStartDate, agendaDuration), @@ -110,20 +130,28 @@ 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.length > 0) { + date = getFirstVisibleDate(date, skippedDays, nextDay); } 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 +160,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..061118859c4b 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..f2ee28d53680 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 'hiddenWeekDays': + this.repaint(); + break; case 'useDropDownViewSwitcher': this.updateOption('header', name, value); break; 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..f9e08019185e 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,38 @@ describe('calculateRows', () => { [0, 2, 1, 0, 2, 0, 0], ]); }); + + 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('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 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 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 8bddcb45b093..c6d475fd17dd 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/agenda.ts @@ -2,23 +2,45 @@ import timeZoneUtils from '../../m_utils_time_zone'; import type { ListEntity } from '../../view_model/types'; import { setOptionHour } from './base'; -export const calculateStartViewDate = (currentDate: Date, startDayHour: number): Date => { +export const calculateStartViewDate = ( + currentDate: Date, + startDayHour: number, +): Date => { const validCurrentDate = new Date(currentDate); - return setOptionHour(validCurrentDate, startDayHour); }; const getDayStart = (date: Date | number): number => new Date(date).setUTCHours(0, 0, 0, 0); +export const getDateByIndex = ( + startViewDate: Date, + index: number, +): Date => { + const date = new Date(startViewDate); + date.setDate(date.getDate() + index); + return date; +}; + +export const calculateEndViewDate = ( + startViewDate: Date, + endDayHour: number, + agendaDuration: number, +): Date => { + const lastVisibleDate = getDateByIndex( + startViewDate, + Math.max(agendaDuration - 1, 0), + ); + const endViewDate = setOptionHour(lastVisibleDate, endDayHour); + + return new Date(endViewDate.getTime() - 60000); +}; + export const calculateRows = ( appointments: ListEntity[], agendaDuration: number, - currentDate: Date, + startViewDate: Date, groupCount: number, ): number[][] => { - const dayMs = getDayStart( - timeZoneUtils.createUTCDateWithLocalOffset(currentDate), - ); const intervalsStartMap = new Map(); const result = Array.from( { length: groupCount || 1 }, @@ -26,8 +48,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); + 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/scheduler_options_base_widget.ts b/packages/devextreme/js/__internal/scheduler/scheduler_options_base_widget.ts index 23883fcf5d21..67affdb4733f 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,11 @@ export class SchedulerOptionsBaseWidget extends Widget { protected updateViews(): void { const views = this.option('views') ?? []; - this.views = getViews(views); + this.views = getViews(views, this.option('hiddenWeekDays')); this.currentView = getCurrentView( this.option('currentView') ?? '', views, + this.option('hiddenWeekDays'), ); } @@ -71,6 +72,7 @@ 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/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index b77adcfbdb06..c0aa187f34bb 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -54,6 +54,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = { maxAppointmentsPerCell: 'auto', selectedCellData: [], groupByDate: false, + hiddenWeekDays: undefined, onAppointmentRendered: undefined, onAppointmentClick: undefined, onAppointmentDblClick: undefined, 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..6585e5bcc9a4 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 = [0, 6]; +const WEEKENDS: number[] = [0, 6]; const getView = ( type: ViewType, groupOrientation: View['groupOrientation'], 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..a82da2a4cc40 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.test.ts @@ -1,19 +1,27 @@ import { - describe, expect, it, + describe, expect, it, jest, } from '@jest/globals'; +import errors from '@js/ui/widget/ui.errors'; +import type { RawViewType, ViewType } from './types'; import { - getCurrentView, getViewOption, getViews, parseCurrentDate, parseDateOption, + getCurrentView, + getViewOption, + getViews, + parseCurrentDate, + parseDateOption, } from './utils'; 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', () => { @@ -24,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([ @@ -106,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([ @@ -126,7 +134,100 @@ 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: RawViewType[], + viewType: ViewType, + globalHiddenWeekDays?: number[], + ): number[] => { + const result = getViews(views, globalHiddenWeekDays); + const view = result.find((v) => v.type === viewType); + return view?.skippedDays ?? []; + }; + + it('per-view hiddenWeekDays on week → uses per-view value', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [3] }], 'week')).toEqual([3]); + }); + + it('per-view hiddenWeekDays: [] on workWeek → overrides built-in default', () => { + expect(getSkipped([{ type: 'workWeek', hiddenWeekDays: [] }], 'workWeek')).toEqual([]); + }); + + it('per-view hiddenWeekDays on workWeek → overrides built-in default', () => { + 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('global hiddenWeekDays on week → applied', () => { + expect(getSkipped(['week'], 'week', [3])).toEqual([3]); + }); + + it('global hiddenWeekDays on month → applied', () => { + expect(getSkipped(['month'], 'month', [0, 6])).toEqual([0, 6]); + }); + + it('global hiddenWeekDays on timelineWeek → applied', () => { + expect(getSkipped(['timelineWeek'], 'timelineWeek', [3])).toEqual([3]); + }); + + it('global hiddenWeekDays on timelineMonth → applied', () => { + expect(getSkipped(['timelineMonth'], 'timelineMonth', [3])).toEqual([3]); + }); + + it('global hiddenWeekDays on day → ignored (unsupported view)', () => { + expect(getSkipped(['day'], 'day', [3])).toEqual([]); + }); + + it('global hiddenWeekDays on agenda → applied', () => { + expect(getSkipped(['agenda'], 'agenda', [3])).toEqual([3]); + }); + + it('per-view hiddenWeekDays dedupes duplicates', () => { + expect(getSkipped([{ type: 'week', hiddenWeekDays: [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 hiddenWeekDays filters out invalid 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('hiddenWeekDays 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'), + ).toEqual([]); + expect(logSpy).toHaveBeenCalledWith('W1029'); + } finally { + logSpy.mockRestore(); + } + }); + + it('global hiddenWeekDays + 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('no hiddenWeekDays anywhere on week → []', () => { + expect(getSkipped(['week'], 'week')).toEqual([]); + }); }); }); @@ -178,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 2a266891a5a9..9a3e1e75cb99 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/utils.ts @@ -1,31 +1,111 @@ 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 VIEWS_SUPPORTING_HIDDEN_DAYS: ReadonlySet = new Set([ + 'week', 'month', 'timelineWeek', 'timelineMonth', 'agenda', +]); + +const VIEWS_WITH_BUILTIN_SKIPPED: ReadonlySet = new Set([ + 'workWeek', 'timelineWorkWeek', +]); + +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 = ( + viewType: ViewType, + perViewHiddenWeekDays: unknown, + globalHiddenWeekDays: number[] | undefined, + viewDefault: number[], +): number[] => { + const perView = normalizeHiddenWeekDays(perViewHiddenWeekDays); + 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) ?? []; + } + 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, + 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 }; +}; -export const getViews = (views: RawViewType[]): NormalizedView[] => views +export const getViews = ( + views: RawViewType[], + globalHiddenWeekDays?: number[], +): NormalizedView[] => views .filter(isKnownView) - .map(normalizeView) + .map((v) => normalizeView(v, globalHiddenWeekDays)) .filter(isExistedView); export function getCurrentView( currentView: string | ViewType, views: RawViewType[], + globalHiddenWeekDays?: number[], ): NormalizedView { - const viewsProps = getViews(views); + const viewsProps = getViews(views, globalHiddenWeekDays); const currentViewProps = viewsProps.find( (view) => [view.name, view.type].includes(currentView), ); 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..d4393c9ddc33 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/skipped_days.ts @@ -0,0 +1,60 @@ +export const isValidWeekday = (value: unknown): value is number => ( + typeof value === 'number' + && Number.isInteger(value) + && value >= 0 + && value <= 6 +); + +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 raw = firstDayOfWeek + count; + const dayOfWeek = ((raw % 7) + 7) % 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 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; +}; 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..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 @@ -12,7 +12,10 @@ interface Options { const filterBySkippedDays = ( intervals: T[], skippedDays: number[], -): T[] => intervals.filter((item) => !skippedDays.includes(new Date(item.min).getUTCDay())); +): T[] => intervals.filter((item) => { + const weekday = new Date(item.min).getUTCDay(); + return !skippedDays.includes(weekday); +}); export const getMinutesCellIntervals = ({ intervals, diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts index a886699802ec..969eeda71374 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_agenda.ts @@ -152,7 +152,10 @@ 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.rows = []; } @@ -441,10 +444,10 @@ 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, + ); } private getRowHeight(rowSize) { @@ -485,14 +488,11 @@ 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, + ); } getEndViewDateByEndDayHour() { 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..ec88f77e17a3 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,6 +909,7 @@ class SchedulerWorkSpace extends Widget { startDate: this.option('startDate'), firstDayOfWeek: this.option('firstDayOfWeek'), showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'), + skippedDays: this.option('skippedDays'), ...this.virtualScrollingDispatcher.getRenderState(), }; @@ -1302,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, }; } @@ -2291,6 +2298,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 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.test.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts new file mode 100644 index 000000000000..567179ed8237 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.test.ts @@ -0,0 +1,292 @@ +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]; + 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, + cellCountInDay: number, + ): number => (g as unknown as { + 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, 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, 1)).toBe(0); + }); + 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, 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, 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('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(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(timelineWorkWeek, 0, 2, 1, 2)).toBe(0); + // After 5 visible days (10 cells), the next day jumps over weekend (+2 days). + expect(callGetVisibleDayOffset(timelineWorkWeek, 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', () => { + const gen = new ViewDataGeneratorMonth('month' as ViewType); + + const callGetVisibleDayOffset = ( + g: ViewDataGeneratorMonth, + rowIndex: number, + columnIndex: number, + firstDayOfWeek: number, + cellCountInDay: number, + ): number => (g as unknown as { + 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, 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, 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, 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); + }); + }); + + 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); + }); + }); + + 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); + }); + }); + + 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.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator.ts index d7626023448d..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 @@ -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, @@ -28,14 +32,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 +60,54 @@ export class ViewDataGenerator { ].includes(this.viewType); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public isSkippedDate(date: any) { + protected usesMonthDayLayout(): boolean { return false; } + public getVisibleDaysOfWeek(firstDayOfWeek: number): number[] { + return getVisibleDaysOfWeek(firstDayOfWeek, this.skippedDays); + } + + protected getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + startViewDate: Date, // eslint-disable-line @typescript-eslint/no-unused-vars + ): number { + return this.getFirstDayOfWeek(firstDayOfWeekOption) ?? 0; + } + + protected getVisibleDayOffset( + rowIndex: number, + columnIndex: number, + anchorDay: number, + cellCountInDay: number, + ): number { + const rotated = this.getVisibleDaysOfWeek(anchorDay); + 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 - anchorDay + 7) % 7); + return actualDayOffset - naiveDayOffset; + } + const dayIndex = isHorizontalView(this.viewType) + ? Math.floor(columnIndex / cellCountInDay) + : columnIndex; + const week = Math.floor(dayIndex / visibleCount); + const idxInWeek = dayIndex % visibleCount; + const targetDayOfWeek = rotated[idxInWeek]; + const naiveDayOffset = dayIndex; + const actualDayOffset = week * 7 + ((targetDayOfWeek - anchorDay + 7) % 7); + return actualDayOffset - naiveDayOffset; + } + + public isSkippedDate(date: Date): boolean { + return isDateSkipped(date, this.skippedDays); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars protected calculateStartViewDate(options: any): Date { return new Date(); @@ -74,6 +134,7 @@ export class ViewDataGenerator { hoursInterval, } = options; + this.skippedDays = options.skippedDays ?? this.skippedDays; this.setVisibilityDates(options); this.setHiddenInterval(startDayHour, endDayHour, hoursInterval); @@ -512,13 +573,24 @@ 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.skippedDays.length > 0) { + offsetByCount = this.getVisibleDayOffset( + rowIndex, + columnIndex, + this.getSkippedDaysAnchorDay(firstDayOfWeek, startViewDate), + cellCountInDay, + ) * toMs('day'); + } else if (this.isWorkWeekView()) { + offsetByCount = this.getTimeOffsetByColumnIndex( columnIndex, this.getFirstDayOfWeek(firstDayOfWeek), columnCountBase, intervalCount, - ) : 0; + ); + } 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_timeline_month.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_view_data_generator_timeline_month.ts index dd0f45ed6e7e..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 @@ -2,12 +2,20 @@ 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; export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { - calculateEndDate(startDate, interval, endDayHour) { + protected override getSkippedDaysAnchorDay( + firstDayOfWeekOption: number | undefined, + startViewDate: Date, + ): number { + return startViewDate.getDay(); + } + + calculateEndDate(startDate: Date, interval: number, endDayHour: number): Date { return setOptionHour(startDate, endDayHour); } @@ -15,7 +23,11 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { return toMs('day'); } - protected calculateStartViewDate(options: any) { + getCellCountInDay(): number { + return 1; + } + + protected calculateStartViewDate(options: any): Date { return timelineMonthUtils.calculateStartViewDate( options.currentDate, options.startDayHour, @@ -24,19 +36,26 @@ export class ViewDataGeneratorTimelineMonth extends ViewDataGenerator { ); } - 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++) { - cellCount += new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 0).getDate(); + 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) { + const date = new Date(monthDate.getFullYear(), monthDate.getMonth(), day); + if (!this.isSkippedDate(date)) { + cellCount += 1; + } + } } return cellCount; } - setHiddenInterval() { + setHiddenInterval(): void { this.hiddenInterval = 0; } 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..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,14 +1,12 @@ -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; + protected baseDaysInInterval = 5; - isSkippedDate(date) { - return isDataOnWeekend(date); - } + public skippedDays: number[] = [0, 6]; - protected calculateStartViewDate(options) { + protected override calculateStartViewDate(options: any): Date { return workWeekUtils.calculateStartViewDate( options.currentDate, options.startDayHour, @@ -18,7 +16,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); } } 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 e340779e1e15..dce5034ed7a1 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 "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 51da85b9e775..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] */ @@ -26909,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] */