Skip to content

Commit 94e0ecb

Browse files
authored
Scheduler: T1308596 — "Uncaught TypeError: Cannot read properties of undefined (reading 'getBoundingClientRect')" is thrown when dragging an appointment while an onAppointmentUpdating promise is pending (#31572)
1 parent c973b69 commit 94e0ecb

File tree

4 files changed

+88
-2
lines changed

4 files changed

+88
-2
lines changed

e2e/testcafe-devextreme/tests/scheduler/common/dragAndDrop/dragEvents.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,48 @@ TEST_CASES.forEach(({ view, expectedToItemData }) => {
127127
await clearCallbackTesting();
128128
});
129129
});
130+
131+
test('Should block appointment dragging while onAppointmentUpdating Promise is pending (T1308596)', async (t) => {
132+
const scheduler = new Scheduler(SCHEDULER_SELECTOR);
133+
const appointment = scheduler.getAppointment('Test Appointment');
134+
135+
const targetCell1 = scheduler.getDateTableCell(18, 2);
136+
const targetCell2 = scheduler.getDateTableCell(18, 5);
137+
138+
const initialPosition = await appointment.element.boundingClientRect;
139+
140+
await t.dragToElement(appointment.element, targetCell1, { speed: 1 });
141+
await t.dragToElement(appointment.element, targetCell2, { speed: 1 });
142+
await t.dragToElement(appointment.element, targetCell2, { speed: 1 });
143+
await t.dragToElement(appointment.element, targetCell2, { speed: 1 });
144+
145+
await t.wait(6000);
146+
147+
const positionAfterPromiseResolved = await appointment.element.boundingClientRect;
148+
const cell1Position = await targetCell1.boundingClientRect;
149+
150+
await t
151+
.expect(positionAfterPromiseResolved.left)
152+
.notEql(initialPosition.left)
153+
.expect(positionAfterPromiseResolved.left)
154+
.eql(cell1Position.left);
155+
}).before(async () => {
156+
await createWidget('dxScheduler', {
157+
dataSource: [{
158+
text: 'Test Appointment',
159+
startDate: new Date(2023, 0, 2, 10, 0),
160+
endDate: new Date(2023, 0, 2, 11, 0),
161+
}],
162+
views: ['week'],
163+
currentView: 'week',
164+
currentDate: new Date(2023, 0, 2),
165+
height: 600,
166+
onAppointmentUpdating: (e) => {
167+
e.cancel = new Promise((resolve) => {
168+
setTimeout(() => {
169+
resolve(false);
170+
}, 5000);
171+
});
172+
},
173+
});
174+
});

packages/devextreme/js/__internal/scheduler/m_appointment_drag_behavior.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export default class AppointmentDragBehavior {
9696
e.itemData = this.getItemData(e.itemElement);
9797
e.itemSettings = this.getItemSettings(e.itemElement);
9898

99+
if (this.scheduler._isAppointmentBeingUpdated(e.itemData)) {
100+
e.cancel = true;
101+
return;
102+
}
103+
99104
appointmentDragging.onDragStart?.(e);
100105

101106
if (!e.cancel) {
@@ -106,6 +111,16 @@ export default class AppointmentDragBehavior {
106111

107112
createDragMoveHandler(options, appointmentDragging) {
108113
return (e) => {
114+
if (!this.appointmentInfo) {
115+
e.cancel = true;
116+
return;
117+
}
118+
119+
if (this.scheduler._isAppointmentBeingUpdated(this.appointmentInfo.appointment)) {
120+
e.cancel = true;
121+
return;
122+
}
123+
109124
appointmentDragging.onDragMove?.(e);
110125

111126
if (!e.cancel) {
@@ -116,6 +131,11 @@ export default class AppointmentDragBehavior {
116131

117132
createDragEndHandler(options, appointmentDragging) {
118133
return (e) => {
134+
if (!this.appointmentInfo) {
135+
e.cancel = true;
136+
return;
137+
}
138+
119139
const updatedData = this.appointments.invoke('getUpdatedData', e.itemData);
120140

121141
this.appointmentInfo = null;

packages/devextreme/js/__internal/scheduler/m_scheduler.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ class Scheduler extends SchedulerOptionsBaseWidget {
199199

200200
_asyncTemplatesTimers!: any[];
201201

202+
_updatingAppointments: Set<Appointment> = new Set();
203+
202204
_dataSourceLoadedCallback: any;
203205

204206
_subscribes: any;
@@ -1754,6 +1756,10 @@ class Scheduler extends SchedulerOptionsBaseWidget {
17541756
dragEvent.cancel = new Deferred();
17551757
}
17561758

1759+
if (isPromise(updatingOptions.cancel) && dragEvent) {
1760+
this._updatingAppointments.add(target);
1761+
}
1762+
17571763
return this._processActionResult(updatingOptions, function (canceled) {
17581764
// @ts-expect-error
17591765
let deferred = new Deferred();
@@ -1767,14 +1773,19 @@ class Scheduler extends SchedulerOptionsBaseWidget {
17671773
.done(() => {
17681774
dragEvent?.cancel.resolve(false);
17691775
})
1770-
.always((storeAppointment) => this._onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment))
1776+
.always((storeAppointment) => {
1777+
this._updatingAppointments.delete(target);
1778+
this._onDataPromiseCompleted(StoreEventNames.UPDATED, storeAppointment);
1779+
})
17711780
.fail(() => performFailAction());
17721781
} catch (err) {
17731782
performFailAction(err);
1783+
this._updatingAppointments.delete(target);
17741784
deferred.resolve();
17751785
}
17761786
} else {
17771787
performFailAction();
1788+
this._updatingAppointments.delete(target);
17781789
deferred.resolve();
17791790
}
17801791

@@ -2145,6 +2156,10 @@ class Scheduler extends SchedulerOptionsBaseWidget {
21452156
return this._workSpace.dragBehavior;
21462157
}
21472158

2159+
_isAppointmentBeingUpdated(appointmentData: Appointment): boolean {
2160+
return this._updatingAppointments.has(appointmentData);
2161+
}
2162+
21482163
getViewOffsetMs(): number {
21492164
const offsetFromOptions = this.getViewOption('offset');
21502165
return this.normalizeViewOffsetValue(offsetFromOptions);

packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,13 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
511511

512512
const isMultiSelectionAllowed = this.option('allowMultipleCellSelection');
513513
const currentCellData = this._getFullCellData($cell);
514-
const focusedCellData = this.cellsSelectionState.getFocusedCell().cellData;
514+
const focusedCell = this.cellsSelectionState.getFocusedCell();
515+
516+
if (!focusedCell) {
517+
return;
518+
}
519+
520+
const focusedCellData = focusedCell.cellData;
515521

516522
const nextFocusedCellData = this.cellsSelectionController.moveToCell({
517523
isMultiSelection,

0 commit comments

Comments
 (0)