Skip to content

Commit ba8d95b

Browse files
Scheduler - Replace action enum with onDone/onCancel callbacks in AppointmentPopup (#33052)
1 parent d1c1b4e commit ba8d95b

File tree

5 files changed

+137
-73
lines changed

5 files changed

+137
-73
lines changed

packages/devextreme/js/__internal/scheduler/__tests__/__mock__/create_appointment_popup.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { Deferred } from '@js/core/utils/deferred';
66
import { mockTimeZoneCalculator } from '../../__mock__/timezone_calculator.mock';
77
import { AppointmentForm } from '../../appointment_popup/m_form';
88
import {
9-
ACTION_TO_APPOINTMENT,
109
APPOINTMENT_POPUP_CLASS,
1110
AppointmentPopup,
11+
type AppointmentPopupConfig,
1212
} from '../../appointment_popup/m_popup';
1313
import {
1414
AppointmentDataAccessor,
@@ -59,11 +59,13 @@ const resolvedDeferred = (): any => {
5959

6060
interface CreateAppointmentPopupOptions {
6161
appointmentData?: Record<string, unknown>;
62-
action?: number;
6362
editing?: Record<string, unknown>;
6463
firstDayOfWeek?: number;
6564
startDayHour?: number;
6665
onAppointmentFormOpening?: (...args: unknown[]) => void;
66+
onSave?: jest.Mock;
67+
title?: string;
68+
readOnly?: boolean;
6769
addAppointment?: jest.Mock;
6870
updateAppointment?: jest.Mock;
6971
}
@@ -78,6 +80,7 @@ interface CreateAppointmentPopupResult {
7880
updateAppointment: jest.Mock;
7981
focus: jest.Mock;
8082
updateScrollPosition: jest.Mock;
83+
onSave: jest.Mock;
8184
};
8285
dispose: () => void;
8386
}
@@ -112,6 +115,7 @@ export const createAppointmentPopup = async (
112115
?? jest.fn(resolvedDeferred);
113116
const focus = jest.fn();
114117
const updateScrollPosition = jest.fn();
118+
const onSave = options.onSave ?? jest.fn(resolvedDeferred);
115119

116120
const formSchedulerProxy = {
117121
getResourceById: (): Record<string, unknown> => resourceManager.resourceById,
@@ -161,9 +165,14 @@ export const createAppointmentPopup = async (
161165

162166
const appointmentData = options.appointmentData
163167
?? { ...DEFAULT_APPOINTMENT };
164-
const action = options.action ?? ACTION_TO_APPOINTMENT.CREATE;
165-
166-
popup.show(appointmentData, { action, allowSaving: true });
168+
const title = options.title ?? 'New Appointment';
169+
const readOnly = options.readOnly ?? false;
170+
171+
popup.show(appointmentData, {
172+
onSave: onSave as unknown as AppointmentPopupConfig['onSave'],
173+
title,
174+
readOnly,
175+
});
167176
await new Promise(process.nextTick);
168177

169178
const selector = `.dx-overlay-wrapper.${APPOINTMENT_POPUP_CLASS}`;
@@ -196,6 +205,7 @@ export const createAppointmentPopup = async (
196205
updateAppointment,
197206
focus,
198207
updateScrollPosition,
208+
onSave,
199209
},
200210
dispose,
201211
};

packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import {
2-
afterEach, beforeEach, describe, expect, it,
2+
afterEach, beforeEach, describe, expect, it, jest,
33
} from '@jest/globals';
44

55
import fx from '../../../common/core/animation/fx';
66
import {
77
createAppointmentPopup,
88
disposeAppointmentPopups,
99
} from '../__tests__/__mock__/create_appointment_popup';
10-
import { ACTION_TO_APPOINTMENT } from './m_popup';
1110

1211
describe('Isolated AppointmentPopup environment', () => {
1312
beforeEach(() => {
@@ -45,20 +44,19 @@ describe('Isolated AppointmentPopup environment', () => {
4544
expect(POM.cancelButton).toBeTruthy();
4645
});
4746

48-
it('should call addAppointment on Save click for CREATE action', async () => {
47+
it('should call onSave callback on Save click', async () => {
4948
const { POM, callbacks } = await createAppointmentPopup({
5049
appointmentData: {
5150
text: 'New Appointment',
5251
startDate: new Date(2021, 3, 26, 9, 30),
5352
endDate: new Date(2021, 3, 26, 11, 0),
5453
},
55-
action: ACTION_TO_APPOINTMENT.CREATE,
5654
});
5755

5856
POM.saveButton.click();
5957

60-
expect(callbacks.addAppointment).toHaveBeenCalledTimes(1);
61-
expect(callbacks.addAppointment).toHaveBeenCalledWith(
58+
expect(callbacks.onSave).toHaveBeenCalledTimes(1);
59+
expect(callbacks.onSave).toHaveBeenCalledWith(
6260
expect.objectContaining({
6361
text: 'New Appointment',
6462
startDate: new Date(2021, 3, 26, 9, 30),
@@ -67,19 +65,45 @@ describe('Isolated AppointmentPopup environment', () => {
6765
);
6866
});
6967

70-
it('should call updateAppointment on Save click for UPDATE action', async () => {
68+
it('should not call addAppointment or updateAppointment directly', async () => {
7169
const { POM, callbacks } = await createAppointmentPopup({
7270
appointmentData: {
73-
text: 'Existing Appointment',
71+
text: 'Test',
7472
startDate: new Date(2021, 3, 26, 9, 30),
7573
endDate: new Date(2021, 3, 26, 11, 0),
7674
},
77-
action: ACTION_TO_APPOINTMENT.UPDATE,
7875
});
7976

8077
POM.saveButton.click();
8178

82-
expect(callbacks.updateAppointment).toHaveBeenCalledTimes(1);
79+
expect(callbacks.addAppointment).not.toHaveBeenCalled();
80+
expect(callbacks.updateAppointment).not.toHaveBeenCalled();
81+
});
82+
83+
it('should display title from config', async () => {
84+
const { POM } = await createAppointmentPopup({
85+
title: 'Edit Appointment',
86+
});
87+
88+
const titleElement = POM.element.querySelector('.dx-toolbar-label');
89+
expect(titleElement?.textContent).toBe('Edit Appointment');
90+
});
91+
92+
it('should hide Save button when readOnly is true', async () => {
93+
const { POM } = await createAppointmentPopup({
94+
readOnly: true,
95+
});
96+
97+
const saveButtons = POM.element.querySelectorAll('.dx-popup-done');
98+
expect(saveButtons.length).toBe(0);
99+
});
100+
101+
it('should show Save button when readOnly is false', async () => {
102+
const { POM } = await createAppointmentPopup({
103+
readOnly: false,
104+
});
105+
106+
expect(POM.saveButton).toBeTruthy();
83107
});
84108

85109
it('should hide popup on Cancel click', async () => {
@@ -89,4 +113,35 @@ describe('Isolated AppointmentPopup environment', () => {
89113
POM.cancelButton.click();
90114
expect(popup.visible).toBe(false);
91115
});
116+
117+
it('should support composite onSave for exclude-from-series scenario', async () => {
118+
const updateAppointment = jest.fn();
119+
const addAppointment: jest.Mock = jest.fn(() => Promise.resolve());
120+
121+
const sourceAppointment = { text: 'Series', recurrenceRule: 'FREQ=DAILY' };
122+
const updatedAppointment = { text: 'Series', recurrenceException: '20210426' };
123+
124+
const onSave = jest.fn((newAppointment) => {
125+
updateAppointment(sourceAppointment, updatedAppointment);
126+
return addAppointment(newAppointment);
127+
});
128+
129+
const { POM } = await createAppointmentPopup({
130+
appointmentData: {
131+
text: 'Single occurrence',
132+
startDate: new Date(2021, 3, 26, 9, 30),
133+
endDate: new Date(2021, 3, 26, 11, 0),
134+
},
135+
title: 'Edit Appointment',
136+
onSave,
137+
});
138+
139+
POM.saveButton.click();
140+
141+
expect(onSave).toHaveBeenCalledTimes(1);
142+
expect(updateAppointment).toHaveBeenCalledWith(sourceAppointment, updatedAppointment);
143+
expect(addAppointment).toHaveBeenCalledWith(
144+
expect.objectContaining({ text: 'Single occurrence' }),
145+
);
146+
});
92147
});

packages/devextreme/js/__internal/scheduler/appointment_popup/m_popup.ts

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ const POPUP_FULL_SCREEN_MODE_WINDOW_WIDTH_THRESHOLD = 485;
2424

2525
const DAY_IN_MS = dateUtils.dateToMilliseconds('day');
2626

27-
export const ACTION_TO_APPOINTMENT = {
28-
CREATE: 0,
29-
UPDATE: 1,
30-
EXCLUDE_FROM_SERIES: 2,
31-
};
27+
export interface AppointmentPopupConfig {
28+
onSave: (appointment: Record<string, unknown>) => PromiseLike<unknown>;
29+
title: string;
30+
readOnly: boolean;
31+
}
3232

3333
export class AppointmentPopup {
3434
scheduler: any;
@@ -42,6 +42,12 @@ export class AppointmentPopup {
4242

4343
state: any;
4444

45+
private config: AppointmentPopupConfig = {
46+
onSave: () => Promise.resolve(),
47+
title: '',
48+
readOnly: false,
49+
};
50+
4551
get popup(): dxPopup {
4652
return this._popup as dxPopup;
4753
}
@@ -55,7 +61,6 @@ export class AppointmentPopup {
5561
this.form = form;
5662

5763
this.state = {
58-
action: null,
5964
lastEditData: null,
6065
saveChangesLocker: false,
6166
appointment: {
@@ -64,11 +69,9 @@ export class AppointmentPopup {
6469
};
6570
}
6671

67-
show(appointment, config) {
72+
show(appointment, config: AppointmentPopupConfig) {
6873
this.state.appointment.data = appointment;
69-
this.state.action = config.action;
70-
this.state.allowSaving = config.allowSaving;
71-
this.state.excludeInfo = config.excludeInfo;
74+
this.config = config;
7275

7376
this.disposePopup();
7477

@@ -174,18 +177,6 @@ export class AppointmentPopup {
174177
});
175178
}
176179

177-
private isReadOnly(appointmentAdapter: AppointmentAdapter): boolean {
178-
if (Boolean(appointmentAdapter.source) && appointmentAdapter.disabled) {
179-
return true;
180-
}
181-
182-
if (this.state.action === ACTION_TO_APPOINTMENT.CREATE) {
183-
return false;
184-
}
185-
186-
return !this.scheduler.getEditingConfig().allowUpdating;
187-
}
188-
189180
private createAppointmentAdapter(rawAppointment): AppointmentAdapter {
190181
return new AppointmentAdapter(
191182
rawAppointment,
@@ -201,7 +192,7 @@ export class AppointmentPopup {
201192

202193
const formData = this.createFormData(appointmentAdapter);
203194

204-
this.form.readOnly = this.isReadOnly(appointmentAdapter);
195+
this.form.readOnly = this.config.readOnly;
205196
this.form.formData = formData;
206197

207198
this.form.showMainGroup();
@@ -287,20 +278,7 @@ export class AppointmentPopup {
287278

288279
const appointment = clonedAdapter.source;
289280

290-
switch (this.state.action) {
291-
case ACTION_TO_APPOINTMENT.CREATE:
292-
this.scheduler.addAppointment(appointment).done(deferred.resolve);
293-
break;
294-
case ACTION_TO_APPOINTMENT.UPDATE:
295-
this.scheduler.updateAppointment(this.state.appointment.data, appointment).done(deferred.resolve);
296-
break;
297-
case ACTION_TO_APPOINTMENT.EXCLUDE_FROM_SERIES:
298-
this.scheduler.updateAppointment(this.state.excludeInfo.sourceAppointment, this.state.excludeInfo.updatedAppointment);
299-
this.scheduler.addAppointment(appointment).done(deferred.resolve);
300-
break;
301-
default:
302-
break;
303-
}
281+
when(this.config.onSave(appointment)).done(deferred.resolve);
304282

305283
deferred.done(() => {
306284
hideLoading();
@@ -414,13 +392,10 @@ export class AppointmentPopup {
414392
return;
415393
}
416394

417-
const isCreating = this.state.action === ACTION_TO_APPOINTMENT.CREATE;
418-
const formTitleKey = isCreating ? 'dxScheduler-newPopupTitle' : 'dxScheduler-editPopupTitle';
419-
420395
const toolbarItems: ToolbarItem[] = [{
421396
toolbar: 'top',
422397
location: 'before',
423-
text: messageLocalization.format(formTitleKey),
398+
text: this.config.title,
424399
cssClass: 'dx-toolbar-label',
425400
}];
426401

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

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1621,14 +1621,24 @@ class Scheduler extends SchedulerOptionsBaseWidget {
16211621
}
16221622

16231623
if (isPopupEditing) {
1624-
this.appointmentPopup.show(singleRawAppointment, {
1625-
isToolbarVisible: true, // TODO: remove when legacyForm is deleted
1626-
action: ACTION_TO_APPOINTMENT.EXCLUDE_FROM_SERIES,
1627-
excludeInfo: {
1628-
sourceAppointment: rawAppointment,
1629-
updatedAppointment: appointment.source,
1630-
},
1631-
});
1624+
const popupConfig = this.editing.legacyForm
1625+
? {
1626+
isToolbarVisible: true,
1627+
action: ACTION_TO_APPOINTMENT.EXCLUDE_FROM_SERIES,
1628+
excludeInfo: {
1629+
sourceAppointment: rawAppointment,
1630+
updatedAppointment: appointment.source,
1631+
},
1632+
}
1633+
: {
1634+
onSave: (newAppointment) => {
1635+
this.updateAppointment(rawAppointment, appointment.source);
1636+
return this.addAppointment(newAppointment);
1637+
},
1638+
title: messageLocalization.format('dxScheduler-editPopupTitle'),
1639+
readOnly: Boolean(appointment.source) && appointment.disabled,
1640+
};
1641+
this.appointmentPopup.show(singleRawAppointment, popupConfig);
16321642
this.editAppointmentData = rawAppointment;
16331643
} else {
16341644
this.updateAppointmentCore(rawAppointment, appointment.source, () => {
@@ -2003,20 +2013,34 @@ class Scheduler extends SchedulerOptionsBaseWidget {
20032013

20042014
if (isCreateAppointment) {
20052015
delete this.editAppointmentData; // TODO
2006-
this.editing.allowAdding && this.appointmentPopup.show(rawAppointment, {
2007-
isToolbarVisible: true, // TODO: remove when legacyForm is deleted
2008-
action: ACTION_TO_APPOINTMENT.CREATE,
2009-
});
2016+
if (this.editing.allowAdding) {
2017+
const popupConfig = this.editing.legacyForm
2018+
? { isToolbarVisible: true, action: ACTION_TO_APPOINTMENT.CREATE }
2019+
: {
2020+
onSave: (appointment) => this.addAppointment(appointment),
2021+
title: messageLocalization.format('dxScheduler-newPopupTitle'),
2022+
readOnly: false,
2023+
};
2024+
this.appointmentPopup.show(rawAppointment, popupConfig);
2025+
}
20102026
} else {
20112027
const startDate = this._dataAccessors.get('startDate', newRawTargetedAppointment || rawAppointment);
20122028

20132029
this.checkRecurringAppointment(rawAppointment, newTargetedAppointment, startDate, () => {
20142030
this.editAppointmentData = rawAppointment; // TODO
20152031

2016-
this.appointmentPopup.show(rawAppointment, {
2017-
isToolbarVisible: this.editing.allowUpdating, // TODO: remove when legacyForm is deleted
2018-
action: ACTION_TO_APPOINTMENT.UPDATE,
2019-
});
2032+
const adapter = new AppointmentAdapter(rawAppointment, this._dataAccessors);
2033+
const isDisabled = Boolean(adapter.source) && adapter.disabled;
2034+
const readOnly = isDisabled || !this.editing.allowUpdating;
2035+
2036+
const popupConfig = this.editing.legacyForm
2037+
? { isToolbarVisible: this.editing.allowUpdating, action: ACTION_TO_APPOINTMENT.UPDATE }
2038+
: {
2039+
onSave: (appointment) => this.updateAppointment(rawAppointment, appointment),
2040+
title: messageLocalization.format('dxScheduler-editPopupTitle'),
2041+
readOnly,
2042+
};
2043+
this.appointmentPopup.show(rawAppointment, popupConfig);
20202044
}, false, true);
20212045
}
20222046
}

packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ module('Integration: Appointment tooltip', moduleConfig, () => {
335335
},
336336
'show has a right appointment data arg');
337337

338-
assert.equal(args[1].isToolbarVisible, true, 'show has a right createNewAppointment arg');
338+
assert.equal(args[1].readOnly, false, 'show has a right readOnly arg');
339339

340340
assert.notOk(scheduler.tooltip.isVisible(), 'tooltip was hidden');
341341
});

0 commit comments

Comments
 (0)