Skip to content

Scheduler - Refactor Appointments Collection - Resizing#34187

Open
bit-byte0 wants to merge 21 commits into
DevExpress:26_1from
bit-byte0:refactor/scheduler-appointments-resizing
Open

Scheduler - Refactor Appointments Collection - Resizing#34187
bit-byte0 wants to merge 21 commits into
DevExpress:26_1from
bit-byte0:refactor/scheduler-appointments-resizing

Conversation

@bit-byte0

@bit-byte0 bit-byte0 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What

Added appointment resizing (timed + all-day) to the new appointments collection behind the _newAppointments flag with Esc-to-cancel, focus handling, group-bounded drag, and rollback on cancel/failure

How

Extracted the resize math into small pure, unit-tested helpers; added opt-in onCancelByEsc to the Resizable widget; wired it into the new grid appointment with a thin scheduler.ts handler that computes the new dates (timed vs all-day) and updates the appointment, focusing it on resize-start and snapping it back if the update is cancelled/fails

Copilot AI review requested due to automatic review settings July 1, 2026 10:52
@bit-byte0 bit-byte0 requested review from a team as code owners July 1, 2026 10:52
@bit-byte0 bit-byte0 self-assigned this Jul 1, 2026
@bit-byte0 bit-byte0 added the 26_1 label Jul 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors Scheduler appointment resizing by moving resize calculations/config into dedicated helpers, wiring the “new appointments” rendering to use dxResizable, and adding Escape-to-cancel support at the internal Resizable level.

Changes:

  • Added onCancelByEsc + onResizeCancel flow to internal Resizable, including ESC key handling and cancel/restore logic.
  • Implemented new Scheduler appointment resizing pipeline (config generation, delta-time calculation, resized date range calculation) and integrated it into scheduler.ts.
  • Enabled resize handles for grid appointments in the “new appointments” implementation and added/updated Jest coverage for the new behavior.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/devextreme/js/__internal/ui/resizable/resizable.ts Adds ESC cancellation support and a new cancel action for Resizable.
packages/devextreme/js/__internal/ui/resizable/resizable.test.ts New unit tests validating ESC-cancel behavior and event firing.
packages/devextreme/js/__internal/scheduler/scheduler.ts Integrates new resizing logic/config into Scheduler and updates resize start/end handling.
packages/devextreme/js/__internal/scheduler/appointments_new/resizing/get_resized_dates.ts New helper to compute resized start/end dates with working-hours correction + TZ offset adjustments.
packages/devextreme/js/__internal/scheduler/appointments_new/resizing/get_resized_dates.test.ts Tests for start/end resize date calculations and handle-direction logic.
packages/devextreme/js/__internal/scheduler/appointments_new/resizing/get_resizable_config.ts New helper to generate Resizable rules (handles/step/min sizes) based on view model/workspace.
packages/devextreme/js/__internal/scheduler/appointments_new/resizing/get_resizable_config.test.ts Tests for resizable rule generation (vertical/horizontal/reduced/RTL/grouping).
packages/devextreme/js/__internal/scheduler/appointments_new/resizing/get_delta_time.ts New helper to compute resize delta-time from pixel deltas across view types.
packages/devextreme/js/__internal/scheduler/appointments_new/resizing/get_delta_time.test.ts Tests for delta-time calculation across view types and all-day/timeline behavior.
packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts Adds getResizableConfig hook and passes resize settings into GridAppointmentView.
packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts Updates test properties to include getResizableConfig.
packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts Exposes focusViewItem to support focusing the resized appointment.
packages/devextreme/js/__internal/scheduler/appointments_new/appointment/grid_appointment.ts Creates a Resizable instance for grid appointments when resizing is enabled.
packages/devextreme/js/__internal/scheduler/appointments_new/appointment/grid_appointment.test.ts Tests that resize handles appear/disappear based on allowResize.
packages/devextreme/js/__internal/scheduler/tests/appointments_resizing.test.ts Integration tests for resize handles rendering and focus-on-resize-start in Scheduler.

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts Outdated
Copilot AI review requested due to automatic review settings July 1, 2026 11:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.test.ts Outdated
Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts
Copilot AI review requested due to automatic review settings July 1, 2026 14:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts
Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts Outdated
Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts Outdated
Copilot AI review requested due to automatic review settings July 1, 2026 16:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comment on lines +90 to +94
roundStepValue?: boolean;

onCancelByEsc?: boolean;

onResizeCancel?: (e: Record<string, unknown>) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, Copilot is right here.

Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts
Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.test.ts Outdated
Copilot AI review requested due to automatic review settings July 1, 2026 22:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.

Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts
private onAppointmentResizeEnd(e: AppointmentResizeEvent): void {
const $element = $(e.element);
const settings = this._appointments
.getAppointmentSettings($element) as AppointmentItemViewModel;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

settings can return undefined here. We should add guard
if (!settings) {
return;
}

to avoid type error in runtime

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the guard

return handles.top;
};

const correctEndDateByDelta = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correctStartDateByDelta/correctEndDateByDelta — are they almost the same here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do look almost identical, but they actually work in opposite directions: correctStartDateByDelta moves the start of the appointment backward (subtracts the delta, walks days back, clamps to startDayHour), while correctEndDateByDelta moves the end forward (adds the delta, walks days ahead, clamps to endDayHour). Legacy kept them as two separate functions too. I did try to picture merging them into one, but you'd end up passing a bunch of "which direction?" flags, so I left them split on purpose

const adapter = new AppointmentAdapter(rawAppointment, this._dataAccessors);
const { startDateTimeZone, isRecurrent } = adapter;

if (!e.handles.top && !isRecurrent) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in old _getEndResizeAppointmentStartDate there was a guard check for appointment not being allDay. In new method we don't have it. Is it by a choice?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's on purpose. In the old code, all-day and regular appointments went through the same getDateRange, so _getEndResizeAppointmentStartDate had to check !isAllDay right there to skip all-day ones. Now the split happens earlier: onAppointmentResizeEnd sends all-day appointments to getResizedAllDayDateRange and regular ones to getResizedTimedDateRange, and this method only gets called from the regular (timed) path, so an all-day appointment never reaches it and that's why the !isAllDay check isn't needed here anymore

sjbur
sjbur previously approved these changes Jul 6, 2026
@bit-byte0 bit-byte0 enabled auto-merge July 7, 2026 09:40
@bit-byte0 bit-byte0 disabled auto-merge July 7, 2026 09:40
@@ -0,0 +1,111 @@
import {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's follow the main flow and place Jest tests into tests folder:
packages/devextreme/js/__internal/ui/resizable/__tests__/resizable.test.ts‎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to ui/resizable/__tests__/resizable.test.ts


const instances: Resizable[] = [];

const createResizable = (options: Partial<ResizableProperties> = {}): {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create corresponding ResizableModel in the following folder:
packages/devextreme/js/__internal/ui/__tests__/__mock__/model

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added ResizableModel

return { instance, $element, $handle };
};

const startResize = ($handle: ReturnType<typeof $>): void => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let every such method be a public method of ResizableModel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startResize / moveResize / endResize / pressEscape / isResizing / hasResizingClass are now public methods on ResizableModel, and the test drives everything through it

}

_isResizing(): boolean {
return this.$element().hasClass(RESIZABLE_RESIZING_CLASS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component state should not relate to the DOM state. We need to use business logic. For example we can use internal field
this._isResizing = false;
We can set it to true when starting resizing and to false when ending or cancelling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced the class-based check with an internal _isResizing boolean, set on resize start and cleared on end/cancel. The CSS class is now only presentational

});

if (this.option('onCancelByEsc')) {
eventsEngine.on(this.$element(), KEYDOWN_EVENT_NAME, this._keydownHandler.bind(this));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to subscribe the handler every time. The guard
if (this.option('onCancelByEsc'))
should work inside of the handler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the keydown handler is now always attached, and the cancelOnEscape check moved inside _keydownHandler


roundStepValue?: boolean;

onCancelByEsc?: boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can improve the name. The prefix on- uses for handlers only. We could name it cancelOnEscape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed

Comment on lines +90 to +94
roundStepValue?: boolean;

onCancelByEsc?: boolean;

onResizeCancel?: (e: Record<string, unknown>) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, Copilot is right here.


onCancelByEsc?: boolean;

onResizeCancel?: (e: Record<string, unknown>) => void;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see usings of onResizeCancel handler in the scheduler. Do you really need it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it originally was created to make the widget's event API "symmetric" (onResizeStart / onResizeEnd / onResizeCancel), mirroring how the drag side has events. It felt like the "proper" complete API. Anyways it was removed

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
case 'onResizeCancel':
this._renderActions();
break;
case 'onCancelByEsc':

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case should disappear after refactoring _attachEventHandlers() method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the keydown handler is now always attached, that _optionChanged case is gone

@bit-byte0 bit-byte0 Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small correction to my earlier reply: the detach/attach logic is gone as you asked, but I had to keep a bare no-op case cancelOnEscape: break; The optionChanged widget test (DevExpress.ui.widgets/optionChanged) requires every option to be handled in _optionChanged without falling through to super, otherwise it fails with "Option cancelOnEscape is not processed". So the case stays, but only as a no-op alongside area/step/etc

Copilot AI review requested due to automatic review settings July 7, 2026 12:03
@bit-byte0 bit-byte0 force-pushed the refactor/scheduler-appointments-resizing branch from 838d4ae to b86b556 Compare July 7, 2026 12:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts Outdated
Comment thread packages/devextreme/js/__internal/scheduler/scheduler.ts
Copilot AI review requested due to automatic review settings July 7, 2026 15:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
Copilot AI review requested due to automatic review settings July 7, 2026 17:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
Comment thread packages/devextreme/js/__internal/ui/__tests__/__mock__/model/resizable.ts Outdated
Comment thread packages/devextreme/js/__internal/ui/__tests__/__mock__/model/resizable.ts Outdated
Comment thread packages/devextreme/js/__internal/ui/__tests__/__mock__/model/resizable.ts Outdated
Comment thread packages/devextreme/js/__internal/ui/resizable/__tests__/resizable.test.ts Outdated
Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
});
});

eventsEngine.off(this.$element(), KEYDOWN_EVENT_NAME);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to detach events first? In which case there are already attached keydown events?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand we need it on repeated renders. _attachEventHandlers re-runs on invalidation (e.g. a handles option change). Drag handlers sit on the handle elements, which _clean() removes and recreates, so they never stack. The keydown handler sits on the root element, which survives re-renders. Without the off, each re-render adds another subscription. The QUnit Memory Leaks suite (eventsOnRefresh) caught exactly this in CI. Can move the unbind into _detachEventHandlers / _clean() instead if you prefer

Copilot AI review requested due to automatic review settings July 8, 2026 09:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Comment thread packages/devextreme/js/__internal/ui/resizable/resizable.ts
@bit-byte0 bit-byte0 requested a review from Raushen July 8, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants