Scheduler - Refactor Appointments Collection - Resizing#34187
Scheduler - Refactor Appointments Collection - Resizing#34187bit-byte0 wants to merge 21 commits into
Conversation
There was a problem hiding this comment.
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+onResizeCancelflow 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. |
| roundStepValue?: boolean; | ||
|
|
||
| onCancelByEsc?: boolean; | ||
|
|
||
| onResizeCancel?: (e: Record<string, unknown>) => void; |
| private onAppointmentResizeEnd(e: AppointmentResizeEvent): void { | ||
| const $element = $(e.element); | ||
| const settings = this._appointments | ||
| .getAppointmentSettings($element) as AppointmentItemViewModel; |
There was a problem hiding this comment.
settings can return undefined here. We should add guard
if (!settings) {
return;
}
to avoid type error in runtime
| return handles.top; | ||
| }; | ||
|
|
||
| const correctEndDateByDelta = ( |
There was a problem hiding this comment.
correctStartDateByDelta/correctEndDateByDelta — are they almost the same here?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
| @@ -0,0 +1,111 @@ | |||
| import { | |||
There was a problem hiding this comment.
Let's follow the main flow and place Jest tests into tests folder:
packages/devextreme/js/__internal/ui/resizable/__tests__/resizable.test.ts
There was a problem hiding this comment.
moved to ui/resizable/__tests__/resizable.test.ts
|
|
||
| const instances: Resizable[] = []; | ||
|
|
||
| const createResizable = (options: Partial<ResizableProperties> = {}): { |
There was a problem hiding this comment.
Please create corresponding ResizableModel in the following folder:
packages/devextreme/js/__internal/ui/__tests__/__mock__/model
There was a problem hiding this comment.
added ResizableModel
| return { instance, $element, $handle }; | ||
| }; | ||
|
|
||
| const startResize = ($handle: ReturnType<typeof $>): void => { |
There was a problem hiding this comment.
Let every such method be a public method of ResizableModel
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
We need to subscribe the handler every time. The guard
if (this.option('onCancelByEsc'))
should work inside of the handler.
There was a problem hiding this comment.
the keydown handler is now always attached, and the cancelOnEscape check moved inside _keydownHandler
|
|
||
| roundStepValue?: boolean; | ||
|
|
||
| onCancelByEsc?: boolean; |
There was a problem hiding this comment.
We can improve the name. The prefix on- uses for handlers only. We could name it cancelOnEscape.
| roundStepValue?: boolean; | ||
|
|
||
| onCancelByEsc?: boolean; | ||
|
|
||
| onResizeCancel?: (e: Record<string, unknown>) => void; |
|
|
||
| onCancelByEsc?: boolean; | ||
|
|
||
| onResizeCancel?: (e: Record<string, unknown>) => void; |
There was a problem hiding this comment.
I do not see usings of onResizeCancel handler in the scheduler. Do you really need it?
There was a problem hiding this comment.
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
| case 'onResizeCancel': | ||
| this._renderActions(); | ||
| break; | ||
| case 'onCancelByEsc': |
There was a problem hiding this comment.
This case should disappear after refactoring _attachEventHandlers() method.
There was a problem hiding this comment.
since the keydown handler is now always attached, that _optionChanged case is gone
There was a problem hiding this comment.
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
838d4ae to
b86b556
Compare
| }); | ||
| }); | ||
|
|
||
| eventsEngine.off(this.$element(), KEYDOWN_EVENT_NAME); |
There was a problem hiding this comment.
Do we need to detach events first? In which case there are already attached keydown events?
There was a problem hiding this comment.
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
What
Added appointment resizing (timed + all-day) to the new appointments collection behind the
_newAppointmentsflag with Esc-to-cancel, focus handling, group-bounded drag, and rollback on cancel/failureHow
Extracted the resize math into small pure, unit-tested helpers; added opt-in
onCancelByEscto theResizablewidget; wired it into the new grid appointment with a thinscheduler.tshandler 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