Skip to content

Commit 5b7b285

Browse files
Scheduler: Appointment popup Customization (#31530)
Co-authored-by: Mikhail Preyskurantov <[email protected]>
1 parent 0db9358 commit 5b7b285

File tree

14 files changed

+220
-13
lines changed

14 files changed

+220
-13
lines changed

packages/devextreme-angular/src/ui/scheduler/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,10 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh
393393
394394
*/
395395
@Input()
396-
get editing(): boolean | { allowAdding?: boolean, allowDeleting?: boolean, allowDragging?: boolean, allowResizing?: boolean, allowTimeZoneEditing?: boolean, allowUpdating?: boolean, form?: undefined | { iconsShowMode?: AppointmentFormIconsShowMode, items?: Array<dxFormButtonItem | dxFormEmptyItem | dxFormGroupItem | dxFormSimpleItem | dxFormTabbedItem>, onCanceled?: ((formData: any) => void), onSaved?: ((formData: any) => void) } } {
396+
get editing(): boolean | { allowAdding?: boolean, allowDeleting?: boolean, allowDragging?: boolean, allowResizing?: boolean, allowTimeZoneEditing?: boolean, allowUpdating?: boolean, form?: undefined | { iconsShowMode?: AppointmentFormIconsShowMode, items?: Array<dxFormButtonItem | dxFormEmptyItem | dxFormGroupItem | dxFormSimpleItem | dxFormTabbedItem>, onCanceled?: ((formData: any) => void), onSaved?: ((formData: any) => void) }, popup?: Record<string, any> } {
397397
return this._getOption('editing');
398398
}
399-
set editing(value: boolean | { allowAdding?: boolean, allowDeleting?: boolean, allowDragging?: boolean, allowResizing?: boolean, allowTimeZoneEditing?: boolean, allowUpdating?: boolean, form?: undefined | { iconsShowMode?: AppointmentFormIconsShowMode, items?: Array<dxFormButtonItem | dxFormEmptyItem | dxFormGroupItem | dxFormSimpleItem | dxFormTabbedItem>, onCanceled?: ((formData: any) => void), onSaved?: ((formData: any) => void) } }) {
399+
set editing(value: boolean | { allowAdding?: boolean, allowDeleting?: boolean, allowDragging?: boolean, allowResizing?: boolean, allowTimeZoneEditing?: boolean, allowUpdating?: boolean, form?: undefined | { iconsShowMode?: AppointmentFormIconsShowMode, items?: Array<dxFormButtonItem | dxFormEmptyItem | dxFormGroupItem | dxFormSimpleItem | dxFormTabbedItem>, onCanceled?: ((formData: any) => void), onSaved?: ((formData: any) => void) }, popup?: Record<string, any> }) {
400400
this._setOption('editing', value);
401401
}
402402

@@ -1202,7 +1202,7 @@ export class DxSchedulerComponent extends DxComponent implements OnDestroy, OnCh
12021202
* This member supports the internal infrastructure and is not intended to be used directly from your code.
12031203
12041204
*/
1205-
@Output() editingChange: EventEmitter<boolean | { allowAdding?: boolean, allowDeleting?: boolean, allowDragging?: boolean, allowResizing?: boolean, allowTimeZoneEditing?: boolean, allowUpdating?: boolean, form?: undefined | { iconsShowMode?: AppointmentFormIconsShowMode, items?: Array<dxFormButtonItem | dxFormEmptyItem | dxFormGroupItem | dxFormSimpleItem | dxFormTabbedItem>, onCanceled?: ((formData: any) => void), onSaved?: ((formData: any) => void) } }>;
1205+
@Output() editingChange: EventEmitter<boolean | { allowAdding?: boolean, allowDeleting?: boolean, allowDragging?: boolean, allowResizing?: boolean, allowTimeZoneEditing?: boolean, allowUpdating?: boolean, form?: undefined | { iconsShowMode?: AppointmentFormIconsShowMode, items?: Array<dxFormButtonItem | dxFormEmptyItem | dxFormGroupItem | dxFormSimpleItem | dxFormTabbedItem>, onCanceled?: ((formData: any) => void), onSaved?: ((formData: any) => void) }, popup?: Record<string, any> }>;
12061206

12071207
/**
12081208

packages/devextreme-angular/src/ui/scheduler/nested/editing.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ export class DxoSchedulerEditingComponent extends NestedOption implements OnDest
8989
this._setOption('form', value);
9090
}
9191

92+
@Input()
93+
get popup(): Record<string, any> {
94+
return this._getOption('popup');
95+
}
96+
set popup(value: Record<string, any>) {
97+
this._setOption('popup', value);
98+
}
99+
92100

93101
protected get _optionPath() {
94102
return 'editing';

packages/devextreme-metadata/make-angular-metadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Ng.makeMetadata({
5959
removeMembers(/\/grids:LoadPanel.indicatorOptions/),
6060
removeMembers(/\/scheduler:Toolbar/),
6161
removeMembers(/\/scheduler:dxSchedulerOptions\.editing\.form/),
62+
removeMembers(/\/scheduler:dxSchedulerOptions\.editing\.popup/),
6263
removeMembers(/\/scheduler:dxSchedulerOptions\.resources\.icon/),
6364
removeMembers(/\/stepper:/),
6465
removeMembers(/\/speech_to_text:/),

packages/devextreme-metadata/make-integration-metadata.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Imd.makeMetadata({
1212
replaceTypes('common/grids:ColumnAIOptions.popup', ['*'], ['object']),
1313
replaceTypes('ui/card_view:dxCardViewOptions.filterBuilderPopup', ['*'], ['object']),
1414
replaceTypes('ui/card_view:Editing.popup', ['*'], ['object']),
15+
replaceTypes('ui/scheduler:dxSchedulerOptions.editing.popup', ['*'], ['object']),
1516

1617
removeMembers('core/dom_component:DOMComponentOptions.bindingOptions'),
1718

packages/devextreme-react/src/scheduler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ type IEditingProps = React.PropsWithChildren<{
403403
onCanceled?: ((formData: any) => void);
404404
onSaved?: ((formData: any) => void);
405405
};
406+
popup?: Record<string, any>;
406407
}>
407408
const _componentEditing = (props: IEditingProps) => {
408409
return React.createElement(NestedOption<IEditingProps>, {

packages/devextreme-vue/src/scheduler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ const DxEditingConfig = {
667667
"update:allowTimeZoneEditing": null,
668668
"update:allowUpdating": null,
669669
"update:form": null,
670+
"update:popup": null,
670671
},
671672
props: {
672673
allowAdding: Boolean,
@@ -675,7 +676,8 @@ const DxEditingConfig = {
675676
allowResizing: Boolean,
676677
allowTimeZoneEditing: Boolean,
677678
allowUpdating: Boolean,
678-
form: Object as PropType<Record<string, any>>
679+
form: Object as PropType<Record<string, any>>,
680+
popup: Object as PropType<Record<string, any>>
679681
}
680682
};
681683

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

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ describe('Appointment Popup Form', () => {
463463

464464
POM.popup.getBackButton().click();
465465

466-
expect(POM.popup.component.option('height')).toBeUndefined();
466+
expect(POM.popup.component.option('height')).toBe('auto');
467467
expect(mainGroup.hasClass(CLASSES.mainGroupHidden)).toBe(false);
468468
expect(recurrenceGroup.hasClass(CLASSES.recurrenceGroupHidden)).toBe(true);
469469
});
@@ -1398,6 +1398,163 @@ describe('Appointment Popup Form', () => {
13981398
expect(data[0].Subject).toBe('qwerty');
13991399
expect(data[0].text).toBeUndefined();
14001400
});
1401+
1402+
describe('Popup options', () => {
1403+
it('should pass custom popup options from editing.popup to appointment popup', async () => {
1404+
const { scheduler, POM } = await createScheduler({
1405+
...getDefaultConfig(),
1406+
editing: {
1407+
allowAdding: true,
1408+
allowUpdating: true,
1409+
popup: {
1410+
showTitle: true,
1411+
title: 'Custom Appointment Form',
1412+
maxHeight: '80%',
1413+
dragEnabled: true,
1414+
},
1415+
},
1416+
});
1417+
1418+
scheduler.showAppointmentPopup(commonAppointment);
1419+
1420+
expect(POM.popup.component.option('showTitle')).toBe(true);
1421+
expect(POM.popup.component.option('title')).toBe('Custom Appointment Form');
1422+
expect(POM.popup.component.option('maxHeight')).toBe('80%');
1423+
expect(POM.popup.component.option('dragEnabled')).toBe(true);
1424+
});
1425+
1426+
it('should use default popup options when editing.popup is not specified', async () => {
1427+
const { scheduler, POM } = await createScheduler({
1428+
...getDefaultConfig(),
1429+
editing: {
1430+
allowAdding: true,
1431+
allowUpdating: true,
1432+
},
1433+
});
1434+
1435+
scheduler.showAppointmentPopup(commonAppointment);
1436+
1437+
expect(POM.popup.component.option('showTitle')).toBe(false);
1438+
expect(POM.popup.component.option('height')).toBe('auto');
1439+
expect(POM.popup.component.option('maxHeight')).toBe('90%');
1440+
});
1441+
1442+
it('should merge custom popup options with default options', async () => {
1443+
const { scheduler, POM } = await createScheduler({
1444+
...getDefaultConfig(),
1445+
editing: {
1446+
allowAdding: true,
1447+
allowUpdating: true,
1448+
popup: {
1449+
showTitle: true,
1450+
title: 'My Form',
1451+
},
1452+
},
1453+
});
1454+
1455+
scheduler.showAppointmentPopup(commonAppointment);
1456+
1457+
expect(POM.popup.component.option('showTitle')).toBe(true);
1458+
expect(POM.popup.component.option('title')).toBe('My Form');
1459+
1460+
expect(POM.popup.component.option('showCloseButton')).toBe(false);
1461+
expect(POM.popup.component.option('enableBodyScroll')).toBe(false);
1462+
expect(POM.popup.component.option('preventScrollEvents')).toBe(false);
1463+
});
1464+
1465+
it('should allow overriding default popup options', async () => {
1466+
const { scheduler, POM } = await createScheduler({
1467+
...getDefaultConfig(),
1468+
editing: {
1469+
allowAdding: true,
1470+
allowUpdating: true,
1471+
popup: {
1472+
showCloseButton: true,
1473+
enableBodyScroll: true,
1474+
},
1475+
},
1476+
});
1477+
1478+
scheduler.showAppointmentPopup(commonAppointment);
1479+
1480+
expect(POM.popup.component.option('showCloseButton')).toBe(true);
1481+
expect(POM.popup.component.option('enableBodyScroll')).toBe(true);
1482+
});
1483+
1484+
it('should apply wrapperAttr configuration to popup', async () => {
1485+
const { scheduler, POM } = await createScheduler({
1486+
...getDefaultConfig(),
1487+
editing: {
1488+
allowAdding: true,
1489+
allowUpdating: true,
1490+
popup: {
1491+
wrapperAttr: {
1492+
id: 'test',
1493+
},
1494+
},
1495+
},
1496+
});
1497+
1498+
scheduler.showAppointmentPopup(commonAppointment);
1499+
1500+
const wrapperAttr = POM.popup.component.option('wrapperAttr');
1501+
expect(wrapperAttr.id).toBe('test');
1502+
expect(wrapperAttr.class).toBeDefined();
1503+
});
1504+
1505+
it('should call onShowing callback when popup is shown', async () => {
1506+
const onShowing = jest.fn();
1507+
const onAppointmentFormOpening = jest.fn();
1508+
const { scheduler } = await createScheduler({
1509+
...getDefaultConfig(),
1510+
editing: {
1511+
allowAdding: true,
1512+
allowUpdating: true,
1513+
popup: {
1514+
onShowing,
1515+
},
1516+
},
1517+
onAppointmentFormOpening,
1518+
});
1519+
1520+
scheduler.showAppointmentPopup(commonAppointment);
1521+
1522+
expect(onShowing).toHaveBeenCalled();
1523+
expect(onShowing).toHaveBeenCalledTimes(1);
1524+
expect(onAppointmentFormOpening).toHaveBeenCalled();
1525+
expect(onAppointmentFormOpening).toHaveBeenCalledTimes(1);
1526+
});
1527+
1528+
it('should call onHiding callback when popup is hidden', async () => {
1529+
const onHiding = jest.fn();
1530+
const { scheduler } = await createScheduler({
1531+
...getDefaultConfig(),
1532+
editing: {
1533+
allowAdding: true,
1534+
allowUpdating: true,
1535+
popup: {
1536+
onHiding,
1537+
},
1538+
},
1539+
});
1540+
1541+
const focusSpy = jest.spyOn(scheduler, 'focus');
1542+
1543+
scheduler.showAppointmentPopup(commonAppointment);
1544+
1545+
expect(onHiding).not.toHaveBeenCalled();
1546+
expect(focusSpy).not.toHaveBeenCalled();
1547+
1548+
scheduler.hideAppointmentPopup();
1549+
1550+
expect(onHiding).toHaveBeenCalled();
1551+
expect(onHiding).toHaveBeenCalledTimes(1);
1552+
expect(focusSpy).toHaveBeenCalled();
1553+
expect(focusSpy).toHaveBeenCalledTimes(1);
1554+
1555+
focusSpy.mockRestore();
1556+
});
1557+
});
14011558
});
14021559

14031560
describe('Appointment Popup Content', () => {

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -805,8 +805,12 @@ export class AppointmentForm {
805805
}
806806

807807
showRecurrenceGroup(): void {
808-
const overlayHeight = this.dxPopup.$overlayContent().get(0).clientHeight;
809-
this.dxPopup.option('height', overlayHeight);
808+
const currentHeight = this.dxPopup.option('height') as string | number | undefined;
809+
810+
if (currentHeight === 'auto' || currentHeight === undefined) {
811+
const overlayHeight = this.dxPopup.$overlayContent().get(0).clientHeight;
812+
this.dxPopup.option('height', overlayHeight);
813+
}
810814

811815
this._$mainGroup?.addClass(CLASSES.mainHidden);
812816
this._$recurrenceGroup?.removeClass(CLASSES.recurrenceHidden);
@@ -823,7 +827,13 @@ export class AppointmentForm {
823827
}
824828

825829
showMainGroup(saveRecurrenceValue = true): void {
826-
this.dxPopup.option('height', undefined);
830+
const currentHeight = this.dxPopup.option('height') as string | number | undefined;
831+
const editingConfig = this.scheduler.getEditingConfig();
832+
const configuredHeight = editingConfig?.popup?.height ?? 'auto';
833+
834+
if (typeof currentHeight === 'number') {
835+
this.dxPopup.option('height', configuredHeight);
836+
}
827837

828838
this._$mainGroup?.removeClass(CLASSES.mainHidden);
829839
this._$recurrenceGroup?.addClass(CLASSES.recurrenceHidden);

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { dxElementWrapper } from '@js/core/renderer';
44
import $ from '@js/core/renderer';
55
import dateUtils from '@js/core/utils/date';
66
import { Deferred, when } from '@js/core/utils/deferred';
7+
import { extend } from '@js/core/utils/extend';
78
import { getWidth } from '@js/core/utils/size';
89
import { getWindow } from '@js/core/utils/window';
910
import type { ToolbarItem } from '@js/ui/popup';
@@ -93,16 +94,20 @@ export class AppointmentPopup {
9394
}
9495

9596
_createPopupConfig() {
96-
return {
97+
const editingConfig = this.scheduler.getEditingConfig();
98+
const customPopupOptions = editingConfig?.popup ?? {};
99+
100+
const defaultPopupConfig = {
97101
height: 'auto',
98102
maxHeight: '90%',
99103
showCloseButton: false,
100104
showTitle: false,
101105
preventScrollEvents: false,
102106
enableBodyScroll: false,
103107
_ignorePreventScrollEventsDeprecation: true,
104-
onHiding: (): void => {
108+
onHiding: (e): void => {
105109
this.scheduler.focus();
110+
customPopupOptions?.onHiding?.(e);
106111
},
107112
contentTemplate: (): dxElementWrapper => {
108113
this.form.create({
@@ -113,9 +118,17 @@ export class AppointmentPopup {
113118

114119
return this.form.dxForm.$element();
115120
},
116-
onShowing: (e): void => this._onShowing(e),
121+
onShowing: (e): void => {
122+
this._onShowing(e);
123+
customPopupOptions?.onShowing?.(e);
124+
},
117125
wrapperAttr: { class: APPOINTMENT_POPUP_CLASS },
118126
};
127+
128+
return extend(true, {}, defaultPopupConfig, customPopupOptions, {
129+
onHiding: defaultPopupConfig.onHiding,
130+
onShowing: defaultPopupConfig.onShowing,
131+
});
119132
}
120133

121134
_onShowing(e) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,7 @@ class Scheduler extends SchedulerOptionsBaseWidget {
922922
const isReadOnly = Object.values({
923923
...this._editing,
924924
form: undefined,
925+
popup: undefined,
925926
}).every((value) => !value);
926927

927928
(this.$element() as any).toggleClass(WIDGET_READONLY_CLASS, isReadOnly);

0 commit comments

Comments
 (0)