From 5df48f5879409cf87fe0d0b189917f83108a27db Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 10:27:55 +0200 Subject: [PATCH 01/17] refactor: add simple return types --- .../js/__internal/scheduler/m_scheduler.ts | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 5a02a006afb0..8893c5b264a5 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -244,7 +244,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.timeZoneCalculatorInstance; } - private postponeDataSourceLoading(promise?: any) { + private postponeDataSourceLoading(promise?: any): void { this.postponedOperations.add('_reloadDataSource', this.reloadDataSource.bind(this), promise); } @@ -613,7 +613,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private bringEditingModeToAppointments(editing) { + private bringEditingModeToAppointments(editing): void { const editingConfig: any = { allowDelete: editing.allowUpdating && editing.allowDeleting, }; @@ -628,7 +628,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.repaint(); } - private isAgenda() { + private isAgenda(): boolean { return this.currentView.type === 'agenda'; } @@ -640,19 +640,19 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.allowDragging() && !this._isAppointmentBeingUpdated(appointmentData); } - private allowResizing() { + private allowResizing(): boolean { return this.editing.allowResizing && !this.isAgenda(); } - private allowAllDayResizing() { + private allowAllDayResizing(): boolean { return this.editing.allowResizing && this.supportAllDayResizing(); } - private supportAllDayResizing() { + private supportAllDayResizing(): boolean { return this.currentView.type !== 'day' || this.currentView.intervalCount > 1; } - private isAllDayExpanded() { + private isAllDayExpanded(): boolean { return this.option('showAllDayPanel') && this._layoutManager.hasAllDayAppointments(); } @@ -712,7 +712,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return result.promise(); } - _fireContentReadyAction(result?: any) { + _fireContentReadyAction(result?: any): void { // @ts-expect-error const contentReadyBase = super._fireContentReadyAction.bind(this); const fireContentReady = () => { @@ -729,7 +729,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - _dimensionChanged(value, isForce = false) { + _dimensionChanged(value, isForce = false): void { const isFixedHeight = typeof this.option('height') === 'number'; const isFixedWidth = typeof this.option('width') === 'number'; @@ -764,22 +764,22 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.appointmentPopup.updatePopupFullScreenMode(); } - _clean() { + _clean(): void { this.cleanPopup(); // @ts-expect-error super._clean(); } - private toggleSmallClass() { + private toggleSmallClass(): void { const { width } = getBoundingRect((this.$element() as any).get(0)); (this.$element() as any).toggleClass(WIDGET_SMALL_CLASS, width < WIDGET_SMALL_WIDTH); } - private toggleAdaptiveClass() { + private toggleAdaptiveClass(): void { (this.$element() as any).toggleClass(WIDGET_ADAPTIVE_CLASS, this.option('adaptivityEnabled')); } - _visibilityChanged(visible) { + _visibilityChanged(visible): void { visible && this._dimensionChanged(null, true); } @@ -787,13 +787,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { return { paginate: false }; } - private initAllDayPanel() { + private initAllDayPanel(): void { if (this.option('allDayPanelMode') === 'hidden') { this.option('showAllDayPanel', false); } } - _init() { + _init(): void { this.timeZonesPromise = timeZoneUtils.cacheTimeZones(); this.initExpressions({ startDateExpr: this.option('startDateExpr'), @@ -852,12 +852,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - createAppointmentDataSource() { + createAppointmentDataSource(): void { this.appointmentDataSource?.destroy(); this.appointmentDataSource = new AppointmentDataSource(this._dataSource); } - updateAppointmentDataSource() { + updateAppointmentDataSource(): void { this.timeZoneCalculatorInstance = null; if (this.getWorkSpace()) { @@ -865,14 +865,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private customizeDataSourceLoadOptions() { + private customizeDataSourceLoadOptions(): void { this._dataSource?.on('customizeStoreLoadOptions', ({ storeLoadOptions }) => { storeLoadOptions.startDate = this.getStartViewDate(); storeLoadOptions.endDate = this.getEndViewDate(); }); } - _initTemplates() { + _initTemplates(): void { this.initAppointmentTemplate(); this._templateManager.addDefaultTemplates({ @@ -884,7 +884,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } // TODO: delete this method when old impl is removed - private initAppointmentTemplate() { + private initAppointmentTemplate(): void { const { expr } = this._dataAccessors; const createGetter = (property) => compileGetter(`appointmentData.${property}`); @@ -925,12 +925,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - _renderContent() { + _renderContent(): void { // @ts-expect-error this._renderContentImpl(); } - _dataSourceChangedHandler(result?: Appointment[]) { + _dataSourceChangedHandler(result?: Appointment[]): void { if (this.readyToRenderAppointments) { this.workSpaceRecalculation.done(() => { this._layoutManager.prepareAppointments(result); @@ -952,7 +952,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return scrolling?.mode === 'virtual'; } - private renderAppointments() { + private renderAppointments(): void { const workspace = this.getWorkSpace(); this._layoutManager.filterAppointments(); @@ -971,7 +971,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private initExpressions(fields: IFieldExpr) { + private initExpressions(fields: IFieldExpr): void { this._dataAccessors = new AppointmentDataAccessor( fields, Boolean(config().forceIsoDateParsing), @@ -979,11 +979,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - private updateExpression(name: string, value: string) { + private updateExpression(name: string, value: string): void { this._dataAccessors.updateExpression(name, value); } - private initEditing() { + private initEditing(): void { const editing = this.option('editing'); this.editing = { @@ -1010,7 +1010,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { (this.$element() as any).toggleClass(WIDGET_READONLY_CLASS, isReadOnly); } - _dispose() { + _dispose(): void { this.resourceManager?.dispose(); this.appointmentTooltip?.dispose(); this.recurrenceDialog?.hide(RECURRENCE_EDITING_MODE.CANCEL); @@ -1028,7 +1028,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { super._dispose(); } - private initActions() { + private initActions(): void { this.actions = { onAppointmentAdding: this._createActionByOption(StoreEventNames.ADDING), onAppointmentAdded: this._createActionByOption(StoreEventNames.ADDED), @@ -1053,9 +1053,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - _renderFocusTarget() { return noop(); } + _renderFocusTarget(): void { return noop(); } - private updateA11yStatus() { + private updateA11yStatus(): void { const dateRange = this._workSpace.getDateRange(); const indicatorTime = this.option('showCurrentTimeIndicator') ? getToday(this.option('indicatorTime') as Date, this.timeZoneCalculator) @@ -1073,14 +1073,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.a11yStatus.text(label); } - private renderA11yStatus() { + private renderA11yStatus(): void { this.a11yStatus = createA11yStatusContainer(); this.a11yStatus.prependTo(this.$element()); // @ts-expect-error this.setAria({ role: 'application' }); } - private initMarkupOnResourceLoaded() { + private initMarkupOnResourceLoaded(): void { if (!(this as any)._disposed) { this.initMarkupCore(); this.reloadDataSource(); @@ -1161,7 +1161,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private createAppointmentPopupForm() { + private createAppointmentPopupForm(): void { if (this.appointmentForm) { this.appointmentForm.dispose(); } @@ -1171,7 +1171,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.appointmentPopup = this.createAppointmentPopup(this.appointmentForm); } - private renderMainContainer() { + private renderMainContainer(): void { this.mainContainer = $('
').addClass('dx-scheduler-container'); this.$draggableContainer = $('
') .addClass('dx-scheduler-draggable-container') @@ -1290,7 +1290,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - checkAndDeleteAppointment(appointment, targetedAppointment) { + checkAndDeleteAppointment(appointment, targetedAppointment): void { const targetedAdapter = new AppointmentAdapter( targetedAppointment, this._dataAccessors, @@ -1316,14 +1316,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { }; } - isAppointmentInAllDayPanel(appointmentData) { + isAppointmentInAllDayPanel(appointmentData): boolean { const workSpace = this._workSpace; const itTakesAllDay = this.appointmentTakesAllDay(appointmentData); return itTakesAllDay && workSpace.supportAllDayRow() && workSpace.option('showAllDayPanel'); } - private initMarkupCore() { + private initMarkupCore(): void { this.readyToRenderAppointments = hasWindow(); this._workSpace && this.cleanWorkspace(); @@ -1345,18 +1345,18 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateA11yStatus(); } - private isDataSourceLoaded() { + private isDataSourceLoaded(): boolean { return this._dataSource?.isLoaded(); } - _render() { + _render(): void { this.getWorkSpace()?.updateHeaderEmptyCellWidth(); // @ts-expect-error super._render(); } - private renderHeader() { + private renderHeader(): void { const toolbarOptions = this.option('toolbar'); const isHeaderShown = Boolean( toolbarOptions.visible ?? toolbarOptions.items?.length, @@ -1431,7 +1431,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return config; } - private renderWorkSpace() { + private renderWorkSpace(): void { const currentViewOptions = this.currentView; if (!currentViewOptions) { return; @@ -1482,7 +1482,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._workSpace.getWorkArea().append(this._appointments.$element()); } - private recalculateWorkspace() { + private recalculateWorkspace(): void { // @ts-expect-error this.workSpaceRecalculation = new Deferred(); triggerResizeEvent(this._workSpace.$element()); @@ -1587,7 +1587,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return result; } - private waitAsyncTemplate(callback) { + private waitAsyncTemplate(callback): void { if (this._options.silent('templatesRenderAsynchronously')) { const timer = setTimeout(() => { callback(); @@ -1637,7 +1637,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private cleanWorkspace() { + private cleanWorkspace(): void { this._appointments.$element().detach(); this._workSpace._dispose(); this._workSpace.$element().remove(); @@ -1661,7 +1661,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.header; } - private cleanPopup() { + private cleanPopup(): void { this.appointmentPopup?.dispose(); } @@ -1704,7 +1704,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { dragEvent?: any, recurrenceEditMode?: any, onCancel?: () => void, - ) { + ): void { const recurrenceRule = this._dataAccessors.get('recurrenceRule', rawAppointment); if (!validateRRule(recurrenceRule) || !this.editing.allowUpdating) { @@ -1748,7 +1748,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private excludeAppointmentFromSeries(rawAppointment, newRawAppointment, exceptionDate, isDeleted, isPopupEditing, dragEvent) { + private excludeAppointmentFromSeries(rawAppointment, newRawAppointment, exceptionDate, isDeleted, isPopupEditing, dragEvent): void { const appointment = excludeFromRecurrence( rawAppointment, exceptionDate, @@ -1787,7 +1787,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private createRecurrenceException(appointment, exceptionDate) { + private createRecurrenceException(appointment, exceptionDate): string { const result: any[] = []; if (appointment.recurrenceException) { @@ -1798,7 +1798,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return result.join(); } - private getSerializedDate(date, startDate, isAllDay) { + private getSerializedDate(date, startDate, isAllDay): string { isAllDay && date.setHours( startDate.getHours(), startDate.getMinutes(), @@ -1934,7 +1934,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - subscribe(subject, action) { + subscribe(subject, action): void { this.subscribes[subject] = subscribes[subject] = action; } @@ -2038,13 +2038,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { return deferred.promise(); } - private expandAllDayPanel(appointment) { + private expandAllDayPanel(appointment): void { if (!this.isAllDayExpanded() && this.appointmentTakesAllDay(appointment)) { this.updateOption('workSpace', 'allDayExpanded', true); } } - private onDataPromiseCompleted(handlerName, storeAppointment, appointment?: any) { + private onDataPromiseCompleted(handlerName, storeAppointment, appointment?: any): void { const args: any = { appointmentData: appointment || storeAppointment }; if (storeAppointment instanceof Error) { @@ -2069,7 +2069,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.actions; } - appointmentTakesAllDay(rawAppointment) { + appointmentTakesAllDay(rawAppointment): boolean { const appointment = new AppointmentAdapter( rawAppointment, this._dataAccessors, @@ -2081,7 +2081,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - dayHasAppointment(day, rawAppointment, trimTime) { + dayHasAppointment(day, rawAppointment, trimTime): boolean { const getConvertedToTimeZone = (date) => this.timeZoneCalculator.createDate(date, 'toGrid'); const appointment = new AppointmentAdapter( @@ -2120,7 +2120,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace.getEndViewDate(); } - showAddAppointmentPopup(cellData, cellGroups) { + showAddAppointmentPopup(cellData, cellGroups): void { const appointmentAdapter = new AppointmentAdapter({}, this._dataAccessors); appointmentAdapter.allDay = Boolean(cellData.allDay); @@ -2131,7 +2131,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.showAppointmentPopup(resultAppointment, true); } - showAppointmentPopup(rawAppointment?: any, createNewAppointment?: boolean, rawTargetedAppointment?: any) { + showAppointmentPopup(rawAppointment?: any, createNewAppointment?: boolean, rawTargetedAppointment?: any): void { const newRawTargetedAppointment = { ...rawTargetedAppointment }; if (newRawTargetedAppointment) { delete newRawTargetedAppointment.displayStartDate; @@ -2189,7 +2189,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return result; } - hideAppointmentPopup(saveChanges?: any) { + hideAppointmentPopup(saveChanges?: any): void { if (this.appointmentPopup?.visible) { saveChanges && this.appointmentPopup.saveChangesAsync(); this.appointmentPopup.hide(); @@ -2201,7 +2201,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { appointment: SafeAppointment, element: dxElementWrapper, targetedAppointment?: SafeAppointment, - ) { + ): void { if (appointment) { const settings: any = utils.dataAccessors.getAppointmentSettings(element); const appointmentConfig = { @@ -2224,7 +2224,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { target: dxElementWrapper, data: AppointmentTooltipItem[], options?: AppointmentTooltipExtraOptions, - ) { + ): void { const arg: Omit = { cancel: false, appointments: data.map((item) => ({ @@ -2249,7 +2249,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - hideAppointmentTooltip() { + hideAppointmentTooltip(): void { this.appointmentTooltip?.hide(); } @@ -2257,7 +2257,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { date: Date, groupValuesOrOptions?: ScrollToGroupValuesOrOptions, allDay?: boolean | undefined, - ) { + ): void { let groupValues; let allDayValue; let align: 'start' | 'center' = 'center'; @@ -2283,7 +2283,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { && ('alignInView' in options || 'allDay' in options || 'group' in options); } - private isHorizontalVirtualScrolling() { + private isHorizontalVirtualScrolling(): boolean { const scrolling = this.option('scrolling'); const { orientation, mode } = scrolling; const isVirtualScrolling = mode === 'virtual'; @@ -2327,7 +2327,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.updateAppointmentCore(target, appointment); } - deleteAppointment(rawAppointment) { + deleteAppointment(rawAppointment): void { const deletingOptions = this.fireOnAppointmentDeleting(rawAppointment); this.processDeleteAppointment(rawAppointment, deletingOptions); } @@ -2344,7 +2344,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return deletingOptions; } - processDeleteAppointment(rawAppointment, deletingOptions) { + processDeleteAppointment(rawAppointment, deletingOptions): void { this.processActionResult(deletingOptions, function (canceled) { if (!canceled) { this.appointmentDataSource @@ -2362,7 +2362,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { appointment, date: Date | string, recurrenceEditMode, - ) { + ): void { if (typeof date === 'string') { date = new Date(date); } @@ -2383,7 +2383,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - focus() { + focus(): void { if (this.editAppointmentData) { this._appointments.focus(); } else { @@ -2401,7 +2401,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { : dateLocalization.firstDayOfWeekIndex() as DayOfWeek; } - private validateKeyFieldIfAgendaExist() { + private validateKeyFieldIfAgendaExist(): void { if (!this.appointmentDataSource.isDataSourceInit) { return; } From 5e91cc3843cb87662979bc63705d95f4e1102fe6 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 10:29:55 +0200 Subject: [PATCH 02/17] refactor: enhance type safety for time zone calculator in scheduler --- .../devextreme/js/__internal/scheduler/m_scheduler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 8893c5b264a5..5e7aed7a9916 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -52,7 +52,7 @@ import { CompactAppointmentsHelper } from './m_compact_appointments_helper'; import type { SubscribeKey, SubscribeMethods } from './m_subscribes'; import subscribes from './m_subscribes'; import { combineRemoteFilter } from './r1/filterting/remote'; -import { createTimeZoneCalculator } from './r1/timezone_calculator/index'; +import { createTimeZoneCalculator, type TimeZoneCalculator } from './r1/timezone_calculator/index'; import { excludeFromRecurrence, getToday, @@ -165,7 +165,7 @@ const RECURRENCE_EDITING_MODE = { class Scheduler extends SchedulerOptionsBaseWidget { // NOTE: Do not initialize variables here, because `_initMarkup` function runs before constructor, // and initialization in constructor will erase the data - private timeZoneCalculatorInstance!: any; + private timeZoneCalculatorInstance: TimeZoneCalculator | null = null; postponedOperations: any; @@ -236,7 +236,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { private timeZonesPromise!: Promise; - get timeZoneCalculator() { + get timeZoneCalculator(): TimeZoneCalculator { if (!this.timeZoneCalculatorInstance) { this.timeZoneCalculatorInstance = createTimeZoneCalculator(this.option('timeZone')); } @@ -1206,7 +1206,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { getEditingConfig: () => this.editing, - getTimeZoneCalculator: () => this.timeZoneCalculator, + getTimeZoneCalculator: (): TimeZoneCalculator => this.timeZoneCalculator, getDataAccessors: () => this._dataAccessors, getAppointmentFormOpening: () => this.actions.onAppointmentFormOpening, processActionResult: (arg, canceled) => this.processActionResult(arg, canceled), From 20a1eb3f2437776b28b7697b86e1739daea13428 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 10:31:38 +0200 Subject: [PATCH 03/17] refactor: improve type annotations for scheduler methods --- .../js/__internal/scheduler/m_scheduler.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 5e7aed7a9916..ca4061f3dd27 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -26,6 +26,7 @@ import { isPromise, } from '@js/core/utils/type'; import { hasWindow } from '@js/core/utils/window'; +import type { DataSourceOptions } from '@js/data/data_source'; import DataHelperMixin from '@js/data_helper'; import { custom as customDialog } from '@js/ui/dialog'; import type { @@ -783,7 +784,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { visible && this._dimensionChanged(null, true); } - _dataSourceOptions() { + _dataSourceOptions(): DataSourceOptions { return { paginate: false }; } @@ -1120,7 +1121,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, getDataAccessor: () => this._dataAccessors, - getStartViewDate: () => this.getStartViewDate(), + getStartViewDate: (): Date => this.getStartViewDate()!, getSortedItems: () => this._layoutManager.sortedItems, isVirtualScrolling: () => this.isVirtualScrolling(), @@ -1180,7 +1181,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.$element().append(this.mainContainer); } - createAppointmentForm() { + createAppointmentForm(): AppointmentForm { const config: AppointmentFormConfig = { dataAccessors: this._dataAccessors, editing: this.editing, @@ -1195,7 +1196,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return new AppointmentForm(config); } - createAppointmentPopup(form) { + createAppointmentPopup(form: AppointmentForm): AppointmentPopup { const scheduler = { getElement: () => this.$element(), // @ts-expect-error @@ -1307,7 +1308,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - private getExtraAppointmentTooltipOptions() { + private getExtraAppointmentTooltipOptions(): AppointmentTooltipExtraOptions { return { rtlEnabled: this.option('rtlEnabled'), focusStateEnabled: this.option('focusStateEnabled'), @@ -1379,7 +1380,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { min: this.getViewOption('min'), max: this.getViewOption('max'), indicatorTime: this.option('indicatorTime'), - startViewDate: this.getStartViewDate(), + startViewDate: this.getStartViewDate()!, tabIndex: this.option('tabIndex'), focusStateEnabled: this.option('focusStateEnabled'), useDropDownViewSwitcher: this.option('useDropDownViewSwitcher'), @@ -1657,7 +1658,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace; } - getHeader() { + getHeader(): SchedulerHeader | undefined { return this.header; } @@ -2061,7 +2062,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._appointments; } - getLayoutManager() { + getLayoutManager(): AppointmentLayoutManager { return this._layoutManager; } @@ -2112,11 +2113,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { return startDateTimeStamp <= dayTimeStamp && dayTimeStamp <= endDateTimeStamp; } - getStartViewDate() { + getStartViewDate(): Date | undefined { return this._workSpace?.getStartViewDate(); } - getEndViewDate() { + getEndViewDate(): Date { return this._workSpace.getEndViewDate(); } From 52ebedc4fca1ad1b83dfdbc110a58d77deaac15c Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 11:51:32 +0200 Subject: [PATCH 04/17] refactor: add type to workspace in scheduler --- .../js/__internal/scheduler/m_scheduler.ts | 45 ++++++++++++------- .../scheduler/workspaces/work_space.ts | 2 +- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index ca4061f3dd27..014f6a75653f 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -8,6 +8,7 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; import { EmptyTemplate } from '@js/core/templates/empty_template'; +import type { FunctionTemplate } from '@js/core/templates/function_template'; import Callbacks from '@js/core/utils/callbacks'; import { noop } from '@js/core/utils/common'; import { compileGetter } from '@js/core/utils/data'; @@ -35,6 +36,7 @@ import type { import errors from '@js/ui/widget/ui.errors'; import { dateUtilsTs } from '@ts/core/utils/date'; import type { OptionChanged } from '@ts/core/widget/types'; +import type Scrollable from '@ts/ui/scroll_view/scrollable'; import { createA11yStatusContainer } from './a11y_status/a11y_status_render'; import { getA11yStatusText } from './a11y_status/a11y_status_text'; @@ -49,6 +51,7 @@ import NotifyScheduler from './base/widget_notify_scheduler'; import { SchedulerHeader } from './header/header'; import type { HeaderOptions } from './header/types'; import { hide as hideLoading, show as showLoading } from './loading'; +import type AppointmentDragBehavior from './m_appointment_drag_behavior'; import { CompactAppointmentsHelper } from './m_compact_appointments_helper'; import type { SubscribeKey, SubscribeMethods } from './m_subscribes'; import subscribes from './m_subscribes'; @@ -91,6 +94,8 @@ import SchedulerAgenda from './workspaces/agenda'; import SchedulerTimelineDay from './workspaces/timeline_day'; import SchedulerTimelineMonth from './workspaces/timeline_month'; import SchedulerTimelineWeek from './workspaces/timeline_week'; +import type SchedulerWorkSpace from './workspaces/work_space'; +import type { DroppableCellData } from './workspaces/work_space'; import SchedulerWorkSpaceDay from './workspaces/work_space_day'; import SchedulerWorkSpaceMonth from './workspaces/work_space_month'; import SchedulerWorkSpaceWeek from './workspaces/work_space_week'; @@ -173,7 +178,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { private a11yStatus!: dxElementWrapper; // TODO: used externally in m_appointment_drag_behavior.ts, m_subscribes.ts, workspaces/work_space.ts - _workSpace: any; + _workSpace!: SchedulerWorkSpace; private header?: SchedulerHeader; @@ -957,7 +962,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const workspace = this.getWorkSpace(); this._layoutManager.filterAppointments(); - workspace.option('allDayExpanded', this.isAllDayExpanded()); + workspace?.option('allDayExpanded', this.isAllDayExpanded()); // @ts-expect-error const viewModel: AppointmentViewModelPlain[] = this._isVisible() @@ -968,7 +973,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.appointmentDataSource.cleanState(); if (this.isAgenda()) { - this._workSpace.renderAgendaLayout(viewModel); + // @ts-expect-error renderAgendaLayout exists only on agenda workspace + this._workSpace.renderAgendaLayout?.(viewModel); } } @@ -1321,7 +1327,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const workSpace = this._workSpace; const itTakesAllDay = this.appointmentTakesAllDay(appointmentData); - return itTakesAllDay && workSpace.supportAllDayRow() && workSpace.option('showAllDayPanel'); + return itTakesAllDay && workSpace.supportAllDayRow() && Boolean(workSpace.option('showAllDayPanel')); } private initMarkupCore(): void { @@ -1422,6 +1428,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { getResizableStep: () => (this._workSpace ? this._workSpace.positionHelper.getResizableStep() : 0), getDOMElementsMetaData: () => this._workSpace?.getDOMElementsMetaData(), getViewDataProvider: () => this._workSpace?.viewDataProvider, + // @ts-expect-error protected method isVerticalGroupedWorkSpace: () => this._workSpace.isVerticalGroupedWorkSpace(), isDateAndTimeView: () => isDateAndTimeView(this._workSpace.type), onContentReady: () => { @@ -1446,6 +1453,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.recalculateWorkspace(); if (currentViewOptions.startDate) { + // @ts-expect-error protected method this.updateOption('header', 'currentDate', this._workSpace.getHeaderDate()); } } @@ -1468,8 +1476,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._workSpace = this._createComponent($workSpace, workSpaceComponent, workSpaceConfig); if (!this.option('_newAppointments')) { - this.allowDragging() && this._workSpace.initDragBehavior(this, this.all); + if (this.allowDragging()) { + this._workSpace.initDragBehavior(this); + } } + // @ts-expect-error protected method this._workSpace.attachTablesEvents(); this._workSpace.getWorkArea().append(this._appointments.$element()); } @@ -1488,6 +1499,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.workSpaceRecalculation = new Deferred(); triggerResizeEvent(this._workSpace.$element()); this.waitAsyncTemplate(() => { + // @ts-expect-error protected method this._workSpace.renderCurrentDateTimeLineAndShader(); }); } @@ -1600,7 +1612,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - getAppointmentTemplate(optionName) { + getAppointmentTemplate(optionName: string): FunctionTemplate { if (this.currentView?.[optionName]) { return this._getTemplate(this.currentView[optionName]); } @@ -1620,7 +1632,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { private refreshWorkSpace(): void { this.cleanWorkspace(); - delete this._workSpace; + // @ts-expect-error + this._workSpace = undefined; this.renderWorkSpace(); @@ -1646,15 +1659,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.option('selectedCellData', []); } - getWorkSpaceScrollable() { + getWorkSpaceScrollable(): Scrollable { return this._workSpace.getScrollable(); } - getWorkSpaceScrollableContainer() { + getWorkSpaceScrollableContainer(): dxElementWrapper { return this._workSpace.getScrollableContainer(); } - getWorkSpace() { + getWorkSpace(): SchedulerWorkSpace | undefined { return this._workSpace; } @@ -1836,7 +1849,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.recurrenceDialog.show(); } - getUpdatedData(rawAppointment, $cell?) { + getUpdatedData(rawAppointment: Appointment, $cell?: dxElementWrapper): SafeAppointment { const viewOffset = this.getViewOffsetMs(); const getConvertedFromGrid = (date: any): Date | undefined => { @@ -1952,7 +1965,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return callback.call(this, ...args); } - getTargetCellData() { + getTargetCellData(): DroppableCellData { return this._workSpace.getDataByDroppableCell(); } @@ -2058,7 +2071,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._fireContentReadyAction(); } - getAppointmentsInstance() { + getAppointmentsInstance(): Appointments | AppointmentCollection { return this._appointments; } @@ -2177,8 +2190,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - createPopupAppointment() { - const result: any = {}; + createPopupAppointment(): SafeAppointment { + const result: SafeAppointment = {}; const toMs = dateUtils.dateToMilliseconds; const startDate = new Date(this.option('currentDate')); @@ -2416,7 +2429,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } // TODO: used externally in m_appointment_drag_behavior.ts - _getDragBehavior() { + _getDragBehavior(): AppointmentDragBehavior | null { return this._workSpace.dragBehavior; } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts index b6f56ca6b301..6e839a9a16fe 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts @@ -266,7 +266,7 @@ interface WorkspaceOptionActionMap { type WorkspaceCoordinates = Coordinates & { groupIndex?: number }; -type DroppableCellData = Pick; +export type DroppableCellData = Pick; export interface WorkspaceOptionsInternal extends WidgetProperties { newAppointments: boolean; From 13a4d2924cf8c7098c5f8494df8bfc41bdae086e Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 11:52:14 +0200 Subject: [PATCH 05/17] refactor: improve type definitions for deferred --- .../js/__internal/scheduler/m_scheduler.ts | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 014f6a75653f..049279f3712a 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -14,7 +14,8 @@ import { noop } from '@js/core/utils/common'; import { compileGetter } from '@js/core/utils/data'; import dateUtils from '@js/core/utils/date'; import dateSerialization from '@js/core/utils/date_serialization'; -// @ts-expect-error +import type { DeferredObj } from '@js/core/utils/deferred'; +// @ts-expect-error untyped deferred module re-export import { Deferred, fromPromise, when } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import { getBoundingRect } from '@js/core/utils/position'; @@ -168,6 +169,20 @@ const RECURRENCE_EDITING_MODE = { CANCEL: 'cancel', }; +type SchedulerActionCancel = boolean | PromiseLike | DeferredObj; + +type SchedulerActionOptions = { + cancel: SchedulerActionCancel; +} & Record; + +type ProcessActionCallback = ( + canceled: boolean, +) => unknown; + +interface SchedulerDragEvent { + cancel: DeferredObj; +} + class Scheduler extends SchedulerOptionsBaseWidget { // NOTE: Do not initialize variables here, because `_initMarkup` function runs before constructor, // and initialization in constructor will erase the data @@ -254,15 +269,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.postponedOperations.add('_reloadDataSource', this.reloadDataSource.bind(this), promise); } - private postponeResourceLoading(forceReload = false) { + private postponeResourceLoading(forceReload = false): DeferredObj { const whenLoaded = this.postponedOperations.add('loadResources', () => { const groups = this.getViewOption('groups'); return fromPromise(this.resourceManager.loadGroupResources(groups, forceReload)); }); - // @ts-expect-error - const resolveCallbacks = new Deferred(); + const resolveCallbacks: DeferredObj = Deferred(); whenLoaded.done(() => { resolveCallbacks.resolve(); @@ -270,7 +284,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.postponeDataSourceLoading(whenLoaded); - return resolveCallbacks.promise(); + return resolveCallbacks; } _optionChanged(args: OptionChanged): void { @@ -689,9 +703,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { dataSource.filter(filter); } - private reloadDataSource() { - // @ts-expect-error - const result = new Deferred(); + private reloadDataSource(): Promise { + const result: DeferredObj = Deferred(); if (this._dataSource) { this._dataSource.load().done(() => { @@ -718,7 +731,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return result.promise(); } - _fireContentReadyAction(result?: any): void { + _fireContentReadyAction(result?: DeferredObj): void { // @ts-expect-error const contentReadyBase = super._fireContentReadyAction.bind(this); const fireContentReady = () => { @@ -1697,7 +1710,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateAppointmentCore( appointmentData, newAppointmentData, - ).always(resolve); + ).finally(resolve); }, false, undefined, @@ -1823,7 +1836,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return dateSerialization.serializeDate(date, UTC_FULL_DATE_FORMAT); } - private showRecurrenceChangeConfirm(isDeleted) { + private showRecurrenceChangeConfirm(isDeleted: boolean): DeferredObj { const title = messageLocalization.format(isDeleted ? 'dxScheduler-confirmRecurrenceDeleteTitle' : 'dxScheduler-confirmRecurrenceEditTitle'); const message = messageLocalization.format(isDeleted ? 'dxScheduler-confirmRecurrenceDeleteMessage' : 'dxScheduler-confirmRecurrenceEditMessage'); const seriesText = messageLocalization.format(isDeleted ? 'dxScheduler-confirmRecurrenceDeleteSeries' : 'dxScheduler-confirmRecurrenceEditSeries'); @@ -1969,7 +1982,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace.getDataByDroppableCell(); } - updateAppointmentCore(target, rawAppointment, onUpdatePrevented?: any, dragEvent?: any) { + updateAppointmentCore( + target: SafeAppointment, + rawAppointment: SafeAppointment, + onUpdatePrevented?: () => void, + dragEvent?: SchedulerDragEvent, + ): Promise { const updatingOptions = { newData: rawAppointment, oldData: extend({}, target), @@ -1989,8 +2007,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.actions[StoreEventNames.UPDATING](updatingOptions); if (dragEvent && !isDeferred(dragEvent.cancel)) { - // @ts-expect-error - dragEvent.cancel = new Deferred(); + dragEvent.cancel = Deferred(); } if (isPromise(updatingOptions.cancel) && (dragEvent || this.option('_newAppointments'))) { @@ -1998,8 +2015,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } return this.processActionResult(updatingOptions, function (canceled) { - // @ts-expect-error - let deferred = new Deferred(); + let deferred: DeferredObj = Deferred(); if (!canceled) { this.expandAllDayPanel(rawAppointment); @@ -2030,23 +2046,25 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - private processActionResult(actionOptions, callback) { - // @ts-expect-error - const deferred = new Deferred(); - const resolveCallback = (callbackResult) => { + private processActionResult( + actionOptions: SchedulerActionOptions, + callback: ProcessActionCallback, + ): Promise { + const deferred: DeferredObj = Deferred(); + const resolveCallback = (callbackResult: unknown): void => { when(fromPromise(callbackResult)) - .always(deferred.resolve); + .always(() => { deferred.resolve(); }); }; if (isPromise(actionOptions.cancel)) { when(fromPromise(actionOptions.cancel)).always((cancel) => { if (!isDefined(cancel)) { - cancel = actionOptions.cancel.state() === 'rejected'; + cancel = (actionOptions.cancel as DeferredObj).state() === 'rejected'; } resolveCallback(callback.call(this, cancel)); }); } else { - resolveCallback(callback.call(this, actionOptions.cancel)); + resolveCallback(callback.call(this, actionOptions.cancel as boolean)); } return deferred.promise(); @@ -2254,7 +2272,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (this.appointmentTooltip.isShownForTarget(target)) { this.hideAppointmentTooltip(); } else { - this.processActionResult(arg, (canceled) => { + this.processActionResult({ ...arg, cancel: arg.cancel ?? false }, (canceled) => { !canceled && this.appointmentTooltip.show(target, data, { ...this.getExtraAppointmentTooltipOptions(), ...options, @@ -2306,7 +2324,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { && (orientation === 'horizontal' || orientation === 'both'); } - addAppointment(rawAppointment) { + addAppointment(rawAppointment: SafeAppointment): Promise { // NOTE: mutation of raw appointment const appointment = new AppointmentAdapter( rawAppointment, @@ -2325,8 +2343,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.processActionResult(addingOptions, (canceled) => { if (canceled) { - // @ts-expect-error - return new Deferred().resolve(); + return Deferred().resolve().promise(); } this.expandAllDayPanel(serializedAppointment); @@ -2337,7 +2354,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - updateAppointment(target, appointment) { + updateAppointment(target: SafeAppointment, appointment: SafeAppointment): Promise { return this.updateAppointmentCore(target, appointment); } From 8b179639b975d7280d1e98c0af478f1fd799088c Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 11:55:48 +0200 Subject: [PATCH 06/17] refactor: add type for actions --- .../js/__internal/scheduler/m_scheduler.ts | 87 +++++++++++++++---- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 049279f3712a..b175b983bcd2 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -32,7 +32,18 @@ import type { DataSourceOptions } from '@js/data/data_source'; import DataHelperMixin from '@js/data_helper'; import { custom as customDialog } from '@js/ui/dialog'; import type { - Appointment, AppointmentTooltipShowingEvent, DayOfWeek, Occurrence, + Appointment, + AppointmentAddingEvent, + AppointmentClickEvent, + AppointmentContextMenuEvent, + AppointmentDblClickEvent, + AppointmentFormOpeningEvent, + AppointmentRenderedEvent, + AppointmentTooltipShowingEvent, + AppointmentUpdatingEvent, + DayOfWeek, + Occurrence, + SelectionEndEvent, } from '@js/ui/scheduler'; import errors from '@js/ui/widget/ui.errors'; import { dateUtilsTs } from '@ts/core/utils/date'; @@ -183,6 +194,40 @@ interface SchedulerDragEvent { cancel: DeferredObj; } +type SchedulerEventArgs = Omit; + +type AppointmentAddingOptions = Pick; + +type AppointmentUpdatingOptions = Pick; + +interface AppointmentCompletedOptions { + appointmentData: SafeAppointment; + error?: Error; +} + +interface AppointmentDeletingOptions { + [key: string]: unknown; + appointmentData: SafeAppointment; + targetedAppointmentData?: SafeAppointment | AppointmentAdapter; + cancel: SchedulerActionCancel; +} + +interface SchedulerActions { + onAppointmentAdding: (args: AppointmentAddingOptions) => void; + onAppointmentAdded: (args: AppointmentCompletedOptions) => void; + onAppointmentUpdating: (args: AppointmentUpdatingOptions) => void; + onAppointmentUpdated: (args: AppointmentCompletedOptions) => void; + onAppointmentDeleting: (args: AppointmentDeletingOptions) => void; + onAppointmentDeleted: (args: AppointmentCompletedOptions) => void; + onAppointmentFormOpening: (args: SchedulerEventArgs) => void; + onAppointmentTooltipShowing: (args: SchedulerEventArgs) => void; + onSelectionEnd: (args: SchedulerEventArgs) => void; + onAppointmentRendered: (args: AppointmentRenderedEvent) => void; + onAppointmentClick: (args: AppointmentClickEvent) => void; + onAppointmentDblClick: (args: AppointmentDblClickEvent) => void; + onAppointmentContextMenu: (args: AppointmentContextMenuEvent) => void; +} + class Scheduler extends SchedulerOptionsBaseWidget { // NOTE: Do not initialize variables here, because `_initMarkup` function runs before constructor, // and initialization in constructor will erase the data @@ -211,9 +256,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { resourceManager!: ResourceManager; - private actions: any; + private actions!: SchedulerActions; - _createActionByOption: any; + // TODO: used externally in workspaces/work_space.ts + declare _createActionByOption: ( + optionName: string, + config?: { excludeValidators?: string[] }, + ) => (event?: unknown) => void; private appointmentTooltip!: MobileTooltipStrategy | DesktopTooltipStrategy; @@ -446,7 +495,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { case 'onAppointmentFormOpening': case 'onAppointmentTooltipShowing': case 'onSelectionEnd': - this.actions[name] = this._createActionByOption(name); + (this.actions as unknown as Record void>)[name] = this._createActionByOption(name); break; case 'onAppointmentRendered': if (this.option('_newAppointments')) { @@ -1063,14 +1112,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { onAppointmentClick: this._createActionByOption('onAppointmentClick'), onAppointmentDblClick: this._createActionByOption('onAppointmentDblClick'), onAppointmentContextMenu: this._createActionByOption('onAppointmentContextMenu'), - }; + } as SchedulerActions; } // TODO: delete this method when old impl is removed - private getAppointmentRenderedAction() { + private getAppointmentRenderedAction(): (args: AppointmentRenderedEvent) => void { return this._createActionByOption('onAppointmentRendered', { excludeValidators: ['disabled', 'readOnly'], - }); + }) as (args: AppointmentRenderedEvent) => void; } _renderFocusTarget(): void { return noop(); } @@ -1130,7 +1179,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { appointmentCollectorTemplate: this.getViewOption('appointmentCollectorTemplate'), onAppointmentRendered: (...args) => this.actions.onAppointmentRendered(...args), - onAppointmentClick: (...args) => this.actions.onAppointmentClick(...args), + onAppointmentClick: (args: AppointmentClickEvent) => this.actions.onAppointmentClick(args), onAppointmentDblClick: (...args) => this.actions.onAppointmentDblClick(...args), onAppointmentContextMenu: (...args) => this.actions.onAppointmentContextMenu(...args), onDeleteKeyPress: (e) => { @@ -1235,7 +1284,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { updateAppointment: (sourceAppointment, updatedAppointment) => this.updateAppointment(sourceAppointment, updatedAppointment), }; - return new AppointmentPopup(scheduler, form); + return new AppointmentPopup(scheduler as never, form); } private scrollToAppointment(appointment: Record): void { @@ -1278,7 +1327,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { createEventArgs: that._createEventArgs.bind(that), newAppointments: Boolean(this.option('_newAppointments')), - onAppointmentClick: (...args) => this.actions.onAppointmentClick(...args), + onAppointmentClick: (args: AppointmentClickEvent) => this.actions.onAppointmentClick(args), onListInitialized: (e) => { if (this.option('_newAppointments')) { this.appointmentDragController.createTooltipDraggable( @@ -1561,8 +1610,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { }, onSelectionEnd: (args) => { this.actions.onSelectionEnd({ - component: this, - element: this.$element(), selectedCellData: args.selectedCellData, }); }, @@ -2097,7 +2144,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._layoutManager; } - getActions() { + getActions(): SchedulerActions { return this.actions; } @@ -2363,8 +2410,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.processDeleteAppointment(rawAppointment, deletingOptions); } - fireOnAppointmentDeleting(rawAppointment, targetedAppointmentData?: any) { - const deletingOptions = { + fireOnAppointmentDeleting( + rawAppointment: SafeAppointment, + targetedAppointmentData?: SafeAppointment | AppointmentAdapter, + ): AppointmentDeletingOptions { + const deletingOptions: AppointmentDeletingOptions = { appointmentData: rawAppointment, targetedAppointmentData, cancel: false, @@ -2375,7 +2425,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { return deletingOptions; } - processDeleteAppointment(rawAppointment, deletingOptions): void { + processDeleteAppointment( + rawAppointment: SafeAppointment, + deletingOptions: AppointmentDeletingOptions, + ): void { this.processActionResult(deletingOptions, function (canceled) { if (!canceled) { this.appointmentDataSource @@ -2404,7 +2457,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { () => { this.processDeleteAppointment( appointment, - { cancel: false }, + { appointmentData: appointment, cancel: false }, ); }, true, From 5dba430c307994af1e761f457429c44049fd9641 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 12:08:04 +0200 Subject: [PATCH 07/17] refactor: type appointment collection options --- .../appointment_collection_options.ts | 51 ++++++++++++++++ .../js/__internal/scheduler/m_scheduler.ts | 61 ++++++++++++------- .../tooltip_strategy_base.ts | 11 +++- .../js/__internal/scheduler/types.ts | 12 ++++ 4 files changed, 110 insertions(+), 25 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/appointments/appointment_collection_options.ts diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment_collection_options.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment_collection_options.ts new file mode 100644 index 000000000000..38e76729e96e --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment_collection_options.ts @@ -0,0 +1,51 @@ +import type { + AppointmentClickEvent, + AppointmentContextMenuEvent, + AppointmentDblClickEvent, + AppointmentRenderedEvent, +} from '@js/ui/scheduler'; + +import type NotifyScheduler from '../base/widget_notify_scheduler'; +import type { TimeZoneCalculator } from '../r1/timezone_calculator/index'; +import type { DesktopTooltipStrategy } from '../tooltip_strategies/desktop_tooltip_strategy'; +import type { MobileTooltipStrategy } from '../tooltip_strategies/mobile_tooltip_strategy'; +import type { DOMMetaData, ScrollToGroupValuesOrOptions } from '../types'; +import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; +import type { ResourceManager } from '../utils/resource_manager/resource_manager'; +import type { AppointmentDataSource } from '../view_model/m_appointment_data_source'; +import type { SortedEntity } from '../view_model/types'; +import type ViewDataProvider from '../workspaces/view_model/view_data_provider'; + +export interface AppointmentCollectionOptions { + getResourceManager: () => ResourceManager; + getAppointmentDataSource: () => AppointmentDataSource; + getSortedAppointments: () => SortedEntity[]; + scrollTo: ( + date: Date, + groupValuesOrOptions?: ScrollToGroupValuesOrOptions, + allDay?: boolean, + ) => void; + appointmentTooltip: MobileTooltipStrategy | DesktopTooltipStrategy; + dataAccessors: AppointmentDataAccessor; + notifyScheduler: NotifyScheduler; + onItemRendered: (args: AppointmentRenderedEvent) => void; + onItemClick: (args: AppointmentClickEvent) => void; + onItemContextMenu: (args: AppointmentContextMenuEvent) => void; + onAppointmentDblClick: (args: AppointmentDblClickEvent) => void; + tabIndex: number; + focusStateEnabled: boolean; + allowDrag: boolean; + allowDelete: boolean; + allowResize: boolean; + allowAllDayResize: boolean; + rtlEnabled: boolean; + groups: string[]; + groupByDate: boolean; + timeZoneCalculator: TimeZoneCalculator; + getResizableStep: () => number; + getDOMElementsMetaData: () => DOMMetaData | undefined; + getViewDataProvider: () => ViewDataProvider | undefined; + isVerticalGroupedWorkSpace: () => boolean; + isDateAndTimeView: () => boolean; + onContentReady: () => void; +} diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index b175b983bcd2..6a500350679e 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -31,6 +31,7 @@ import { hasWindow } from '@js/core/utils/window'; import type { DataSourceOptions } from '@js/data/data_source'; import DataHelperMixin from '@js/data_helper'; import { custom as customDialog } from '@js/ui/dialog'; +import type { ItemContextMenuEvent } from '@js/ui/list'; import type { Appointment, AppointmentAddingEvent, @@ -56,6 +57,7 @@ import { AppointmentDragController } from './appointment_drag_controller'; import type { AppointmentFormConfig } from './appointment_popup/form'; import { AppointmentForm } from './appointment_popup/form'; import { AppointmentPopup } from './appointment_popup/popup'; +import type { AppointmentCollectionOptions } from './appointments/appointment_collection_options'; import AppointmentCollection from './appointments/m_appointment_collection'; import type { AppointmentsProperties } from './appointments_new/appointments'; import { Appointments } from './appointments_new/appointments'; @@ -80,9 +82,11 @@ import { validateRRule } from './recurrence/validate_rule'; import { SchedulerOptionsBaseWidget } from './scheduler_options_base_widget'; import { DesktopTooltipStrategy } from './tooltip_strategies/desktop_tooltip_strategy'; import { MobileTooltipStrategy } from './tooltip_strategies/mobile_tooltip_strategy'; -import type { AppointmentTooltipExtraOptions } from './tooltip_strategies/tooltip_strategy_base'; +import type { AppointmentTooltipExtraOptions, AppointmentTooltipOptions } from './tooltip_strategies/tooltip_strategy_base'; import type { + AppointmentTooltipContextMenuEventArgs, AppointmentTooltipItem, + MappedAppointmentFields, SafeAppointment, ScrollToGroupValuesOrOptions, ScrollToOptions, TargetedAppointment, ViewType, @@ -1308,10 +1312,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._workSpace.updateScrollPosition(startDate, appointmentGroupValues, inAllDayRow); } - private getAppointmentTooltipOptions() { + private getAppointmentTooltipOptions(): AppointmentTooltipOptions { const that = this; return { - // @ts-expect-error + // @ts-expect-error _createComponent is not defined in ts createComponent: that._createComponent.bind(that), container: that.$element(), getScrollableContainer: that.getWorkSpaceScrollableContainer.bind(that), @@ -1321,7 +1325,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { checkAndDeleteAppointment: that.checkAndDeleteAppointment.bind(that), isAppointmentInAllDayPanel: that.isAppointmentInAllDayPanel.bind(that), - createFormattedDateText: (appointment, targetedAppointment, format) => this.fire('createFormattedDateText', appointment, targetedAppointment, format), + createFormattedDateText: (appointment, targetedAppointment, format) => this.fire( + 'createFormattedDateText', + appointment, + (targetedAppointment ?? appointment) as TargetedAppointment, + format, + ), getAppointmentDisabled: (appointment) => this._dataAccessors.get('disabled', appointment), onItemContextMenu: that._createActionByOption('onAppointmentContextMenu'), createEventArgs: that._createEventArgs.bind(that), @@ -1329,9 +1338,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { newAppointments: Boolean(this.option('_newAppointments')), onAppointmentClick: (args: AppointmentClickEvent) => this.actions.onAppointmentClick(args), onListInitialized: (e) => { - if (this.option('_newAppointments')) { + if (this.option('_newAppointments') && e.element) { this.appointmentDragController.createTooltipDraggable( - e.element, + $(e.element), { dragTemplate: this._appointments.renderDragClone.bind(this._appointments), }, @@ -1341,17 +1350,25 @@ class Scheduler extends SchedulerOptionsBaseWidget { onListDisposing: () => { this.appointmentDragController.disposeTooltipDraggable(); }, - }; + } satisfies AppointmentTooltipOptions; } // TODO: delete this method when old impl is removed - _createEventArgs(e) { - const config = { - itemData: e.itemData.appointment, - itemElement: e.itemElement, - targetedAppointment: e.itemData.targetedAppointment, + _createEventArgs( + e: ItemContextMenuEvent, + ): AppointmentTooltipContextMenuEventArgs { + const config: { + itemData: SafeAppointment; + itemElement: dxElementWrapper; + targetedAppointment?: SafeAppointment | TargetedAppointment; + } = { + itemData: e.itemData!.appointment, + itemElement: $(e.itemElement), + targetedAppointment: e.itemData!.targetedAppointment, }; - return extend({}, (this.fire as any)('mapAppointmentFields', config), { + const mappedFields = this.fire('mapAppointmentFields', config) as MappedAppointmentFields; + + return extend({}, mappedFields, { component: e.component, element: e.element, event: e.event, @@ -1464,8 +1481,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { }; } - private appointmentsConfig() { - const config = { + private appointmentsConfig(): AppointmentCollectionOptions { + const config: AppointmentCollectionOptions = { getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, getSortedAppointments: () => this._layoutManager.sortedItems, @@ -1474,18 +1491,18 @@ class Scheduler extends SchedulerOptionsBaseWidget { dataAccessors: this._dataAccessors, notifyScheduler: this.notifyScheduler, onItemRendered: this.getAppointmentRenderedAction(), - onItemClick: this._createActionByOption('onAppointmentClick'), - onItemContextMenu: this._createActionByOption('onAppointmentContextMenu'), - onAppointmentDblClick: this._createActionByOption('onAppointmentDblClick'), - tabIndex: this.option('tabIndex'), - focusStateEnabled: this.option('focusStateEnabled'), + onItemClick: this._createActionByOption('onAppointmentClick') as (args: AppointmentClickEvent) => void, + onItemContextMenu: this._createActionByOption('onAppointmentContextMenu') as (args: AppointmentContextMenuEvent) => void, + onAppointmentDblClick: this._createActionByOption('onAppointmentDblClick') as (args: AppointmentDblClickEvent) => void, + tabIndex: this.option('tabIndex') ?? 0, + focusStateEnabled: Boolean(this.option('focusStateEnabled')), allowDrag: this.allowDragging(), allowDelete: this.editing.allowUpdating && this.editing.allowDeleting, allowResize: this.allowResizing(), allowAllDayResize: this.allowAllDayResizing(), - rtlEnabled: this.option('rtlEnabled'), + rtlEnabled: Boolean(this.option('rtlEnabled')), groups: this.getViewOption('groups'), - groupByDate: this.getViewOption('groupByDate'), + groupByDate: Boolean(this.getViewOption('groupByDate')), timeZoneCalculator: this.timeZoneCalculator, getResizableStep: () => (this._workSpace ? this._workSpace.positionHelper.getResizableStep() : 0), getDOMElementsMetaData: () => this._workSpace?.getDOMElementsMetaData(), diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts index 0592e3c43d9c..f34d44d08455 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts @@ -19,7 +19,11 @@ import { createPromise } from '@ts/core/utils/promise'; import List from '@ts/ui/list/list.edit'; import type Tooltip from '@ts/ui/tooltip'; -import type { AppointmentTooltipItem, TargetedAppointment } from '../types'; +import type { + AppointmentTooltipContextMenuEventArgs, + AppointmentTooltipItem, + TargetedAppointment, +} from '../types'; const TOOLTIP_APPOINTMENT_ITEM = 'dx-tooltip-appointment-item'; const TOOLTIP_APPOINTMENT_ITEM_CONTENT = `${TOOLTIP_APPOINTMENT_ITEM}-content`; @@ -33,7 +37,7 @@ const TOOLTIP_APPOINTMENT_ITEM_DELETE_BUTTON = `${TOOLTIP_APPOINTMENT_ITEM}-dele const APPOINTMENT_TOOLTIP_TEMPLATE = 'appointmentTooltipTemplate'; -interface AppointmentTooltipOptions { +export interface AppointmentTooltipOptions { createComponent: ( element: dxElementWrapper, component: unknown, @@ -63,7 +67,8 @@ interface AppointmentTooltipOptions { }; getAppointmentDisabled: (appointment: Appointment) => boolean | undefined; onItemContextMenu: (eventArgs: unknown) => void; - createEventArgs: (e: ItemContextMenuEvent) => unknown; + createEventArgs: (e: ItemContextMenuEvent) => + AppointmentTooltipContextMenuEventArgs; newAppointments?: boolean; // TODO onAppointmentClick: (e: AppointmentClickEvent) => void; onListInitialized: (e: InitializedEvent) => void; diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts index 7ddba344ba65..11d0bcbf465b 100644 --- a/packages/devextreme/js/__internal/scheduler/types.ts +++ b/packages/devextreme/js/__internal/scheduler/types.ts @@ -1,4 +1,5 @@ import type { dxElementWrapper } from '@js/core/renderer'; +import type { ItemContextMenuEvent } from '@js/ui/list'; import type { Appointment, Properties } from '@js/ui/scheduler'; import type { Component } from '@ts/core/widget/component'; @@ -298,6 +299,17 @@ export interface AppointmentTooltipItem { settings: AppointmentItemViewModel; } +export interface MappedAppointmentFields { + appointmentData: SafeAppointment; + appointmentElement: dxElementWrapper; + targetedAppointmentData: SafeAppointment | TargetedAppointment; +} + +export type AppointmentTooltipContextMenuEventArgs = MappedAppointmentFields & Pick< + ItemContextMenuEvent, + 'component' | 'element' | 'event' | 'model' +>; + export interface CompactAppointmentOptions { $container: dxElementWrapper; coordinates: { top: number; left: number }; From 744c1bba0466a51d44e9374ce8262d56af9467d0 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 12:11:41 +0200 Subject: [PATCH 08/17] refactor: type workSpace config --- .../js/__internal/scheduler/m_scheduler.ts | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 6a500350679e..53ac5add63c2 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -9,6 +9,7 @@ import $ from '@js/core/renderer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; import { EmptyTemplate } from '@js/core/templates/empty_template'; import type { FunctionTemplate } from '@js/core/templates/function_template'; +import type { TemplateBase } from '@js/core/templates/template_base'; import Callbacks from '@js/core/utils/callbacks'; import { noop } from '@js/core/utils/common'; import { compileGetter } from '@js/core/utils/data'; @@ -42,6 +43,8 @@ import type { AppointmentRenderedEvent, AppointmentTooltipShowingEvent, AppointmentUpdatingEvent, + CellClickEvent, + CellContextMenuEvent, DayOfWeek, Occurrence, SelectionEndEvent, @@ -89,6 +92,7 @@ import type { MappedAppointmentFields, SafeAppointment, ScrollToGroupValuesOrOptions, ScrollToOptions, TargetedAppointment, + ViewCellData, ViewType, } from './types'; import { utils } from './utils'; @@ -111,7 +115,7 @@ import SchedulerTimelineDay from './workspaces/timeline_day'; import SchedulerTimelineMonth from './workspaces/timeline_month'; import SchedulerTimelineWeek from './workspaces/timeline_week'; import type SchedulerWorkSpace from './workspaces/work_space'; -import type { DroppableCellData } from './workspaces/work_space'; +import type { DroppableCellData, WorkspaceOptionsInternal } from './workspaces/work_space'; import SchedulerWorkSpaceDay from './workspaces/work_space_day'; import SchedulerWorkSpaceMonth from './workspaces/work_space_month'; import SchedulerWorkSpaceWeek from './workspaces/work_space_week'; @@ -1583,7 +1587,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - private workSpaceConfig(currentViewOptions: NormalizedView) { + private workSpaceConfig(currentViewOptions: NormalizedView): WorkspaceOptionsInternal { + const cellDuration = this.option('cellDuration'); const scrolling = this.getViewOption('scrolling'); const isVirtualScrolling = scrolling.mode === 'virtual'; const horizontalVirtualScrollingAllowed = isVirtualScrolling @@ -1595,7 +1600,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { || horizontalVirtualScrollingAllowed || isTimelineView(currentViewOptions.type); - const result = extend({ + const workSpaceOptions = extend({ newAppointments: Boolean(this.option('_newAppointments')), resources: this.option('resources'), getResourceManager: () => this.resourceManager, @@ -1606,13 +1611,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { startDayHour: this.option('startDayHour'), endDayHour: this.option('endDayHour'), viewOffset: this.getViewOffsetMs(), - tabIndex: this.option('tabIndex'), - accessKey: this.option('accessKey'), - focusStateEnabled: this.option('focusStateEnabled'), - cellDuration: this.option('cellDuration'), + tabIndex: this.option('tabIndex') ?? 0, + accessKey: this.option('accessKey') ?? '', + focusStateEnabled: Boolean(this.option('focusStateEnabled')), showAllDayPanel: this.option('showAllDayPanel'), showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'), - indicatorTime: this.option('indicatorTime'), + indicatorTime: this.option('indicatorTime') ?? new Date(), indicatorUpdateInterval: this.option('indicatorUpdateInterval'), shadeUntilCurrentTime: this.option('shadeUntilCurrentTime'), crossScrollingEnabled, @@ -1622,15 +1626,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { dateCellTemplate: this.option('dateCellTemplate'), allowMultipleCellSelection: this.option('allowMultipleCellSelection'), selectedCellData: this.option('selectedCellData'), - onSelectionChanged: (args) => { + onSelectionChanged: (args: { selectedCellData: ViewCellData[] }) => { this.option('selectedCellData', args.selectedCellData); }, - onSelectionEnd: (args) => { + onSelectionEnd: (args: { selectedCellData: ViewCellData[] }) => { this.actions.onSelectionEnd({ selectedCellData: args.selectedCellData, }); }, - groupByDate: this.getViewOption('groupByDate'), + groupByDate: Boolean(this.getViewOption('groupByDate')), skippedDays: this.getViewOption('hiddenWeekDays') as number[], scrolling, draggingMode: this.option('_draggingMode'), @@ -1639,42 +1643,50 @@ class Scheduler extends SchedulerOptionsBaseWidget { schedulerWidth: this.option('width'), allDayPanelMode: this.option('allDayPanelMode'), onSelectedCellsClick: this.showAddAppointmentPopup.bind(this), - renderAppointments: () => { this.renderAppointments(); }, - onShowAllDayPanel: (value) => this.option('showAllDayPanel', value), - getHeaderHeight: () => utils.DOM.getHeaderHeight(this.header), - onScrollEnd: () => this._appointments.updateResizableArea(), + renderAppointments: (): void => { this.renderAppointments(); }, + onShowAllDayPanel: (value: boolean) => this.option('showAllDayPanel', value), + getHeaderHeight: (): number => utils.DOM.getHeaderHeight(this.header), + onScrollEnd: (): void => this._appointments.updateResizableArea(), onInitialized: (e) => { if (this.option('_newAppointments')) { this.appointmentDragController.createWorkSpaceDraggable( - e.element, + $(e.element), { getAppointmentData: this._appointments.getAppointmentData.bind(this._appointments), }, ); } }, - onDisposing: () => { + onDisposing: (): void => { if (this.option('_newAppointments')) { this.appointmentDragController.disposeWorkSpaceDraggable(); } }, - - }, currentViewOptions); - - result.notifyScheduler = this.notifyScheduler; - result.groups = this.resourceManager.groupResources(); - result.onCellClick = this._createActionByOption('onCellClick'); - result.onCellContextMenu = this._createActionByOption('onCellContextMenu'); - result.currentDate = this.getViewOption('currentDate'); - result.skippedDays = this.getViewOption('hiddenWeekDays') as number[]; - result.hoursInterval = result.cellDuration / 60; - result.allDayExpanded = false; - result.dataCellTemplate = result.dataCellTemplate ? this._getTemplate(result.dataCellTemplate) : null; - result.timeCellTemplate = result.timeCellTemplate ? this._getTemplate(result.timeCellTemplate) : null; - result.resourceCellTemplate = result.resourceCellTemplate ? this._getTemplate(result.resourceCellTemplate) : null; - result.dateCellTemplate = result.dateCellTemplate ? this._getTemplate(result.dateCellTemplate) : null; - - return result; + rtlEnabled: Boolean(this.option('rtlEnabled')), + hoursInterval: cellDuration / 60, + allDayExpanded: false, + currentDate: this.getViewOption('currentDate'), + }, currentViewOptions) as WorkspaceOptionsInternal; + + workSpaceOptions.notifyScheduler = this.notifyScheduler; + workSpaceOptions.groups = this.resourceManager.groupResources(); + workSpaceOptions.onCellClick = this._createActionByOption('onCellClick') as (e: CellClickEvent) => void; + workSpaceOptions.onCellContextMenu = this._createActionByOption('onCellContextMenu') as (e: CellContextMenuEvent) => void; + workSpaceOptions.skippedDays = this.getViewOption('hiddenWeekDays') as number[]; + workSpaceOptions.dataCellTemplate = workSpaceOptions.dataCellTemplate + ? this._getTemplate(workSpaceOptions.dataCellTemplate) as TemplateBase + : null; + workSpaceOptions.timeCellTemplate = workSpaceOptions.timeCellTemplate + ? this._getTemplate(workSpaceOptions.timeCellTemplate) as TemplateBase + : null; + workSpaceOptions.resourceCellTemplate = workSpaceOptions.resourceCellTemplate + ? this._getTemplate(workSpaceOptions.resourceCellTemplate) as TemplateBase + : null; + workSpaceOptions.dateCellTemplate = workSpaceOptions.dateCellTemplate + ? this._getTemplate(workSpaceOptions.dateCellTemplate) as TemplateBase + : null; + + return workSpaceOptions; } private waitAsyncTemplate(callback): void { From 232fa0b4a21cf0186bd6fbe4bafae37593dddfba Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 12:16:54 +0200 Subject: [PATCH 09/17] refactor: type arguments in methods --- .../js/__internal/scheduler/m_scheduler.ts | 179 ++++++++++++------ 1 file changed, 118 insertions(+), 61 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 53ac5add63c2..4cadf21c260e 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -47,6 +47,7 @@ import type { CellContextMenuEvent, DayOfWeek, Occurrence, + RecurrenceEditMode, SelectionEndEvent, } from '@js/ui/scheduler'; import errors from '@js/ui/widget/ui.errors'; @@ -106,6 +107,7 @@ import { VIEWS } from './utils/options/constants_view'; import type { NormalizedView, SafeSchedulerOptions } from './utils/options/types'; import { getAppointmentGroupValues, setAppointmentGroupValues } from './utils/resource_manager/appointment_groups_utils'; import { ResourceManager } from './utils/resource_manager/resource_manager'; +import type { GroupValues } from './utils/resource_manager/types'; import timeZoneUtils, { type TimezoneLabel } from './utils_time_zone'; import AppointmentLayoutManager from './view_model/appointments_layout_manager'; import { AppointmentDataSource } from './view_model/m_appointment_data_source'; @@ -180,7 +182,9 @@ const StoreEventNames = { UPDATING: 'onAppointmentUpdating', UPDATED: 'onAppointmentUpdated', -}; +} as const; + +type StoreEventName = typeof StoreEventNames[keyof typeof StoreEventNames]; const RECURRENCE_EDITING_MODE = { SERIES: 'editSeries', @@ -236,6 +240,22 @@ interface SchedulerActions { onAppointmentContextMenu: (args: AppointmentContextMenuEvent) => void; } +interface SelectedCellsClickCellData { + startDate?: Date; + endDate?: Date; + startDateUTC: number; + endDateUTC: number; + allDay?: boolean; +} + +interface SchedulerEditing { + allowAdding: boolean; + allowUpdating: boolean; + allowDeleting: boolean; + allowResizing: boolean; + allowDragging: boolean; +} + class Scheduler extends SchedulerOptionsBaseWidget { // NOTE: Do not initialize variables here, because `_initMarkup` function runs before constructor, // and initialization in constructor will erase the data @@ -322,7 +342,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.timeZoneCalculatorInstance; } - private postponeDataSourceLoading(promise?: any): void { + private postponeDataSourceLoading(promise?: DeferredObj): void { this.postponedOperations.add('_reloadDataSource', this.reloadDataSource.bind(this), promise); } @@ -690,7 +710,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private bringEditingModeToAppointments(editing): void { + private bringEditingModeToAppointments(editing: SchedulerEditing): void { const editingConfig: any = { allowDelete: editing.allowUpdating && editing.allowDeleting, }; @@ -805,7 +825,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - _dimensionChanged(value, isForce = false): void { + _dimensionChanged(value: unknown, isForce = false): void { const isFixedHeight = typeof this.option('height') === 'number'; const isFixedWidth = typeof this.option('width') === 'number'; @@ -855,7 +875,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { (this.$element() as any).toggleClass(WIDGET_ADAPTIVE_CLASS, this.option('adaptivityEnabled')); } - _visibilityChanged(visible): void { + _visibilityChanged(visible: boolean): void { visible && this._dimensionChanged(null, true); } @@ -962,9 +982,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { // TODO: delete this method when old impl is removed private initAppointmentTemplate(): void { const { expr } = this._dataAccessors; - const createGetter = (property) => compileGetter(`appointmentData.${property}`); + const createGetter = (property: string): ((data: unknown) => unknown) => ( + compileGetter(`appointmentData.${property}`) as (data: unknown) => unknown + ); - const getDate = (getter) => (data) => { + const getDate = (getter: (data: unknown) => unknown) => (data: unknown) => { const value = getter(data); if (value instanceof Date) { return value.valueOf(); @@ -1380,15 +1402,19 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - checkAndDeleteAppointment(appointment, targetedAppointment): void { + checkAndDeleteAppointment( + appointment: SafeAppointment, + targetedAppointment?: SafeAppointment | TargetedAppointment, + ): void { + const targetedData = targetedAppointment ?? appointment; const targetedAdapter = new AppointmentAdapter( - targetedAppointment, + targetedData, this._dataAccessors, ); const deletingOptions = this.fireOnAppointmentDeleting(appointment, targetedAdapter); this.checkRecurringAppointment( appointment, - targetedAppointment, + targetedData, targetedAdapter.startDate, () => { this.processDeleteAppointment(appointment, deletingOptions); @@ -1406,7 +1432,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }; } - isAppointmentInAllDayPanel(appointmentData): boolean { + isAppointmentInAllDayPanel(appointmentData: SafeAppointment | Appointment): boolean { const workSpace = this._workSpace; const itTakesAllDay = this.appointmentTakesAllDay(appointmentData); @@ -1689,7 +1715,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return workSpaceOptions; } - private waitAsyncTemplate(callback): void { + private waitAsyncTemplate(callback: () => void): void { if (this._options.silent('templatesRenderAsynchronously')) { const timer = setTimeout(() => { callback(); @@ -1798,14 +1824,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { } checkRecurringAppointment( - rawAppointment, - singleAppointment, - exceptionDate, - callback, - isDeleted, - isPopupEditing?: any, - dragEvent?: any, - recurrenceEditMode?: any, + rawAppointment: SafeAppointment, + singleAppointment: SafeAppointment, + exceptionDate: Date, + callback: () => void, + isDeleted: boolean, + isPopupEditing?: boolean, + dragEvent?: SchedulerDragEvent | null, + recurrenceEditMode?: RecurrenceEditMode, onCancel?: () => void, ): void { const recurrenceRule = this._dataAccessors.get('recurrenceRule', rawAppointment); @@ -1821,7 +1847,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { callback(); break; case 'occurrence': - this.excludeAppointmentFromSeries(rawAppointment, singleAppointment, exceptionDate, isDeleted, isPopupEditing, dragEvent); + this.excludeAppointmentFromSeries( + rawAppointment, + singleAppointment, + exceptionDate, + isDeleted, + Boolean(isPopupEditing), + dragEvent, + ); break; default: if (dragEvent) { @@ -1837,7 +1870,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { singleAppointment, exceptionDate, isDeleted, - isPopupEditing, + Boolean(isPopupEditing), dragEvent, ); }) @@ -1851,7 +1884,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private excludeAppointmentFromSeries(rawAppointment, newRawAppointment, exceptionDate, isDeleted, isPopupEditing, dragEvent): void { + private excludeAppointmentFromSeries( + rawAppointment: SafeAppointment, + newRawAppointment: SafeAppointment, + exceptionDate: Date, + isDeleted: boolean, + isPopupEditing: boolean, + dragEvent?: SchedulerDragEvent | null, + ): void { const appointment = excludeFromRecurrence( rawAppointment, exceptionDate, @@ -1890,18 +1930,22 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - private createRecurrenceException(appointment, exceptionDate): string { + private createRecurrenceException( + appointment: SafeAppointment & { recurrenceException?: string }, + exceptionDate: Date, + ): string { const result: any[] = []; if (appointment.recurrenceException) { result.push(appointment.recurrenceException); } - result.push(this.getSerializedDate(exceptionDate, appointment.startDate, appointment.allDay)); + const adapter = new AppointmentAdapter(appointment, this._dataAccessors); + result.push(this.getSerializedDate(exceptionDate, adapter.startDate, Boolean(adapter.allDay))); return result.join(); } - private getSerializedDate(date, startDate, isAllDay): string { + private getSerializedDate(date: Date, startDate: Date, isAllDay: boolean): string { isAllDay && date.setHours( startDate.getHours(), startDate.getMinutes(), @@ -1941,7 +1985,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { getUpdatedData(rawAppointment: Appointment, $cell?: dxElementWrapper): SafeAppointment { const viewOffset = this.getViewOffsetMs(); - const getConvertedFromGrid = (date: any): Date | undefined => { + const getConvertedFromGrid = (date: Date | undefined): Date | undefined => { if (!date) { return undefined; } @@ -2037,7 +2081,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - subscribe(subject, action): void { + subscribe(subject: K, action: SubscribeMethods[K]): void { this.subscribes[subject] = subscribes[subject] = action; } @@ -2062,7 +2106,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { target: SafeAppointment, rawAppointment: SafeAppointment, onUpdatePrevented?: () => void, - dragEvent?: SchedulerDragEvent, + dragEvent?: SchedulerDragEvent | null, ): Promise { const updatingOptions = { newData: rawAppointment, @@ -2070,7 +2114,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { cancel: false, }; - const performFailAction = function (err?: any) { + const performFailAction = function (this: Scheduler, err?: Error): void { if (onUpdatePrevented) { onUpdatePrevented.call(this); } @@ -2107,8 +2151,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment); }) .fail(() => performFailAction()); - } catch (err) { - performFailAction(err); + } catch (err: unknown) { + performFailAction(err instanceof Error ? err : undefined); this.updatingAppointments.delete(target); deferred.resolve(); } @@ -2146,13 +2190,17 @@ class Scheduler extends SchedulerOptionsBaseWidget { return deferred.promise(); } - private expandAllDayPanel(appointment): void { + private expandAllDayPanel(appointment: SafeAppointment | Appointment): void { if (!this.isAllDayExpanded() && this.appointmentTakesAllDay(appointment)) { this.updateOption('workSpace', 'allDayExpanded', true); } } - private onDataPromiseCompleted(handlerName, storeAppointment, appointment?: any): void { + private onDataPromiseCompleted( + handlerName: StoreEventName, + storeAppointment: SafeAppointment | Error, + appointment?: SafeAppointment, + ): void { const args: any = { appointmentData: appointment || storeAppointment }; if (storeAppointment instanceof Error) { @@ -2177,7 +2225,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.actions; } - appointmentTakesAllDay(rawAppointment): boolean { + appointmentTakesAllDay(rawAppointment: SafeAppointment | Appointment): boolean { const appointment = new AppointmentAdapter( rawAppointment, this._dataAccessors, @@ -2189,8 +2237,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - dayHasAppointment(day, rawAppointment, trimTime): boolean { - const getConvertedToTimeZone = (date) => this.timeZoneCalculator.createDate(date, 'toGrid'); + dayHasAppointment(day: Date, rawAppointment: SafeAppointment | Appointment, trimTime: boolean): boolean { + const getConvertedToTimeZone = (date: Date): Date => this.timeZoneCalculator.createDate(date, 'toGrid'); const appointment = new AppointmentAdapter( rawAppointment, @@ -2228,36 +2276,40 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace.getEndViewDate(); } - showAddAppointmentPopup(cellData, cellGroups): void { + showAddAppointmentPopup(cellData: SelectedCellsClickCellData, cellGroups: GroupValues): void { const appointmentAdapter = new AppointmentAdapter({}, this._dataAccessors); appointmentAdapter.allDay = Boolean(cellData.allDay); - appointmentAdapter.startDate = cellData.startDateUTC; - appointmentAdapter.endDate = cellData.endDateUTC; + appointmentAdapter.startDate = new Date(cellData.startDateUTC); + appointmentAdapter.endDate = new Date(cellData.endDateUTC); const resultAppointment = extend(appointmentAdapter.source, cellGroups); this.showAppointmentPopup(resultAppointment, true); } - showAppointmentPopup(rawAppointment?: any, createNewAppointment?: boolean, rawTargetedAppointment?: any): void { - const newRawTargetedAppointment = { ...rawTargetedAppointment }; - if (newRawTargetedAppointment) { + showAppointmentPopup( + rawAppointment?: SafeAppointment, + createNewAppointment?: boolean, + rawTargetedAppointment?: SafeAppointment | TargetedAppointment, + ): void { + const newRawTargetedAppointment = rawTargetedAppointment + ? { ...rawTargetedAppointment } + : {} as SafeAppointment; + if (rawTargetedAppointment) { delete newRawTargetedAppointment.displayStartDate; delete newRawTargetedAppointment.displayEndDate; } - const newTargetedAppointment = extend({}, rawAppointment, newRawTargetedAppointment); - const isCreateAppointment = createNewAppointment ?? isEmptyObject(rawAppointment); - if (isEmptyObject(rawAppointment)) { - rawAppointment = this.createPopupAppointment(); - } - if (isCreateAppointment) { + const appointmentData = isEmptyObject(rawAppointment) + ? this.createPopupAppointment() + : rawAppointment as SafeAppointment; + delete this.editAppointmentData; // TODO if (this.editing.allowAdding) { - this.appointmentPopup.show(rawAppointment, { + this.appointmentPopup.show(appointmentData, { onSave: (appointment) => when(this.addAppointment(appointment)) .done(() => this.scrollToAppointment(appointment)), title: messageLocalization.format('dxScheduler-newPopupTitle'), @@ -2265,17 +2317,22 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } } else { - const startDate = this._dataAccessors.get('startDate', newRawTargetedAppointment || rawAppointment); + const appointmentData = rawAppointment as SafeAppointment; + const newTargetedAppointment = extend({}, appointmentData, newRawTargetedAppointment) as SafeAppointment; + const startDate = this._dataAccessors.get( + 'startDate', + Object.keys(newRawTargetedAppointment).length ? newRawTargetedAppointment : appointmentData, + ); - this.checkRecurringAppointment(rawAppointment, newTargetedAppointment, startDate, () => { - this.editAppointmentData = rawAppointment; // TODO + this.checkRecurringAppointment(appointmentData, newTargetedAppointment, startDate, () => { + this.editAppointmentData = appointmentData; // TODO - const adapter = new AppointmentAdapter(rawAppointment, this._dataAccessors); + const adapter = new AppointmentAdapter(appointmentData, this._dataAccessors); const isDisabled = Boolean(adapter.source) && adapter.disabled; const readOnly = isDisabled || !this.editing.allowUpdating; - this.appointmentPopup.show(rawAppointment, { - onSave: (appointment) => when(this.updateAppointment(rawAppointment, appointment)) + this.appointmentPopup.show(appointmentData, { + onSave: (appointment) => when(this.updateAppointment(appointmentData, appointment)) .done(() => this.scrollToAppointment(appointment)), title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly, @@ -2297,7 +2354,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return result; } - hideAppointmentPopup(saveChanges?: any): void { + hideAppointmentPopup(saveChanges?: boolean): void { if (this.appointmentPopup?.visible) { saveChanges && this.appointmentPopup.saveChangesAsync(); this.appointmentPopup.hide(); @@ -2434,7 +2491,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.updateAppointmentCore(target, appointment); } - deleteAppointment(rawAppointment): void { + deleteAppointment(rawAppointment: SafeAppointment): void { const deletingOptions = this.fireOnAppointmentDeleting(rawAppointment); this.processDeleteAppointment(rawAppointment, deletingOptions); } @@ -2472,9 +2529,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { } deleteRecurrence( - appointment, + appointment: SafeAppointment, date: Date | string, - recurrenceEditMode, + recurrenceEditMode: RecurrenceEditMode, ): void { if (typeof date === 'string') { date = new Date(date); @@ -2491,7 +2548,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }, true, false, - null, + undefined, recurrenceEditMode, ); } From f8695c1b93f8c576bfec7cc2823cafcea3e94c17 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Tue, 23 Jun 2026 13:15:29 +0200 Subject: [PATCH 10/17] refactor: enhance type definitions and improve type safety in scheduler --- .../js/__internal/scheduler/m_scheduler.ts | 569 +++++++++++------- 1 file changed, 362 insertions(+), 207 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 4cadf21c260e..ec6f956a02d6 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1,9 +1,12 @@ +/* eslint-disable devextreme-custom/no-deferred */ import { triggerResizeEvent } from '@js/common/core/events/visibility_change'; import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; +import type { DataSource } from '@js/common/data'; import registerComponent from '@js/core/component_registrator'; import config from '@js/core/config'; import { getPublicElement } from '@js/core/element'; +import type { PostponedOperations } from '@js/core/postponed_operations'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { BindableTemplate } from '@js/core/templates/bindable_template'; @@ -31,6 +34,7 @@ import { import { hasWindow } from '@js/core/utils/window'; import type { DataSourceOptions } from '@js/data/data_source'; import DataHelperMixin from '@js/data_helper'; +import type { CustomDialogOptions } from '@js/ui/dialog'; import { custom as customDialog } from '@js/ui/dialog'; import type { ItemContextMenuEvent } from '@js/ui/list'; import type { @@ -47,10 +51,12 @@ import type { CellContextMenuEvent, DayOfWeek, Occurrence, + Properties as SchedulerProperties, RecurrenceEditMode, SelectionEndEvent, } from '@js/ui/scheduler'; import errors from '@js/ui/widget/ui.errors'; +import type { Options } from '@ts/core/options/m_index'; import { dateUtilsTs } from '@ts/core/utils/date'; import type { OptionChanged } from '@ts/core/widget/types'; import type Scrollable from '@ts/ui/scroll_view/scrollable'; @@ -90,6 +96,7 @@ import type { AppointmentTooltipExtraOptions, AppointmentTooltipOptions } from ' import type { AppointmentTooltipContextMenuEventArgs, AppointmentTooltipItem, + CreateComponentFn, MappedAppointmentFields, SafeAppointment, ScrollToGroupValuesOrOptions, ScrollToOptions, TargetedAppointment, @@ -111,7 +118,7 @@ import type { GroupValues } from './utils/resource_manager/types'; import timeZoneUtils, { type TimezoneLabel } from './utils_time_zone'; import AppointmentLayoutManager from './view_model/appointments_layout_manager'; import { AppointmentDataSource } from './view_model/m_appointment_data_source'; -import type { AppointmentViewModelPlain } from './view_model/types'; +import type { AppointmentViewModelPlain, AppointmentItemViewModel } from './view_model/types'; import SchedulerAgenda from './workspaces/agenda'; import SchedulerTimelineDay from './workspaces/timeline_day'; import SchedulerTimelineMonth from './workspaces/timeline_month'; @@ -256,30 +263,69 @@ interface SchedulerEditing { allowDragging: boolean; } +type SchedulerEditingObject = Exclude, boolean>; + +type SchedulerEditingState = SchedulerEditing & Partial; + +interface RecurrenceDialog { + show: () => DeferredObj; + hide: (mode: string) => void; +} + +type AppointmentsEditingOptions = Partial<{ + allowDelete: boolean; + allowDrag: boolean; + allowResize: boolean; + allowAllDayResize: boolean; +}>; + +interface SchedulerAppointmentsHost { + option(options: Record): unknown; + option(name: string, value?: unknown): unknown; + repaint(): void; + $element(): dxElementWrapper; + focus(): void; + moveAppointmentBack(dragEvent?: SchedulerDragEvent | null): void; + updateResizableArea(): void; + renderDragClone(...args: unknown[]): dxElementWrapper; + getAppointmentData(...args: unknown[]): unknown; +} + class Scheduler extends SchedulerOptionsBaseWidget { // NOTE: Do not initialize variables here, because `_initMarkup` function runs before constructor, // and initialization in constructor will erase the data private timeZoneCalculatorInstance: TimeZoneCalculator | null = null; - postponedOperations: any; + declare postponedOperations: PostponedOperations; + + declare _disposed?: boolean; + + declare _options: Options; private a11yStatus!: dxElementWrapper; - // TODO: used externally in m_appointment_drag_behavior.ts, m_subscribes.ts, workspaces/work_space.ts + // TODO: used externally in m_appointment_drag_behavior.ts, + // m_subscribes.ts, workspaces/work_space.ts _workSpace!: SchedulerWorkSpace; private header?: SchedulerHeader; - // TODO: used externally in m_appointment_drag_behavior.ts, m_subscribes.ts, workspaces/work_space.ts - _appointments: any; + // TODO: used externally in m_appointment_drag_behavior.ts, + // m_subscribes.ts, workspaces/work_space.ts + _appointments!: SchedulerAppointmentsHost; private appointmentDragController!: AppointmentDragController; appointmentDataSource!: AppointmentDataSource; - _dataSource: any; + declare _dataSource: DataSource | undefined; - // TODO: used externally in m_subscribes.ts, view_model/preparation/prepare_appointments.ts, view_model/filtration/utils/get_filter_options/get_filter_options.ts + // TODO: used externally in workspaces/work_space.ts + declare _createComponent: CreateComponentFn; + + // TODO: used externally in m_subscribes.ts, + // view_model/preparation/prepare_appointments.ts, + // view_model/filtration/utils/get_filter_options/get_filter_options.ts _dataAccessors!: AppointmentDataAccessor; resourceManager!: ResourceManager; @@ -296,41 +342,37 @@ class Scheduler extends SchedulerOptionsBaseWidget { private readyToRenderAppointments?: boolean; - private editing: any; + private editing!: SchedulerEditingState; - private workSpaceRecalculation: any; + private workSpaceRecalculation!: DeferredObj; - private appointmentPopup: any; + private appointmentPopup!: AppointmentPopup; // TODO: used externally in m_subscribes.ts _compactAppointmentsHelper!: CompactAppointmentsHelper; - private asyncTemplatesTimers!: any[]; + private asyncTemplatesTimers: ReturnType[] = []; private readonly updatingAppointments: Set = new Set(); - private dataSourceLoadedCallback: any; + private dataSourceLoadedCallback!: ReturnType; - private subscribes: any; + private subscribes: SubscribeMethods = subscribes; private notifyScheduler!: NotifyScheduler; - private recurrenceDialog: any; + private recurrenceDialog?: RecurrenceDialog; // TODO: used externally in m_subscribes.ts _layoutManager!: AppointmentLayoutManager; - private appointmentForm: any; + private appointmentForm!: AppointmentForm; - private mainContainer: any; + private mainContainer!: dxElementWrapper; private $draggableContainer?: dxElementWrapper; - private readonly all: any; - - _options: any; - - private editAppointmentData: any; + private editAppointmentData?: SafeAppointment; private timeZonesPromise!: Promise; @@ -350,8 +392,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { const whenLoaded = this.postponedOperations.add('loadResources', () => { const groups = this.getViewOption('groups'); - return fromPromise(this.resourceManager.loadGroupResources(groups, forceReload)); - }); + return when(this.resourceManager.loadGroupResources(groups, forceReload)); + }, undefined); const resolveCallbacks: DeferredObj = Deferred(); @@ -395,16 +437,18 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._initDataSource(); this.postponeResourceLoading().done(() => { - this.appointmentDataSource.setDataSource(this._dataSource); + if (this._dataSource) { + this.appointmentDataSource.setDataSource(this._dataSource); + } this.setRemoteFilterIfNeeded(); this.updateOption('workSpace', 'showAllDayPanel', this.option('showAllDayPanel')); }); break; case 'min': case 'max': { - const value = this.getViewOption(name); - this.updateOption('header', name, value); - this.updateOption('workSpace', name, value); + const viewValue = this.getViewOption(name); + this.updateOption('header', name, viewValue); + this.updateOption('workSpace', name, viewValue); break; } case 'views': @@ -523,7 +567,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { case 'onAppointmentFormOpening': case 'onAppointmentTooltipShowing': case 'onSelectionEnd': - (this.actions as unknown as Record void>)[name] = this._createActionByOption(name); + this.actions[name] = this._createActionByOption(name); break; case 'onAppointmentRendered': if (this.option('_newAppointments')) { @@ -700,9 +744,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateOption('workSpace', 'draggingMode', value); break; case 'toolbar': - this.header - ? this.header.onToolbarOptionChanged(args.fullName, value) - : this.repaint(); + if (this.header) { + this.header.onToolbarOptionChanged(args.fullName, value); + } else { + this.repaint(); + } break; default: // @ts-expect-error @@ -711,7 +757,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private bringEditingModeToAppointments(editing: SchedulerEditing): void { - const editingConfig: any = { + const editingConfig: AppointmentsEditingOptions = { allowDelete: editing.allowUpdating && editing.allowDeleting, }; @@ -784,11 +830,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { const result: DeferredObj = Deferred(); if (this._dataSource) { - this._dataSource.load().done(() => { + void this._dataSource.load().then(() => { hideLoading().catch(noop); this._fireContentReadyAction(result); - }).fail(() => { + }).catch(() => { hideLoading().catch(noop); result.reject(); }); @@ -797,7 +843,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { showLoading({ container: this.$element().get(0), position: { - of: this.$element() as any, + of: this.$element().get(0), }, }).catch(noop); } @@ -811,13 +857,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { _fireContentReadyAction(result?: DeferredObj): void { // @ts-expect-error const contentReadyBase = super._fireContentReadyAction.bind(this); - const fireContentReady = () => { + const fireContentReady = (): void => { contentReadyBase(); result?.resolve(); }; - if (this.workSpaceRecalculation) { - this.workSpaceRecalculation?.done(() => { + if (this.workSpaceRecalculation?.state() === 'pending') { + this.workSpaceRecalculation.done(() => { fireContentReady(); }); } else { @@ -867,16 +913,19 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private toggleSmallClass(): void { - const { width } = getBoundingRect((this.$element() as any).get(0)); - (this.$element() as any).toggleClass(WIDGET_SMALL_CLASS, width < WIDGET_SMALL_WIDTH); + const element = this.$element().get(0); + const { width } = getBoundingRect(element); + this.$element().toggleClass(WIDGET_SMALL_CLASS, width < WIDGET_SMALL_WIDTH); } private toggleAdaptiveClass(): void { - (this.$element() as any).toggleClass(WIDGET_ADAPTIVE_CLASS, this.option('adaptivityEnabled')); + (this.$element()).toggleClass(WIDGET_ADAPTIVE_CLASS, this.option('adaptivityEnabled')); } _visibilityChanged(visible: boolean): void { - visible && this._dimensionChanged(null, true); + if (visible) { + this._dimensionChanged(null, true); + } } _dataSourceOptions(): DataSourceOptions { @@ -914,7 +963,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.customizeDataSourceLoadOptions(); - (this.$element() as any).addClass(WIDGET_CLASS); + this.$element().addClass(WIDGET_CLASS); this.initEditing(); @@ -936,9 +985,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.appointmentDragController = new AppointmentDragController({ component: this, - $draggableContainer: () => this.$draggableContainer!, + $draggableContainer: (): dxElementWrapper => this.$draggableContainer ?? this.$element(), canDragAppointment: this.canDragAppointment.bind(this), - getCellFromDragTarget: ($dragTarget) => this._workSpace.getCellFromDragTarget($dragTarget), + getCellFromDragTarget: ($dragTarget: dxElementWrapper): dxElementWrapper | null => ( + this._workSpace.getCellFromDragTarget($dragTarget) + ), // @ts-expect-error _createComponent is not defined in ts createComponent: this._createComponent.bind(this), @@ -950,6 +1001,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { createAppointmentDataSource(): void { this.appointmentDataSource?.destroy(); + if (!this._dataSource) { + return; + } this.appointmentDataSource = new AppointmentDataSource(this._dataSource); } @@ -962,6 +1016,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private customizeDataSourceLoadOptions(): void { + // @ts-expect-error customizeStoreLoadOptions is a custom DataSource event this._dataSource?.on('customizeStoreLoadOptions', ({ storeLoadOptions }) => { storeLoadOptions.startDate = this.getStartViewDate(); storeLoadOptions.endDate = this.getEndViewDate(); @@ -986,7 +1041,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { compileGetter(`appointmentData.${property}`) as (data: unknown) => unknown ); - const getDate = (getter: (data: unknown) => unknown) => (data: unknown) => { + const getDate = (getter: (data: unknown) => unknown) => (data: unknown): unknown => { const value = getter(data); if (value instanceof Date) { return value.valueOf(); @@ -996,7 +1051,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._templateManager.addDefaultTemplates({ item: new BindableTemplate( - ($container, data, model) => this.getAppointmentsInstance()._renderAppointmentTemplate($container, data, model), + ($container, data, model) => this.getAppointmentsInstance() + ._renderAppointmentTemplate($container, data, model), [ 'html', 'text', @@ -1106,7 +1162,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { popup: undefined, }).every((value) => !value); - (this.$element() as any).toggleClass(WIDGET_READONLY_CLASS, isReadOnly); + this.$element().toggleClass(WIDGET_READONLY_CLASS, isReadOnly); } _dispose(): void { @@ -1180,9 +1236,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private initMarkupOnResourceLoaded(): void { - if (!(this as any)._disposed) { + if (!this._disposed) { this.initMarkupCore(); - this.reloadDataSource(); + this.reloadDataSource().catch(noop); } } @@ -1219,7 +1275,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, getDataAccessor: () => this._dataAccessors, - getStartViewDate: (): Date => this.getStartViewDate()!, + getStartViewDate: (): Date => { + const startViewDate = this.getStartViewDate(); + if (!startViewDate) { + throw errors.Error('E0001'); + } + return startViewDate; + }, getSortedItems: () => this._layoutManager.sortedItems, isVirtualScrolling: () => this.isVirtualScrolling(), @@ -1246,14 +1308,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { // @ts-expect-error if (this.isDataSourceLoaded() || this._isDataSourceLoading()) { this.initMarkupCore(); - this._dataSourceChangedHandler(this._dataSource.items()); + this._dataSourceChangedHandler(this._dataSource?.items() ?? []); this._fireContentReadyAction(); } else { const groups = this.getViewOption('groups'); if (groups?.length) { this.resourceManager.loadGroupResources(groups, true) - .then(() => this.initMarkupOnResourceLoaded()); + .then(() => this.initMarkupOnResourceLoaded()) + .catch(noop); } else { this.initMarkupOnResourceLoaded(); } @@ -1280,38 +1343,50 @@ class Scheduler extends SchedulerOptionsBaseWidget { } createAppointmentForm(): AppointmentForm { - const config: AppointmentFormConfig = { + const appointmentFormConfig: AppointmentFormConfig = { dataAccessors: this._dataAccessors, editing: this.editing, resourceManager: this.resourceManager, firstDayOfWeek: this.getFirstDayOfWeek(), startDayHour: this.option('startDayHour') ?? 0, // @ts-expect-error - createComponent: (element, component, options) => this._createComponent(element, component, options), - getCalculatedEndDate: (startDateWithStartHour) => this._workSpace.calculateEndDate(startDateWithStartHour), + createComponent: this._createComponent.bind(this), + getCalculatedEndDate: (startDateWithStartHour: Date): Date => ( + this._workSpace.calculateEndDate(startDateWithStartHour) + ), }; - return new AppointmentForm(config); + return new AppointmentForm(appointmentFormConfig); } createAppointmentPopup(form: AppointmentForm): AppointmentPopup { const scheduler = { - getElement: () => this.$element(), + getElement: (): dxElementWrapper => this.$element(), // @ts-expect-error - createComponent: (element, component, options) => this._createComponent(element, component, options), - focus: () => this.focus(), + createComponent: this._createComponent.bind(this), + focus: (): void => { this.focus(); }, - getResourceManager: () => this.resourceManager, + getResourceManager: (): ResourceManager => this.resourceManager, - getEditingConfig: () => this.editing, + getEditingConfig: (): SchedulerEditingState => this.editing, getTimeZoneCalculator: (): TimeZoneCalculator => this.timeZoneCalculator, - getDataAccessors: () => this._dataAccessors, - getAppointmentFormOpening: () => this.actions.onAppointmentFormOpening, - processActionResult: (arg, canceled) => this.processActionResult(arg, canceled), + getDataAccessors: (): AppointmentDataAccessor => this._dataAccessors, + getAppointmentFormOpening: (): SchedulerActions['onAppointmentFormOpening'] => ( + this.actions.onAppointmentFormOpening + ), + processActionResult: ( + arg: SchedulerActionOptions, + canceled: ProcessActionCallback, + ): Promise => this.processActionResult(arg, canceled), - addAppointment: (appointment) => this.addAppointment(appointment), - updateAppointment: (sourceAppointment, updatedAppointment) => this.updateAppointment(sourceAppointment, updatedAppointment), + addAppointment: (appointment: SafeAppointment): Promise => ( + this.addAppointment(appointment) + ), + updateAppointment: ( + sourceAppointment: SafeAppointment, + updatedAppointment: SafeAppointment, + ): Promise => this.updateAppointment(sourceAppointment, updatedAppointment), }; return new AppointmentPopup(scheduler as never, form); @@ -1339,17 +1414,16 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private getAppointmentTooltipOptions(): AppointmentTooltipOptions { - const that = this; return { // @ts-expect-error _createComponent is not defined in ts - createComponent: that._createComponent.bind(that), - container: that.$element(), - getScrollableContainer: that.getWorkSpaceScrollableContainer.bind(that), - addDefaultTemplates: that._templateManager.addDefaultTemplates.bind(that._templateManager), - getAppointmentTemplate: that.getAppointmentTemplate.bind(that), - showAppointmentPopup: that.showAppointmentPopup.bind(that), - checkAndDeleteAppointment: that.checkAndDeleteAppointment.bind(that), - isAppointmentInAllDayPanel: that.isAppointmentInAllDayPanel.bind(that), + createComponent: this._createComponent.bind(this), + container: this.$element(), + getScrollableContainer: this.getWorkSpaceScrollableContainer.bind(this), + addDefaultTemplates: this._templateManager.addDefaultTemplates.bind(this._templateManager), + getAppointmentTemplate: this.getAppointmentTemplate.bind(this), + showAppointmentPopup: this.showAppointmentPopup.bind(this), + checkAndDeleteAppointment: this.checkAndDeleteAppointment.bind(this), + isAppointmentInAllDayPanel: this.isAppointmentInAllDayPanel.bind(this), createFormattedDateText: (appointment, targetedAppointment, format) => this.fire( 'createFormattedDateText', @@ -1358,12 +1432,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { format, ), getAppointmentDisabled: (appointment) => this._dataAccessors.get('disabled', appointment), - onItemContextMenu: that._createActionByOption('onAppointmentContextMenu'), - createEventArgs: that._createEventArgs.bind(that), + onItemContextMenu: this._createActionByOption('onAppointmentContextMenu'), + createEventArgs: this._createEventArgs.bind(this), newAppointments: Boolean(this.option('_newAppointments')), onAppointmentClick: (args: AppointmentClickEvent) => this.actions.onAppointmentClick(args), - onListInitialized: (e) => { + onListInitialized: (e): void => { if (this.option('_newAppointments') && e.element) { this.appointmentDragController.createTooltipDraggable( $(e.element), @@ -1373,7 +1447,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } }, - onListDisposing: () => { + onListDisposing: (): void => { this.appointmentDragController.disposeTooltipDraggable(); }, } satisfies AppointmentTooltipOptions; @@ -1383,23 +1457,28 @@ class Scheduler extends SchedulerOptionsBaseWidget { _createEventArgs( e: ItemContextMenuEvent, ): AppointmentTooltipContextMenuEventArgs { - const config: { + const itemData = e.itemData?.appointment; + if (!itemData) { + throw errors.Error('E0001'); + } + + const eventConfig: { itemData: SafeAppointment; itemElement: dxElementWrapper; targetedAppointment?: SafeAppointment | TargetedAppointment; } = { - itemData: e.itemData!.appointment, + itemData, itemElement: $(e.itemElement), - targetedAppointment: e.itemData!.targetedAppointment, + targetedAppointment: e.itemData?.targetedAppointment, }; - const mappedFields = this.fire('mapAppointmentFields', config) as MappedAppointmentFields; + const mappedFields = this.fire('mapAppointmentFields', eventConfig) as MappedAppointmentFields; return extend({}, mappedFields, { component: e.component, element: e.element, event: e.event, model: e.model, - }); + }) as AppointmentTooltipContextMenuEventArgs; } checkAndDeleteAppointment( @@ -1442,7 +1521,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { private initMarkupCore(): void { this.readyToRenderAppointments = hasWindow(); - this._workSpace && this.cleanWorkspace(); + if (this._workSpace) { + this.cleanWorkspace(); + } this.renderWorkSpace(); if (this.option('_newAppointments')) { @@ -1453,7 +1534,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { allDayContainer: this._workSpace.getAllDayContainer(), }); } - this.waitAsyncTemplate(() => this.workSpaceRecalculation?.resolve()); + this.waitAsyncTemplate((): void => { this.workSpaceRecalculation?.resolve(); }); this.createAppointmentDataSource(); this.setRemoteFilterIfNeeded(); @@ -1462,7 +1543,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private isDataSourceLoaded(): boolean { - return this._dataSource?.isLoaded(); + return Boolean(this._dataSource?.isLoaded()); } _render(): void { @@ -1495,7 +1576,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { min: this.getViewOption('min'), max: this.getViewOption('max'), indicatorTime: this.option('indicatorTime'), - startViewDate: this.getStartViewDate()!, + startViewDate: this.getStartViewDate() ?? new Date(), tabIndex: this.option('tabIndex'), focusStateEnabled: this.option('focusStateEnabled'), useDropDownViewSwitcher: this.option('useDropDownViewSwitcher'), @@ -1512,7 +1593,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private appointmentsConfig(): AppointmentCollectionOptions { - const config: AppointmentCollectionOptions = { + const collectionConfig: AppointmentCollectionOptions = { getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, getSortedAppointments: () => this._layoutManager.sortedItems, @@ -1534,7 +1615,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { groups: this.getViewOption('groups'), groupByDate: Boolean(this.getViewOption('groupByDate')), timeZoneCalculator: this.timeZoneCalculator, - getResizableStep: () => (this._workSpace ? this._workSpace.positionHelper.getResizableStep() : 0), + getResizableStep: () => ( + this._workSpace ? this._workSpace.positionHelper.getResizableStep() : 0 + ), getDOMElementsMetaData: () => this._workSpace?.getDOMElementsMetaData(), getViewDataProvider: () => this._workSpace?.viewDataProvider, // @ts-expect-error protected method @@ -1545,7 +1628,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }, }; - return config; + return collectionConfig; } private renderWorkSpace(): void { @@ -1574,7 +1657,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { Promise.resolve().then(() => { this.toggleSmallClass(); this._workSpace?.updateHeaderEmptyCellWidth(); - }); + }).catch(noop); } const $workSpace = $('
').appendTo(this.mainContainer); @@ -1672,7 +1755,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { renderAppointments: (): void => { this.renderAppointments(); }, onShowAllDayPanel: (value: boolean) => this.option('showAllDayPanel', value), getHeaderHeight: (): number => utils.DOM.getHeaderHeight(this.header), - onScrollEnd: (): void => this._appointments.updateResizableArea(), + onScrollEnd: (): void => { + if (!this.option('_newAppointments')) { + (this._appointments as AppointmentCollection).updateResizableArea(); + } + }, onInitialized: (e) => { if (this.option('_newAppointments')) { this.appointmentDragController.createWorkSpaceDraggable( @@ -1733,7 +1820,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } // @ts-expect-error - return this._getTemplateByOption(optionName); + return this._getTemplateByOption(optionName) as FunctionTemplate; } private updateOption(viewName: 'workSpace' | 'header', optionName: string, value: T): void { @@ -1762,7 +1849,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - this.waitAsyncTemplate(() => this.workSpaceRecalculation.resolve()); + this.waitAsyncTemplate((): void => { this.workSpaceRecalculation.resolve(); }); } } @@ -1812,7 +1899,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateAppointmentCore( appointmentData, newAppointmentData, - ).finally(resolve); + ).finally(resolve).catch(noop); }, false, undefined, @@ -1863,16 +1950,20 @@ class Scheduler extends SchedulerOptionsBaseWidget { } this.showRecurrenceChangeConfirm(isDeleted) .done((editingMode) => { - editingMode === RECURRENCE_EDITING_MODE.SERIES && callback(); - - editingMode === RECURRENCE_EDITING_MODE.OCCURRENCE && this.excludeAppointmentFromSeries( - rawAppointment, - singleAppointment, - exceptionDate, - isDeleted, - Boolean(isPopupEditing), - dragEvent, - ); + if (editingMode === RECURRENCE_EDITING_MODE.SERIES) { + callback(); + } + + if (editingMode === RECURRENCE_EDITING_MODE.OCCURRENCE) { + this.excludeAppointmentFromSeries( + rawAppointment, + singleAppointment, + exceptionDate, + isDeleted, + Boolean(isPopupEditing), + dragEvent, + ); + } }) .fail(() => { if (this.option('_newAppointments')) { @@ -1909,16 +2000,16 @@ class Scheduler extends SchedulerOptionsBaseWidget { const canCreateNewAppointment = !isDeleted && !isPopupEditing; if (canCreateNewAppointment) { - this.addAppointment(singleRawAppointment); + this.addAppointment(singleRawAppointment).catch(noop); } if (isPopupEditing) { this.appointmentPopup.show(singleRawAppointment, { - onSave: (newAppointment) => { - this.updateAppointment(rawAppointment, appointment.source); - return when(this.addAppointment(newAppointment)) - .done(() => this.scrollToAppointment(newAppointment)); - }, + onSave: (newAppointment) => ( + this.updateAppointment(rawAppointment, appointment.source) + .then(() => this.addAppointment(newAppointment as SafeAppointment)) + .then(() => { this.scrollToAppointment(newAppointment as SafeAppointment); }) + ), title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly: Boolean(appointment.source) && appointment.disabled, }); @@ -1926,7 +2017,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } else { this.updateAppointmentCore(rawAppointment, appointment.source, () => { this._appointments.moveAppointmentBack(dragEvent); - }, dragEvent); + }, dragEvent).catch(noop); } } @@ -1934,7 +2025,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { appointment: SafeAppointment & { recurrenceException?: string }, exceptionDate: Date, ): string { - const result: any[] = []; + const result: string[] = []; if (appointment.recurrenceException) { result.push(appointment.recurrenceException); @@ -1946,14 +2037,16 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private getSerializedDate(date: Date, startDate: Date, isAllDay: boolean): string { - isAllDay && date.setHours( - startDate.getHours(), - startDate.getMinutes(), - startDate.getSeconds(), - startDate.getMilliseconds(), - ); + if (isAllDay) { + date.setHours( + startDate.getHours(), + startDate.getMinutes(), + startDate.getSeconds(), + startDate.getMilliseconds(), + ); + } - return dateSerialization.serializeDate(date, UTC_FULL_DATE_FORMAT); + return dateSerialization.serializeDate(date, UTC_FULL_DATE_FORMAT) as string; } private showRecurrenceChangeConfirm(isDeleted: boolean): DeferredObj { @@ -1968,16 +2061,16 @@ class Scheduler extends SchedulerOptionsBaseWidget { showCloseButton: true, showTitle: true, buttons: [ - { text: seriesText, onClick() { return RECURRENCE_EDITING_MODE.SERIES; } }, - { text: occurrenceText, onClick() { return RECURRENCE_EDITING_MODE.OCCURRENCE; } }, + { text: seriesText, onClick: (): string => RECURRENCE_EDITING_MODE.SERIES }, + { text: occurrenceText, onClick: (): string => RECURRENCE_EDITING_MODE.OCCURRENCE }, ], popupOptions: { wrapperAttr: { class: POPUP_DIALOG_CLASS }, - onHidden: () => { + onHidden: (): void => { this._appointments?.focus(); }, }, - } as any); + } as CustomDialogOptions) as RecurrenceDialog; return this.recurrenceDialog.show(); } @@ -2015,8 +2108,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { appointmentStartDate = resultedStartDate; } - if (!dateUtilsTs.isValidDate(appointmentEndDate)) { - appointmentEndDate = cellEndDate!; + if (!dateUtilsTs.isValidDate(appointmentEndDate) && cellEndDate) { + appointmentEndDate = cellEndDate; } const duration = appointmentEndDate.getTime() - appointmentStartDate.getTime(); @@ -2046,11 +2139,19 @@ class Scheduler extends SchedulerOptionsBaseWidget { let resultedEndDate = new Date(resultedStartDate.getTime() + duration); - if (this.appointmentTakesAllDay(rawAppointment) && !result.allDay && this._workSpace.supportAllDayRow()) { + if ( + this.appointmentTakesAllDay(rawAppointment) + && !result.allDay + && this._workSpace.supportAllDayRow() + ) { resultedEndDate = this._workSpace.calculateEndDate(resultedStartDate); } - if (appointment.allDay && !this._workSpace.supportAllDayRow() && !this._workSpace.keepOriginalHours()) { + if ( + appointment.allDay + && !this._workSpace.supportAllDayRow() + && !this._workSpace.keepOriginalHours() + ) { const dateCopy = new Date(resultedStartDate); dateCopy.setHours(0); @@ -2071,8 +2172,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { } // TODO: delete this method when old impl is removed - getTargetedAppointment(appointment: SafeAppointment, element: dxElementWrapper): TargetedAppointment { - const settings = utils.dataAccessors.getAppointmentSettings(element)!; + getTargetedAppointment( + appointment: SafeAppointment, + element: dxElementWrapper, + ): TargetedAppointment { + const settings = utils.dataAccessors.getAppointmentSettings(element); + if (!settings) { + throw errors.Error('E0001'); + } return getTargetedAppointment( appointment, settings, @@ -2082,7 +2189,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { } subscribe(subject: K, action: SubscribeMethods[K]): void { - this.subscribes[subject] = subscribes[subject] = action; + this.subscribes[subject] = action; + subscribes[subject] = action; } fire( @@ -2095,7 +2203,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { throw errors.Error('E1031', subject); } - return callback.call(this, ...args); + // subscribes callbacks are not fully typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return (callback as ( + this: Scheduler, + ...callbackArgs: Parameters + ) => ReturnType).apply(this, args); } getTargetCellData(): DroppableCellData { @@ -2114,12 +2227,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { cancel: false, }; - const performFailAction = function (this: Scheduler, err?: Error): void { + const performFailAction = function performFailAction(this: Scheduler, err?: Error): void { if (onUpdatePrevented) { onUpdatePrevented.call(this); } - if (err && err.name === 'Error') { + if (err?.name === 'Error') { throw err; } }.bind(this); @@ -2134,35 +2247,34 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updatingAppointments.add(target); } - return this.processActionResult(updatingOptions, function (canceled) { - let deferred: DeferredObj = Deferred(); - + return this.processActionResult(updatingOptions, (canceled) => { if (!canceled) { this.expandAllDayPanel(rawAppointment); try { - deferred = this.appointmentDataSource + return this.appointmentDataSource .update(target, rawAppointment) .done(() => { dragEvent?.cancel.resolve(false); }) .always((storeAppointment) => { this.updatingAppointments.delete(target); - this.onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment); + if (isDefined(storeAppointment)) { + this.onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment); + } }) - .fail(() => performFailAction()); + .fail(() => performFailAction()) + .then(() => undefined); } catch (err: unknown) { performFailAction(err instanceof Error ? err : undefined); this.updatingAppointments.delete(target); - deferred.resolve(); + return Deferred().resolve().promise(); } - } else { - performFailAction(); - this.updatingAppointments.delete(target); - deferred.resolve(); } - return deferred.promise(); + performFailAction(); + this.updatingAppointments.delete(target); + return Deferred().resolve().promise(); }); } @@ -2178,10 +2290,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (isPromise(actionOptions.cancel)) { when(fromPromise(actionOptions.cancel)).always((cancel) => { - if (!isDefined(cancel)) { - cancel = (actionOptions.cancel as DeferredObj).state() === 'rejected'; + let isCanceled = cancel; + if (!isDefined(isCanceled)) { + isCanceled = (actionOptions.cancel as DeferredObj).state() === 'rejected'; } - resolveCallback(callback.call(this, cancel)); + resolveCallback(callback.call(this, isCanceled)); }); } else { resolveCallback(callback.call(this, actionOptions.cancel as boolean)); @@ -2201,20 +2314,23 @@ class Scheduler extends SchedulerOptionsBaseWidget { storeAppointment: SafeAppointment | Error, appointment?: SafeAppointment, ): void { - const args: any = { appointmentData: appointment || storeAppointment }; + const args: AppointmentCompletedOptions = { + appointmentData: (appointment ?? storeAppointment) as SafeAppointment, + }; if (storeAppointment instanceof Error) { args.error = storeAppointment; - } else { - this.appointmentPopup.visible && this.appointmentPopup.hide(); + } else if (this.appointmentPopup.visible) { + this.appointmentPopup.hide(); } - this.actions[handlerName](args); + const handler = this.actions[handlerName] as (args: AppointmentCompletedOptions) => void; + handler(args); this._fireContentReadyAction(); } getAppointmentsInstance(): Appointments | AppointmentCollection { - return this._appointments; + return this._appointments as Appointments | AppointmentCollection; } getLayoutManager(): AppointmentLayoutManager { @@ -2237,7 +2353,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { ); } - dayHasAppointment(day: Date, rawAppointment: SafeAppointment | Appointment, trimTime: boolean): boolean { + dayHasAppointment( + day: Date, + rawAppointment: SafeAppointment | Appointment, + trimTime: boolean, + ): boolean { const getConvertedToTimeZone = (date: Date): Date => this.timeZoneCalculator.createDate(date, 'toGrid'); const appointment = new AppointmentAdapter( @@ -2255,15 +2375,19 @@ class Scheduler extends SchedulerOptionsBaseWidget { return startDate.getTime() === endDate.getTime(); } + let dayStart = day; + let startDateStart = startDate; + let endDateEnd = endDate; + if (trimTime) { - day = dateUtils.trimTime(day); - startDate = dateUtils.trimTime(startDate); - endDate = dateUtils.trimTime(endDate); + dayStart = dateUtils.trimTime(day); + startDateStart = dateUtils.trimTime(startDate); + endDateEnd = dateUtils.trimTime(endDate); } - const dayTimeStamp = day.getTime(); - const startDateTimeStamp = startDate.getTime(); - const endDateTimeStamp = endDate.getTime(); + const dayTimeStamp = dayStart.getTime(); + const startDateTimeStamp = startDateStart.getTime(); + const endDateTimeStamp = endDateEnd.getTime(); return startDateTimeStamp <= dayTimeStamp && dayTimeStamp <= endDateTimeStamp; } @@ -2310,15 +2434,21 @@ class Scheduler extends SchedulerOptionsBaseWidget { delete this.editAppointmentData; // TODO if (this.editing.allowAdding) { this.appointmentPopup.show(appointmentData, { - onSave: (appointment) => when(this.addAppointment(appointment)) - .done(() => this.scrollToAppointment(appointment)), + onSave: (appointment) => ( + this.addAppointment(appointment as SafeAppointment) + .then(() => { this.scrollToAppointment(appointment as SafeAppointment); }) + ), title: messageLocalization.format('dxScheduler-newPopupTitle'), readOnly: false, }); } } else { const appointmentData = rawAppointment as SafeAppointment; - const newTargetedAppointment = extend({}, appointmentData, newRawTargetedAppointment) as SafeAppointment; + const newTargetedAppointment = extend( + {}, + appointmentData, + newRawTargetedAppointment, + ) as SafeAppointment; const startDate = this._dataAccessors.get( 'startDate', Object.keys(newRawTargetedAppointment).length ? newRawTargetedAppointment : appointmentData, @@ -2332,8 +2462,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { const readOnly = isDisabled || !this.editing.allowUpdating; this.appointmentPopup.show(appointmentData, { - onSave: (appointment) => when(this.updateAppointment(appointmentData, appointment)) - .done(() => this.scrollToAppointment(appointment)), + onSave: (appointment) => ( + this.updateAppointment(appointmentData, appointment as SafeAppointment) + .then(() => { this.scrollToAppointment(appointment as SafeAppointment); }) + ), title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly, }); @@ -2343,10 +2475,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { createPopupAppointment(): SafeAppointment { const result: SafeAppointment = {}; - const toMs = dateUtils.dateToMilliseconds; + const msPerMinute = dateUtils.dateToMilliseconds('minute'); const startDate = new Date(this.option('currentDate')); - const endDate = new Date(startDate.getTime() + this.option('cellDuration') * toMs('minute')); + const endDate = new Date(startDate.getTime() + this.option('cellDuration') * msPerMinute); this._dataAccessors.set('startDate', result, startDate); this._dataAccessors.set('endDate', result, endDate); @@ -2356,7 +2488,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { hideAppointmentPopup(saveChanges?: boolean): void { if (this.appointmentPopup?.visible) { - saveChanges && this.appointmentPopup.saveChangesAsync(); + if (saveChanges) { + this.appointmentPopup.saveChangesAsync().catch(noop); + } this.appointmentPopup.hide(); } } @@ -2368,17 +2502,21 @@ class Scheduler extends SchedulerOptionsBaseWidget { targetedAppointment?: SafeAppointment, ): void { if (appointment) { - const settings: any = utils.dataAccessors.getAppointmentSettings(element); + const settings = utils.dataAccessors.getAppointmentSettings(element); + if (!settings) { + return; + } + const appointmentConfig = { itemData: targetedAppointment ?? appointment, - groupIndex: settings?.groupIndex, + groupIndex: settings.groupIndex ?? 0, }; const info: AppointmentTooltipItem = { appointment, targetedAppointment, color: this.resourceManager.getAppointmentColor(appointmentConfig), - settings, + settings: settings as AppointmentItemViewModel, }; this.showAppointmentTooltipCore(element, [info]); @@ -2406,11 +2544,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.hideAppointmentTooltip(); } else { this.processActionResult({ ...arg, cancel: arg.cancel ?? false }, (canceled) => { - !canceled && this.appointmentTooltip.show(target, data, { - ...this.getExtraAppointmentTooltipOptions(), - ...options, - }); - }); + if (!canceled) { + this.appointmentTooltip.show(target, data, { + ...this.getExtraAppointmentTooltipOptions(), + ...options, + }); + } + }).catch(noop); } } @@ -2423,29 +2563,39 @@ class Scheduler extends SchedulerOptionsBaseWidget { groupValuesOrOptions?: ScrollToGroupValuesOrOptions, allDay?: boolean | undefined, ): void { - let groupValues; - let allDayValue; - let align: 'start' | 'center' = 'center'; - if (this.isScrollOptionsObject(groupValuesOrOptions)) { - groupValues = groupValuesOrOptions.group; - allDayValue = groupValuesOrOptions.allDay; - align = groupValuesOrOptions.alignInView ?? 'center'; - } else { - if (isDefined(groupValuesOrOptions) || isDefined(allDay)) { - errors.log('W0002', 'dxScheduler', 'scrollTo(date, group, allDay)', '26.1', 'Use scrollTo(date, { group, allDay, alignInView }) instead.'); - } + this._workSpace.scrollTo( + date, + groupValuesOrOptions.group, + groupValuesOrOptions.allDay, + true, + groupValuesOrOptions.alignInView ?? 'center', + ); + return; + } - groupValues = groupValuesOrOptions; - allDayValue = allDay; + if (isDefined(groupValuesOrOptions) || isDefined(allDay)) { + errors.log( + 'W0002', + 'dxScheduler', + 'scrollTo(date, group, allDay)', + '26.1', + 'Use scrollTo(date, { group, allDay, alignInView }) instead.', + ); } - this._workSpace.scrollTo(date, groupValues, allDayValue, true, align); + this._workSpace.scrollTo(date, groupValuesOrOptions, allDay, true, 'center'); } - private isScrollOptionsObject(options?: ScrollToGroupValuesOrOptions): options is ScrollToOptions { + private isScrollOptionsObject( + options?: ScrollToGroupValuesOrOptions, + ): options is ScrollToOptions { return Boolean(options) && typeof options === 'object' - && ('alignInView' in options || 'allDay' in options || 'group' in options); + && ( + 'alignInView' in options + || 'allDay' in options + || 'group' in options + ); } private isHorizontalVirtualScrolling(): boolean { @@ -2483,7 +2633,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this.appointmentDataSource .add(serializedAppointment) - .always((storeAppointment) => this.onDataPromiseCompleted(StoreEventNames.ADDED, storeAppointment)); + .always((storeAppointment) => { + this.onDataPromiseCompleted(StoreEventNames.ADDED, storeAppointment); + }); }); } @@ -2515,17 +2667,21 @@ class Scheduler extends SchedulerOptionsBaseWidget { rawAppointment: SafeAppointment, deletingOptions: AppointmentDeletingOptions, ): void { - this.processActionResult(deletingOptions, function (canceled) { + this.processActionResult(deletingOptions, (canceled) => { if (!canceled) { this.appointmentDataSource .remove(rawAppointment) - .always((storeAppointment) => this.onDataPromiseCompleted( - StoreEventNames.DELETED, - storeAppointment, - rawAppointment, - )); + .always((storeAppointment) => { + if (isDefined(storeAppointment)) { + this.onDataPromiseCompleted( + StoreEventNames.DELETED, + storeAppointment, + rawAppointment, + ); + } + }); } - }); + }).catch(noop); } deleteRecurrence( @@ -2533,13 +2689,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { date: Date | string, recurrenceEditMode: RecurrenceEditMode, ): void { - if (typeof date === 'string') { - date = new Date(date); - } + const dateValue = typeof date === 'string' ? new Date(date) : date; this.checkRecurringAppointment( appointment, {}, - date, + dateValue, () => { this.processDeleteAppointment( appointment, @@ -2608,6 +2762,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any (Scheduler as any).include(DataHelperMixin); // @ts-ignore From c02ca1f6171ea7d539ffebc119de87b50edac3ed Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 10:45:05 +0200 Subject: [PATCH 11/17] fix: fix build --- .../m_compact_appointments_helper.ts | 3 +- .../js/__internal/scheduler/m_scheduler.ts | 152 ++++++++++++++---- .../view_model/common/get_compare_options.ts | 2 +- 3 files changed, 119 insertions(+), 38 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts index dcbf80a8e1b8..d7b142f78d6a 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -91,7 +91,7 @@ export class CompactAppointmentsHelper { getItemSettings, }; - workSpace.createDragBehaviorBase($element, $schedulerElement, options); + workSpace?.createDragBehaviorBase($element, $schedulerElement, options); }; } @@ -105,7 +105,6 @@ export class CompactAppointmentsHelper { private createCompactButton(template, options: CompactAppointmentOptions) { const $button = this.createCompactButtonElement(options); - // @ts-expect-error return this.instance._createComponent($button, Button, { type: 'default', width: options.width, diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index ec6f956a02d6..baea00b95faf 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1,4 +1,3 @@ -/* eslint-disable devextreme-custom/no-deferred */ import { triggerResizeEvent } from '@js/common/core/events/visibility_change'; import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; @@ -63,7 +62,7 @@ import type Scrollable from '@ts/ui/scroll_view/scrollable'; import { createA11yStatusContainer } from './a11y_status/a11y_status_render'; import { getA11yStatusText } from './a11y_status/a11y_status_text'; -import { AppointmentDragController } from './appointment_drag_controller'; +import { AppointmentDragController, type WorkSpaceDraggableOptions } from './appointment_drag_controller'; import type { AppointmentFormConfig } from './appointment_popup/form'; import { AppointmentForm } from './appointment_popup/form'; import { AppointmentPopup } from './appointment_popup/popup'; @@ -97,6 +96,8 @@ import type { AppointmentTooltipContextMenuEventArgs, AppointmentTooltipItem, CreateComponentFn, + DOMMetaData, + GroupBoundsOffset, MappedAppointmentFields, SafeAppointment, ScrollToGroupValuesOrOptions, ScrollToOptions, TargetedAppointment, @@ -117,14 +118,15 @@ import { ResourceManager } from './utils/resource_manager/resource_manager'; import type { GroupValues } from './utils/resource_manager/types'; import timeZoneUtils, { type TimezoneLabel } from './utils_time_zone'; import AppointmentLayoutManager from './view_model/appointments_layout_manager'; +import type { CollectorCSS, RealSize } from './view_model/generate_view_model/steps/add_geometry/types'; import { AppointmentDataSource } from './view_model/m_appointment_data_source'; -import type { AppointmentViewModelPlain, AppointmentItemViewModel } from './view_model/types'; +import type { AppointmentItemViewModel, AppointmentViewModelPlain, PanelName } from './view_model/types'; import SchedulerAgenda from './workspaces/agenda'; import SchedulerTimelineDay from './workspaces/timeline_day'; import SchedulerTimelineMonth from './workspaces/timeline_month'; import SchedulerTimelineWeek from './workspaces/timeline_week'; -import type SchedulerWorkSpace from './workspaces/work_space'; -import type { DroppableCellData, WorkspaceOptionsInternal } from './workspaces/work_space'; +import type ViewDataProvider from './workspaces/view_model/view_data_provider'; +import type { WorkspaceOptionsInternal } from './workspaces/work_space'; import SchedulerWorkSpaceDay from './workspaces/work_space_day'; import SchedulerWorkSpaceMonth from './workspaces/work_space_month'; import SchedulerWorkSpaceWeek from './workspaces/work_space_week'; @@ -199,6 +201,93 @@ const RECURRENCE_EDITING_MODE = { CANCEL: 'cancel', }; +type DroppableCellData = Pick; + +interface WorkSpaceCoordinates { + top: number; + left: number; + groupIndex?: number; +} + +interface WorkSpacePositionHelper { + getResizableStep: () => number; + getHorizontalMax: (groupIndex: number) => number; + getVerticalMax: (options: { + groupIndex: number; + isVirtualScrolling: boolean; + showAllDayPanel: boolean; + supportAllDayRow: boolean; + isGroupedAllDayPanel: boolean; + isVerticalGrouping: boolean; + }) => number; +} + +interface VirtualScrollingDispatcherLike { + cellCountInsideLeftVirtualCell: number; + cellCountInsideRightVirtualCell: number; + cellCountInsideTopVirtualRow: number; +} + +interface SchedulerWorkSpaceLike { + type: ViewType; + NAME?: string; + dragBehavior: AppointmentDragBehavior | null; + positionHelper: WorkSpacePositionHelper; + virtualScrollingDispatcher: VirtualScrollingDispatcherLike; + viewDataProvider: ViewDataProvider; + option: (name: string | Record, value?: unknown) => unknown; + getDateRange: () => Date[]; + getCellFromDragTarget: ($dragTarget: dxElementWrapper) => dxElementWrapper | null; + renderAgendaLayout?: (viewModel: AppointmentViewModelPlain[]) => void; + calculateEndDate: (startDate: Date) => Date; + updateScrollPosition: (date: Date, groupValues: GroupValues, inAllDayRow: boolean) => void; + supportAllDayRow: () => boolean; + getAllDayContainer: () => dxElementWrapper | null; + getFixedContainer: () => dxElementWrapper; + getDOMElementsMetaData: () => DOMMetaData; + isVerticalGroupedWorkSpace: () => boolean; + isVirtualScrolling: () => boolean; + isGroupedByDate: () => boolean; + needRecalculateResizableArea: () => boolean; + getHeaderDate: () => Date; + updateHeaderEmptyCellWidth: () => void; + initDragBehavior: (scheduler: unknown) => void; + attachTablesEvents: () => void; + getWorkArea: () => dxElementWrapper; + $element: () => dxElementWrapper; + renderCurrentDateTimeLineAndShader: () => void; + _dispose: () => void; + _dimensionChanged: () => void; + getScrollable: () => Scrollable; + getScrollableContainer: () => dxElementWrapper; + getCellData: ($cell: dxElementWrapper) => DroppableCellData; + getCellWidth: () => number; + getCellHeight: () => number; + getGroupCount: () => number; + getGroupBounds: (coordinates: WorkSpaceCoordinates) => GroupBoundsOffset | undefined; + getPanelDOMSize: (panelName: PanelName) => RealSize; + getCollectorDimension: (isCompactCollector: boolean, panelName: PanelName) => CollectorCSS; + getAgendaVerticalStepHeight: () => number; + createDragBehaviorBase: ( + targetElement: dxElementWrapper, + rootElement: dxElementWrapper, + options: Record, + ) => void; + removeDroppableCellClass: () => void; + keepOriginalHours: () => boolean; + getDataByDroppableCell: () => DroppableCellData; + getStartViewDate: () => Date | undefined; + getEndViewDate: () => Date; + scrollTo: ( + date: Date, + groupValuesOrOptions?: ScrollToGroupValuesOrOptions, + allDay?: boolean, + throwWarning?: boolean, + align?: 'start' | 'center', + ) => void; + focus: () => void; +} + type SchedulerActionCancel = boolean | PromiseLike | DeferredObj; type SchedulerActionOptions = { @@ -280,15 +369,14 @@ type AppointmentsEditingOptions = Partial<{ }>; interface SchedulerAppointmentsHost { - option(options: Record): unknown; - option(name: string, value?: unknown): unknown; - repaint(): void; - $element(): dxElementWrapper; - focus(): void; - moveAppointmentBack(dragEvent?: SchedulerDragEvent | null): void; - updateResizableArea(): void; - renderDragClone(...args: unknown[]): dxElementWrapper; - getAppointmentData(...args: unknown[]): unknown; + option: ((options: Record) => unknown) & ((name: string, value?: unknown) => unknown); + repaint: () => void; + $element: () => dxElementWrapper; + focus: () => void; + moveAppointmentBack: (dragEvent?: SchedulerDragEvent | null) => void; + updateResizableArea: () => void; + renderDragClone: (...args: unknown[]) => dxElementWrapper; + getAppointmentData: (...args: unknown[]) => unknown; } class Scheduler extends SchedulerOptionsBaseWidget { @@ -306,7 +394,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { // TODO: used externally in m_appointment_drag_behavior.ts, // m_subscribes.ts, workspaces/work_space.ts - _workSpace!: SchedulerWorkSpace; + _workSpace!: SchedulerWorkSpaceLike; private header?: SchedulerHeader; @@ -321,6 +409,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { declare _dataSource: DataSource | undefined; // TODO: used externally in workspaces/work_space.ts + // eslint-disable-next-line @typescript-eslint/naming-convention -- inherited Widget API declare _createComponent: CreateComponentFn; // TODO: used externally in m_subscribes.ts, @@ -830,7 +919,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const result: DeferredObj = Deferred(); if (this._dataSource) { - void this._dataSource.load().then(() => { + this._dataSource.load().then(() => { hideLoading().catch(noop); this._fireContentReadyAction(result); @@ -1121,7 +1210,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.appointmentDataSource.cleanState(); if (this.isAgenda()) { - // @ts-expect-error renderAgendaLayout exists only on agenda workspace this._workSpace.renderAgendaLayout?.(viewModel); } } @@ -1349,7 +1437,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { resourceManager: this.resourceManager, firstDayOfWeek: this.getFirstDayOfWeek(), startDayHour: this.option('startDayHour') ?? 0, - // @ts-expect-error createComponent: this._createComponent.bind(this), getCalculatedEndDate: (startDateWithStartHour: Date): Date => ( this._workSpace.calculateEndDate(startDateWithStartHour) @@ -1362,7 +1449,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { createAppointmentPopup(form: AppointmentForm): AppointmentPopup { const scheduler = { getElement: (): dxElementWrapper => this.$element(), - // @ts-expect-error createComponent: this._createComponent.bind(this), focus: (): void => { this.focus(); }, @@ -1415,8 +1501,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { private getAppointmentTooltipOptions(): AppointmentTooltipOptions { return { - // @ts-expect-error _createComponent is not defined in ts - createComponent: this._createComponent.bind(this), + createComponent: this._createComponent.bind(this) as AppointmentTooltipOptions['createComponent'], container: this.$element(), getScrollableContainer: this.getWorkSpaceScrollableContainer.bind(this), addDefaultTemplates: this._templateManager.addDefaultTemplates.bind(this._templateManager), @@ -1562,7 +1647,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (isHeaderShown) { const $header = $('
').appendTo(this.mainContainer); const headerOptions = this.headerConfig(); - // @ts-expect-error this.header = this._createComponent($header, SchedulerHeader, headerOptions); } } @@ -1620,7 +1704,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { ), getDOMElementsMetaData: () => this._workSpace?.getDOMElementsMetaData(), getViewDataProvider: () => this._workSpace?.viewDataProvider, - // @ts-expect-error protected method isVerticalGroupedWorkSpace: () => this._workSpace.isVerticalGroupedWorkSpace(), isDateAndTimeView: () => isDateAndTimeView(this._workSpace.type), onContentReady: () => { @@ -1645,7 +1728,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.recalculateWorkspace(); if (currentViewOptions.startDate) { - // @ts-expect-error protected method this.updateOption('header', 'currentDate', this._workSpace.getHeaderDate()); } } @@ -1665,14 +1747,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { const workSpaceComponent = VIEWS_CONFIG[currentViewType].workSpace; const workSpaceConfig = this.workSpaceConfig(this.currentView); // @ts-expect-error - this._workSpace = this._createComponent($workSpace, workSpaceComponent, workSpaceConfig); + this._workSpace = this._createComponent($workSpace, workSpaceComponent, workSpaceConfig) as SchedulerWorkSpaceLike; if (!this.option('_newAppointments')) { if (this.allowDragging()) { this._workSpace.initDragBehavior(this); } } - // @ts-expect-error protected method this._workSpace.attachTablesEvents(); this._workSpace.getWorkArea().append(this._appointments.$element()); } @@ -1682,7 +1763,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const workSpaceConfig = this.workSpaceConfig(this.currentView); const workSpaceComponent = VIEWS_CONFIG.agenda.workSpace; // @ts-expect-error - this._workSpace = this._createComponent($workSpace, workSpaceComponent, workSpaceConfig); + this._workSpace = this._createComponent($workSpace, workSpaceComponent, workSpaceConfig) as SchedulerWorkSpaceLike; this._workSpace.getWorkArea().append(this._appointments.$element()); } @@ -1691,7 +1772,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.workSpaceRecalculation = new Deferred(); triggerResizeEvent(this._workSpace.$element()); this.waitAsyncTemplate(() => { - // @ts-expect-error protected method this._workSpace.renderCurrentDateTimeLineAndShader(); }); } @@ -1757,7 +1837,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { getHeaderHeight: (): number => utils.DOM.getHeaderHeight(this.header), onScrollEnd: (): void => { if (!this.option('_newAppointments')) { - (this._appointments as AppointmentCollection).updateResizableArea(); + (this._appointments as unknown as AppointmentCollection).updateResizableArea(); } }, onInitialized: (e) => { @@ -1765,7 +1845,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.appointmentDragController.createWorkSpaceDraggable( $(e.element), { - getAppointmentData: this._appointments.getAppointmentData.bind(this._appointments), + getAppointmentData: this._appointments.getAppointmentData.bind( + this._appointments, + ) as WorkSpaceDraggableOptions['getAppointmentData'], }, ); } @@ -1869,7 +1951,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace.getScrollableContainer(); } - getWorkSpace(): SchedulerWorkSpace | undefined { + getWorkSpace(): SchedulerWorkSpaceLike { return this._workSpace; } @@ -2268,13 +2350,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { } catch (err: unknown) { performFailAction(err instanceof Error ? err : undefined); this.updatingAppointments.delete(target); - return Deferred().resolve().promise(); + return Deferred().resolve().promise(); } } performFailAction(); this.updatingAppointments.delete(target); - return Deferred().resolve().promise(); + return Deferred().resolve().promise(); }); } @@ -2330,7 +2412,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } getAppointmentsInstance(): Appointments | AppointmentCollection { - return this._appointments as Appointments | AppointmentCollection; + return this._appointments as unknown as Appointments | AppointmentCollection; } getLayoutManager(): AppointmentLayoutManager { @@ -2726,7 +2808,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private validateKeyFieldIfAgendaExist(): void { - if (!this.appointmentDataSource.isDataSourceInit) { + if (!this.appointmentDataSource?.isDataSourceInit) { return; } 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 6fbd9b9b0391..f6c0a4e628fe 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 @@ -6,7 +6,7 @@ export const getCompareOptions = ( schedulerStore: Scheduler, ): CompareOptions => { const workspace = schedulerStore.getWorkSpace(); - const dateRange = workspace.getDateRange() as Date[]; + const dateRange = workspace.getDateRange(); const compareOptions = { startDayHour: schedulerStore.getViewOption('startDayHour'), endDayHour: schedulerStore.getViewOption('endDayHour'), From c5340fb7f7ba50ea5b105c55006bf79a7e346278 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 12:23:54 +0200 Subject: [PATCH 12/17] fix: fix tests --- .../js/__internal/scheduler/m_scheduler.ts | 54 +++++++++++++------ .../scheduler/workspaces/work_space.ts | 2 + 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index baea00b95faf..5f4e75c0258e 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -377,6 +377,17 @@ interface SchedulerAppointmentsHost { updateResizableArea: () => void; renderDragClone: (...args: unknown[]) => dxElementWrapper; getAppointmentData: (...args: unknown[]) => unknown; + // eslint-disable-next-line @typescript-eslint/naming-convention + _createComponent: CreateComponentFn; + _renderItem: (index: number, item: unknown, container?: dxElementWrapper) => dxElementWrapper; + // eslint-disable-next-line @typescript-eslint/naming-convention + _getItemData: (element: dxElementWrapper) => unknown; + notifyObserver: (subject: string, args?: unknown) => void; + invoke: (funcName: string, ...args: unknown[]) => unknown; + // eslint-disable-next-line @typescript-eslint/naming-convention + _setDragSourceAppointment: (appointment: unknown, settings: unknown) => void; + // eslint-disable-next-line @typescript-eslint/naming-convention + _removeDragSourceClassFromDraggedAppointment: () => void; } class Scheduler extends SchedulerOptionsBaseWidget { @@ -481,7 +492,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const whenLoaded = this.postponedOperations.add('loadResources', () => { const groups = this.getViewOption('groups'); - return when(this.resourceManager.loadGroupResources(groups, forceReload)); + return fromPromise(this.resourceManager.loadGroupResources(groups, forceReload)); }, undefined); const resolveCallbacks: DeferredObj = Deferred(); @@ -507,6 +518,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { case 'firstDayOfWeek': this.updateOption('workSpace', name, value); this.updateOption('header', name, value); + this.updateOption('header', 'startViewDate', this.getStartViewDate()); this.createAppointmentPopupForm(); break; case 'currentDate': { @@ -527,7 +539,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.postponeResourceLoading().done(() => { if (this._dataSource) { - this.appointmentDataSource.setDataSource(this._dataSource); + if (this.appointmentDataSource) { + this.appointmentDataSource.setDataSource(this._dataSource); + } else { + this.createAppointmentDataSource(); + } } this.setRemoteFilterIfNeeded(); this.updateOption('workSpace', 'showAllDayPanel', this.option('showAllDayPanel')); @@ -932,7 +948,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { showLoading({ container: this.$element().get(0), position: { - of: this.$element().get(0), + // @ts-expect-error + of: this.$element(), }, }).catch(noop); } @@ -1207,7 +1224,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { : []; this._appointments.option('items', viewModel); - this.appointmentDataSource.cleanState(); + this.appointmentDataSource?.cleanState(); if (this.isAgenda()) { this._workSpace.renderAgendaLayout?.(viewModel); @@ -1660,7 +1677,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { min: this.getViewOption('min'), max: this.getViewOption('max'), indicatorTime: this.option('indicatorTime'), - startViewDate: this.getStartViewDate() ?? new Date(), + startViewDate: this.getStartViewDate() ?? this.getViewOption('currentDate'), tabIndex: this.option('tabIndex'), focusStateEnabled: this.option('focusStateEnabled'), useDropDownViewSwitcher: this.option('useDropDownViewSwitcher'), @@ -1727,6 +1744,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { } this.recalculateWorkspace(); + + if (this.header) { + const startViewDate = this.getStartViewDate(); + if (startViewDate) { + this.updateOption('header', 'startViewDate', startViewDate); + } + } + if (currentViewOptions.startDate) { this.updateOption('header', 'currentDate', this._workSpace.getHeaderDate()); } @@ -1777,7 +1802,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { } private workSpaceConfig(currentViewOptions: NormalizedView): WorkspaceOptionsInternal { - const cellDuration = this.option('cellDuration'); const scrolling = this.getViewOption('scrolling'); const isVirtualScrolling = scrolling.mode === 'virtual'; const horizontalVirtualScrollingAllowed = isVirtualScrolling @@ -1805,7 +1829,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { focusStateEnabled: Boolean(this.option('focusStateEnabled')), showAllDayPanel: this.option('showAllDayPanel'), showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'), - indicatorTime: this.option('indicatorTime') ?? new Date(), + indicatorTime: this.option('indicatorTime'), indicatorUpdateInterval: this.option('indicatorUpdateInterval'), shadeUntilCurrentTime: this.option('shadeUntilCurrentTime'), crossScrollingEnabled, @@ -1858,11 +1882,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { } }, rtlEnabled: Boolean(this.option('rtlEnabled')), - hoursInterval: cellDuration / 60, + cellDuration: this.option('cellDuration'), allDayExpanded: false, currentDate: this.getViewOption('currentDate'), }, currentViewOptions) as WorkspaceOptionsInternal; + workSpaceOptions.hoursInterval = workSpaceOptions.cellDuration / 60; + workSpaceOptions.notifyScheduler = this.notifyScheduler; workSpaceOptions.groups = this.resourceManager.groupResources(); workSpaceOptions.onCellClick = this._createActionByOption('onCellClick') as (e: CellClickEvent) => void; @@ -1981,7 +2007,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateAppointmentCore( appointmentData, newAppointmentData, - ).finally(resolve).catch(noop); + ).then(resolve); }, false, undefined, @@ -2585,13 +2611,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { ): void { if (appointment) { const settings = utils.dataAccessors.getAppointmentSettings(element); - if (!settings) { - return; - } const appointmentConfig = { itemData: targetedAppointment ?? appointment, - groupIndex: settings.groupIndex ?? 0, + groupIndex: settings?.groupIndex ?? 0, }; const info: AppointmentTooltipItem = { @@ -2625,14 +2648,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (this.appointmentTooltip.isShownForTarget(target)) { this.hideAppointmentTooltip(); } else { - this.processActionResult({ ...arg, cancel: arg.cancel ?? false }, (canceled) => { + // @ts-expect-error + this.processActionResult(arg, (canceled) => { if (!canceled) { this.appointmentTooltip.show(target, data, { ...this.getExtraAppointmentTooltipOptions(), ...options, }); } - }).catch(noop); + }); } } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts index 6e839a9a16fe..6a04f340bcd2 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/work_space.ts @@ -319,6 +319,7 @@ export interface WorkspaceOptionsInternal extends WidgetProperties void) | undefined; onCellContextMenu: ((e: CellContextMenuEvent) => void) | undefined; currentDate: Date; + cellDuration: number; hoursInterval: number; allDayExpanded: boolean; @@ -2620,6 +2621,7 @@ class SchedulerWorkSpace extends Widget { endDayHour: 24, viewOffset: 0, hoursInterval: 0.5, + cellDuration: 30, activeStateEnabled: true, hoverStateEnabled: true, groups: [], From bde4f6a5b8df3ca5d509eadfa7afbc2a9aa3f718 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 13:11:25 +0200 Subject: [PATCH 13/17] fix: await appointment addition in tooltip test --- .../__internal/scheduler/__tests__/appointment_tooltip.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts index f4d646cd3a42..9d3de292ac14 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointment_tooltip.test.ts @@ -438,7 +438,7 @@ describe.each([ const initialTarget = POM.tooltip.target; - scheduler.addAppointment({ + await scheduler.addAppointment({ text: 'New Apt', startDate: new Date(2017, 4, 20, 9, 30), endDate: new Date(2017, 4, 20, 10, 30), From d900cd8b9888d253efe2c010497ba48158047b7c Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 14:57:13 +0200 Subject: [PATCH 14/17] fix: fix tests related with deferred scrollto --- .../js/__internal/scheduler/m_scheduler.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 5f4e75c0258e..ce46e174342c 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -935,11 +935,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { const result: DeferredObj = Deferred(); if (this._dataSource) { - this._dataSource.load().then(() => { + when(fromPromise(this._dataSource.load())).done(() => { hideLoading().catch(noop); this._fireContentReadyAction(result); - }).catch(() => { + }).fail(() => { hideLoading().catch(noop); result.reject(); }); @@ -968,7 +968,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { result?.resolve(); }; - if (this.workSpaceRecalculation?.state() === 'pending') { + if (isDeferred(this.workSpaceRecalculation)) { this.workSpaceRecalculation.done(() => { fireContentReady(); }); @@ -2113,11 +2113,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { if (isPopupEditing) { this.appointmentPopup.show(singleRawAppointment, { - onSave: (newAppointment) => ( - this.updateAppointment(rawAppointment, appointment.source) - .then(() => this.addAppointment(newAppointment as SafeAppointment)) - .then(() => { this.scrollToAppointment(newAppointment as SafeAppointment); }) - ), + onSave: (newAppointment) => { + const saveResult = when( + this.updateAppointment(rawAppointment, appointment.source) + .then(() => this.addAppointment(newAppointment as SafeAppointment)), + ).done(() => { this.scrollToAppointment(newAppointment as SafeAppointment); }); + + return saveResult.promise(); + }, title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly: Boolean(appointment.source) && appointment.disabled, }); @@ -2542,10 +2545,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { delete this.editAppointmentData; // TODO if (this.editing.allowAdding) { this.appointmentPopup.show(appointmentData, { - onSave: (appointment) => ( - this.addAppointment(appointment as SafeAppointment) - .then(() => { this.scrollToAppointment(appointment as SafeAppointment); }) - ), + onSave: (appointment) => { + const saveResult = when(this.addAppointment(appointment as SafeAppointment)) + .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }); + + return saveResult.promise(); + }, title: messageLocalization.format('dxScheduler-newPopupTitle'), readOnly: false, }); @@ -2570,10 +2575,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { const readOnly = isDisabled || !this.editing.allowUpdating; this.appointmentPopup.show(appointmentData, { - onSave: (appointment) => ( - this.updateAppointment(appointmentData, appointment as SafeAppointment) - .then(() => { this.scrollToAppointment(appointment as SafeAppointment); }) - ), + onSave: (appointment) => { + const saveResult = when(this.updateAppointment(appointmentData, appointment as SafeAppointment)) + .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }); + + return saveResult.promise(); + }, title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly, }); From 0eb95057f97c1035ff525c1d9f8b99b2cd869fcf Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 15:34:06 +0200 Subject: [PATCH 15/17] fix; try to fix tests --- .../js/__internal/scheduler/m_scheduler.ts | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index ce46e174342c..69c74ec8c8db 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -935,10 +935,12 @@ class Scheduler extends SchedulerOptionsBaseWidget { const result: DeferredObj = Deferred(); if (this._dataSource) { - when(fromPromise(this._dataSource.load())).done(() => { + // @ts-ignore + this._dataSource.load().done(() => { hideLoading().catch(noop); this._fireContentReadyAction(result); + // @ts-ignore }).fail(() => { hideLoading().catch(noop); result.reject(); @@ -968,8 +970,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { result?.resolve(); }; - if (isDeferred(this.workSpaceRecalculation)) { - this.workSpaceRecalculation.done(() => { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (this.workSpaceRecalculation) { + this.workSpaceRecalculation?.done(() => { fireContentReady(); }); } else { @@ -2112,14 +2116,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { } if (isPopupEditing) { + // @ts-ignore this.appointmentPopup.show(singleRawAppointment, { + // @ts-ignore onSave: (newAppointment) => { - const saveResult = when( - this.updateAppointment(rawAppointment, appointment.source) - .then(() => this.addAppointment(newAppointment as SafeAppointment)), - ).done(() => { this.scrollToAppointment(newAppointment as SafeAppointment); }); - - return saveResult.promise(); + // @ts-ignore + this.updateAppointment(rawAppointment, appointment.source); + // @ts-ignore + return when(this.addAppointment(newAppointment as SafeAppointment)) + .done(() => { this.scrollToAppointment(newAppointment as SafeAppointment); }); }, title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly: Boolean(appointment.source) && appointment.disabled, @@ -2544,13 +2549,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { delete this.editAppointmentData; // TODO if (this.editing.allowAdding) { + // @ts-ignore this.appointmentPopup.show(appointmentData, { - onSave: (appointment) => { - const saveResult = when(this.addAppointment(appointment as SafeAppointment)) - .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }); - - return saveResult.promise(); - }, + // @ts-ignore + onSave: (appointment) => ( + // @ts-ignore + when(this.addAppointment(appointment as SafeAppointment)) + .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }) + ), title: messageLocalization.format('dxScheduler-newPopupTitle'), readOnly: false, }); @@ -2574,13 +2580,14 @@ class Scheduler extends SchedulerOptionsBaseWidget { const isDisabled = Boolean(adapter.source) && adapter.disabled; const readOnly = isDisabled || !this.editing.allowUpdating; + // @ts-ignore this.appointmentPopup.show(appointmentData, { - onSave: (appointment) => { - const saveResult = when(this.updateAppointment(appointmentData, appointment as SafeAppointment)) - .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }); - - return saveResult.promise(); - }, + // @ts-ignore + onSave: (appointment) => ( + // @ts-ignore + when(this.updateAppointment(appointmentData, appointment as SafeAppointment)) + .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }) + ), title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly, }); From 208022dd38bc0fa476e306e92cb57d78ed339980 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 16:03:18 +0200 Subject: [PATCH 16/17] fix; try to fix tests --- .../js/__internal/scheduler/m_scheduler.ts | 149 ++++++++---------- 1 file changed, 63 insertions(+), 86 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 69c74ec8c8db..904b86b2e523 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -2011,7 +2011,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.updateAppointmentCore( appointmentData, newAppointmentData, - ).then(resolve); + // @ts-ignore + ).always(resolve); }, false, undefined, @@ -2022,15 +2023,16 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } + // @ts-ignore checkRecurringAppointment( - rawAppointment: SafeAppointment, - singleAppointment: SafeAppointment, - exceptionDate: Date, - callback: () => void, - isDeleted: boolean, - isPopupEditing?: boolean, - dragEvent?: SchedulerDragEvent | null, - recurrenceEditMode?: RecurrenceEditMode, + rawAppointment, + singleAppointment, + exceptionDate, + callback, + isDeleted, + isPopupEditing?, + dragEvent?, + recurrenceEditMode?, onCancel?: () => void, ): void { const recurrenceRule = this._dataAccessors.get('recurrenceRule', rawAppointment); @@ -2051,7 +2053,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { singleAppointment, exceptionDate, isDeleted, - Boolean(isPopupEditing), + isPopupEditing, dragEvent, ); break; @@ -2062,20 +2064,16 @@ class Scheduler extends SchedulerOptionsBaseWidget { } this.showRecurrenceChangeConfirm(isDeleted) .done((editingMode) => { - if (editingMode === RECURRENCE_EDITING_MODE.SERIES) { - callback(); - } - - if (editingMode === RECURRENCE_EDITING_MODE.OCCURRENCE) { - this.excludeAppointmentFromSeries( - rawAppointment, - singleAppointment, - exceptionDate, - isDeleted, - Boolean(isPopupEditing), - dragEvent, - ); - } + editingMode === RECURRENCE_EDITING_MODE.SERIES && callback(); + + editingMode === RECURRENCE_EDITING_MODE.OCCURRENCE && this.excludeAppointmentFromSeries( + rawAppointment, + singleAppointment, + exceptionDate, + isDeleted, + isPopupEditing, + dragEvent, + ); }) .fail(() => { if (this.option('_newAppointments')) { @@ -2331,24 +2329,20 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace.getDataByDroppableCell(); } - updateAppointmentCore( - target: SafeAppointment, - rawAppointment: SafeAppointment, - onUpdatePrevented?: () => void, - dragEvent?: SchedulerDragEvent | null, - ): Promise { + // @ts-ignore + updateAppointmentCore(target, rawAppointment, onUpdatePrevented?, dragEvent?) { const updatingOptions = { newData: rawAppointment, oldData: extend({}, target), cancel: false, }; - const performFailAction = function performFailAction(this: Scheduler, err?: Error): void { + const performFailAction = function (err?) { if (onUpdatePrevented) { onUpdatePrevented.call(this); } - if (err?.name === 'Error') { + if (err && err.name === 'Error') { throw err; } }.bind(this); @@ -2356,41 +2350,44 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.actions[StoreEventNames.UPDATING](updatingOptions); if (dragEvent && !isDeferred(dragEvent.cancel)) { - dragEvent.cancel = Deferred(); + // @ts-expect-error + dragEvent.cancel = new Deferred(); } if (isPromise(updatingOptions.cancel) && (dragEvent || this.option('_newAppointments'))) { this.updatingAppointments.add(target); } - return this.processActionResult(updatingOptions, (canceled) => { + return this.processActionResult(updatingOptions, function (canceled) { + // @ts-expect-error + let deferred = new Deferred(); + if (!canceled) { this.expandAllDayPanel(rawAppointment); try { - return this.appointmentDataSource + deferred = this.appointmentDataSource .update(target, rawAppointment) .done(() => { dragEvent?.cancel.resolve(false); }) .always((storeAppointment) => { this.updatingAppointments.delete(target); - if (isDefined(storeAppointment)) { - this.onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment); - } + this.onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment); }) - .fail(() => performFailAction()) - .then(() => undefined); - } catch (err: unknown) { - performFailAction(err instanceof Error ? err : undefined); + .fail(() => performFailAction()); + } catch (err) { + performFailAction(err); this.updatingAppointments.delete(target); - return Deferred().resolve().promise(); + deferred.resolve(); } + } else { + performFailAction(); + this.updatingAppointments.delete(target); + deferred.resolve(); } - performFailAction(); - this.updatingAppointments.delete(target); - return Deferred().resolve().promise(); + return deferred.promise(); }); } @@ -2527,67 +2524,47 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.showAppointmentPopup(resultAppointment, true); } - showAppointmentPopup( - rawAppointment?: SafeAppointment, - createNewAppointment?: boolean, - rawTargetedAppointment?: SafeAppointment | TargetedAppointment, - ): void { - const newRawTargetedAppointment = rawTargetedAppointment - ? { ...rawTargetedAppointment } - : {} as SafeAppointment; - if (rawTargetedAppointment) { + // @ts-ignore + showAppointmentPopup(rawAppointment?, createNewAppointment?, rawTargetedAppointment?) { + const newRawTargetedAppointment = { ...rawTargetedAppointment }; + if (newRawTargetedAppointment) { delete newRawTargetedAppointment.displayStartDate; delete newRawTargetedAppointment.displayEndDate; } + const newTargetedAppointment = extend({}, rawAppointment, newRawTargetedAppointment); + const isCreateAppointment = createNewAppointment ?? isEmptyObject(rawAppointment); - if (isCreateAppointment) { - const appointmentData = isEmptyObject(rawAppointment) - ? this.createPopupAppointment() - : rawAppointment as SafeAppointment; + if (isEmptyObject(rawAppointment)) { + rawAppointment = this.createPopupAppointment(); + } + if (isCreateAppointment) { delete this.editAppointmentData; // TODO if (this.editing.allowAdding) { - // @ts-ignore - this.appointmentPopup.show(appointmentData, { + this.appointmentPopup.show(rawAppointment, { // @ts-ignore - onSave: (appointment) => ( - // @ts-ignore - when(this.addAppointment(appointment as SafeAppointment)) - .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }) - ), + onSave: (appointment) => when(this.addAppointment(appointment)) + .done(() => { this.scrollToAppointment(appointment); }), title: messageLocalization.format('dxScheduler-newPopupTitle'), readOnly: false, }); } } else { - const appointmentData = rawAppointment as SafeAppointment; - const newTargetedAppointment = extend( - {}, - appointmentData, - newRawTargetedAppointment, - ) as SafeAppointment; - const startDate = this._dataAccessors.get( - 'startDate', - Object.keys(newRawTargetedAppointment).length ? newRawTargetedAppointment : appointmentData, - ); + const startDate = this._dataAccessors.get('startDate', newRawTargetedAppointment || rawAppointment); - this.checkRecurringAppointment(appointmentData, newTargetedAppointment, startDate, () => { - this.editAppointmentData = appointmentData; // TODO + this.checkRecurringAppointment(rawAppointment, newTargetedAppointment, startDate, () => { + this.editAppointmentData = rawAppointment; // TODO - const adapter = new AppointmentAdapter(appointmentData, this._dataAccessors); + const adapter = new AppointmentAdapter(rawAppointment, this._dataAccessors); const isDisabled = Boolean(adapter.source) && adapter.disabled; const readOnly = isDisabled || !this.editing.allowUpdating; - // @ts-ignore - this.appointmentPopup.show(appointmentData, { + this.appointmentPopup.show(rawAppointment, { // @ts-ignore - onSave: (appointment) => ( - // @ts-ignore - when(this.updateAppointment(appointmentData, appointment as SafeAppointment)) - .done(() => { this.scrollToAppointment(appointment as SafeAppointment); }) - ), + onSave: (appointment) => when(this.updateAppointment(rawAppointment, appointment)) + .done(() => { this.scrollToAppointment(appointment); }), title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly, }); From 1d1aab6babcd2b2c5f191c2077ab48359a153e62 Mon Sep 17 00:00:00 2001 From: Sergei Burkatskii Date: Wed, 24 Jun 2026 16:39:53 +0200 Subject: [PATCH 17/17] refactor: refactor after fixes --- .../js/__internal/scheduler/m_scheduler.ts | 165 ++++++++++-------- 1 file changed, 95 insertions(+), 70 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 904b86b2e523..a6e6000861e2 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -935,12 +935,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { const result: DeferredObj = Deferred(); if (this._dataSource) { - // @ts-ignore - this._dataSource.load().done(() => { + when(fromPromise(this._dataSource.load())).done(() => { hideLoading().catch(noop); this._fireContentReadyAction(result); - // @ts-ignore }).fail(() => { hideLoading().catch(noop); result.reject(); @@ -970,10 +968,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { result?.resolve(); }; - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (this.workSpaceRecalculation) { - this.workSpaceRecalculation?.done(() => { + if (isDeferred(this.workSpaceRecalculation)) { + this.workSpaceRecalculation.done(() => { fireContentReady(); }); } else { @@ -1520,6 +1516,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { this._workSpace.updateScrollPosition(startDate, appointmentGroupValues, inAllDayRow); } + private saveAppointmentAndScroll( + saveAction: () => PromiseLike, + appointment: SafeAppointment, + ): PromiseLike { + return when(saveAction()).done(() => { + this.scrollToAppointment(appointment); + }).promise(); + } + private getAppointmentTooltipOptions(): AppointmentTooltipOptions { return { createComponent: this._createComponent.bind(this) as AppointmentTooltipOptions['createComponent'], @@ -2008,11 +2013,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { newAppointmentData, startDate, () => { - this.updateAppointmentCore( + when(fromPromise(this.updateAppointmentCore( appointmentData, newAppointmentData, - // @ts-ignore - ).always(resolve); + ))).always(resolve); }, false, undefined, @@ -2023,16 +2027,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { }); } - // @ts-ignore checkRecurringAppointment( - rawAppointment, - singleAppointment, - exceptionDate, - callback, - isDeleted, - isPopupEditing?, - dragEvent?, - recurrenceEditMode?, + rawAppointment: SafeAppointment, + singleAppointment: SafeAppointment, + exceptionDate: Date, + callback: () => void, + isDeleted: boolean, + isPopupEditing?: boolean, + dragEvent?: SchedulerDragEvent | null, + recurrenceEditMode?: RecurrenceEditMode, onCancel?: () => void, ): void { const recurrenceRule = this._dataAccessors.get('recurrenceRule', rawAppointment); @@ -2053,7 +2056,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { singleAppointment, exceptionDate, isDeleted, - isPopupEditing, + Boolean(isPopupEditing), dragEvent, ); break; @@ -2064,16 +2067,20 @@ class Scheduler extends SchedulerOptionsBaseWidget { } this.showRecurrenceChangeConfirm(isDeleted) .done((editingMode) => { - editingMode === RECURRENCE_EDITING_MODE.SERIES && callback(); - - editingMode === RECURRENCE_EDITING_MODE.OCCURRENCE && this.excludeAppointmentFromSeries( - rawAppointment, - singleAppointment, - exceptionDate, - isDeleted, - isPopupEditing, - dragEvent, - ); + if (editingMode === RECURRENCE_EDITING_MODE.SERIES) { + callback(); + } + + if (editingMode === RECURRENCE_EDITING_MODE.OCCURRENCE) { + this.excludeAppointmentFromSeries( + rawAppointment, + singleAppointment, + exceptionDate, + isDeleted, + Boolean(isPopupEditing), + dragEvent, + ); + } }) .fail(() => { if (this.option('_newAppointments')) { @@ -2114,15 +2121,13 @@ class Scheduler extends SchedulerOptionsBaseWidget { } if (isPopupEditing) { - // @ts-ignore this.appointmentPopup.show(singleRawAppointment, { - // @ts-ignore onSave: (newAppointment) => { - // @ts-ignore - this.updateAppointment(rawAppointment, appointment.source); - // @ts-ignore - return when(this.addAppointment(newAppointment as SafeAppointment)) - .done(() => { this.scrollToAppointment(newAppointment as SafeAppointment); }); + this.updateAppointment(rawAppointment, appointment.source).catch(noop); + return this.saveAppointmentAndScroll( + () => this.addAppointment(newAppointment as SafeAppointment), + newAppointment as SafeAppointment, + ); }, title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly: Boolean(appointment.source) && appointment.disabled, @@ -2329,20 +2334,24 @@ class Scheduler extends SchedulerOptionsBaseWidget { return this._workSpace.getDataByDroppableCell(); } - // @ts-ignore - updateAppointmentCore(target, rawAppointment, onUpdatePrevented?, dragEvent?) { + updateAppointmentCore( + target: SafeAppointment, + rawAppointment: SafeAppointment, + onUpdatePrevented?: () => void, + dragEvent?: SchedulerDragEvent | null, + ): Promise { const updatingOptions = { newData: rawAppointment, oldData: extend({}, target), cancel: false, }; - const performFailAction = function (err?) { + const performFailAction = function performFailAction(this: Scheduler, err?: Error): void { if (onUpdatePrevented) { onUpdatePrevented.call(this); } - if (err && err.name === 'Error') { + if (err?.name === 'Error') { throw err; } }.bind(this); @@ -2350,17 +2359,15 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.actions[StoreEventNames.UPDATING](updatingOptions); if (dragEvent && !isDeferred(dragEvent.cancel)) { - // @ts-expect-error - dragEvent.cancel = new Deferred(); + dragEvent.cancel = Deferred(); } if (isPromise(updatingOptions.cancel) && (dragEvent || this.option('_newAppointments'))) { this.updatingAppointments.add(target); } - return this.processActionResult(updatingOptions, function (canceled) { - // @ts-expect-error - let deferred = new Deferred(); + return this.processActionResult(updatingOptions, function onUpdate(this: Scheduler, canceled) { + let deferred: DeferredObj = Deferred(); if (!canceled) { this.expandAllDayPanel(rawAppointment); @@ -2369,15 +2376,18 @@ class Scheduler extends SchedulerOptionsBaseWidget { deferred = this.appointmentDataSource .update(target, rawAppointment) .done(() => { - dragEvent?.cancel.resolve(false); + dragEvent?.cancel?.resolve(false); }) .always((storeAppointment) => { this.updatingAppointments.delete(target); - this.onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment); + this.onDataPromiseCompleted( + StoreEventNames.UPDATED, + storeAppointment as SafeAppointment, + ); }) .fail(() => performFailAction()); - } catch (err) { - performFailAction(err); + } catch (err: unknown) { + performFailAction(err instanceof Error ? err : undefined); this.updatingAppointments.delete(target); deferred.resolve(); } @@ -2524,47 +2534,62 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.showAppointmentPopup(resultAppointment, true); } - // @ts-ignore - showAppointmentPopup(rawAppointment?, createNewAppointment?, rawTargetedAppointment?) { - const newRawTargetedAppointment = { ...rawTargetedAppointment }; - if (newRawTargetedAppointment) { + showAppointmentPopup( + rawAppointment?: SafeAppointment, + createNewAppointment?: boolean, + rawTargetedAppointment?: SafeAppointment | TargetedAppointment, + ): void { + const newRawTargetedAppointment: SafeAppointment = rawTargetedAppointment + ? { ...rawTargetedAppointment } + : {} as SafeAppointment; + if (rawTargetedAppointment) { delete newRawTargetedAppointment.displayStartDate; delete newRawTargetedAppointment.displayEndDate; } - const newTargetedAppointment = extend({}, rawAppointment, newRawTargetedAppointment); + const newTargetedAppointment = extend( + {}, + rawAppointment, + newRawTargetedAppointment, + ) as SafeAppointment; const isCreateAppointment = createNewAppointment ?? isEmptyObject(rawAppointment); - if (isEmptyObject(rawAppointment)) { - rawAppointment = this.createPopupAppointment(); + let appointment: SafeAppointment = rawAppointment ?? {} as SafeAppointment; + if (isEmptyObject(appointment)) { + appointment = this.createPopupAppointment(); } if (isCreateAppointment) { delete this.editAppointmentData; // TODO if (this.editing.allowAdding) { - this.appointmentPopup.show(rawAppointment, { - // @ts-ignore - onSave: (appointment) => when(this.addAppointment(appointment)) - .done(() => { this.scrollToAppointment(appointment); }), + this.appointmentPopup.show(appointment, { + onSave: (appointmentData) => this.saveAppointmentAndScroll( + () => this.addAppointment(appointmentData as SafeAppointment), + appointmentData as SafeAppointment, + ), title: messageLocalization.format('dxScheduler-newPopupTitle'), readOnly: false, }); } } else { - const startDate = this._dataAccessors.get('startDate', newRawTargetedAppointment || rawAppointment); + const startDate = this._dataAccessors.get( + 'startDate', + Object.keys(newRawTargetedAppointment).length ? newRawTargetedAppointment : appointment, + ); - this.checkRecurringAppointment(rawAppointment, newTargetedAppointment, startDate, () => { - this.editAppointmentData = rawAppointment; // TODO + this.checkRecurringAppointment(appointment, newTargetedAppointment, startDate, () => { + this.editAppointmentData = appointment; // TODO - const adapter = new AppointmentAdapter(rawAppointment, this._dataAccessors); + const adapter = new AppointmentAdapter(appointment, this._dataAccessors); const isDisabled = Boolean(adapter.source) && adapter.disabled; const readOnly = isDisabled || !this.editing.allowUpdating; - this.appointmentPopup.show(rawAppointment, { - // @ts-ignore - onSave: (appointment) => when(this.updateAppointment(rawAppointment, appointment)) - .done(() => { this.scrollToAppointment(appointment); }), + this.appointmentPopup.show(appointment, { + onSave: (appointmentData) => this.saveAppointmentAndScroll( + () => this.updateAppointment(appointment, appointmentData as SafeAppointment), + appointmentData as SafeAppointment, + ), title: messageLocalization.format('dxScheduler-editPopupTitle'), readOnly, }); @@ -2862,7 +2887,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { // eslint-disable-next-line @typescript-eslint/no-explicit-any (Scheduler as any).include(DataHelperMixin); -// @ts-ignore +// @ts-expect-error DataHelperMixin changes Scheduler prototype registerComponent('dxScheduler', Scheduler); export default Scheduler;