typo3-typescript-sources 14.3.5 → 14.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+ import { customElement, query, state } from 'lit/decorators.js';
14
+ import { html, LitElement, type PropertyValues, type TemplateResult } from 'lit';
15
+ import type { SubmissionServiceInterface } from '@typo3/backend/wizard/finisher/submission-service-interface';
16
+ import type { WizardStepInterface } from '@typo3/backend/wizard/steps/wizard-step-interface';
17
+ import type { DataStore, Wizard } from '@typo3/backend/wizard/wizard';
18
+ import { AutoAdvanceEvent } from '@typo3/backend/wizard/events/auto-advance-event';
19
+ import PresetStep from '@typo3/dashboard/wizard/steps/preset-step';
20
+ import TitleStep from '@typo3/dashboard/wizard/steps/title-step';
21
+ import { DashboardWizardSubmissionService } from '@typo3/dashboard/wizard/finisher/dashboard-wizard-submission-service';
22
+
23
+ export interface DashboardWizardDataStore extends DataStore {
24
+ preset?: string;
25
+ title?: string;
26
+ }
27
+
28
+ type DashboardWizardDataStoreKey = keyof DashboardWizardDataStore;
29
+
30
+ export interface DashboardWizardContext {
31
+ wizard: Wizard;
32
+ getStoreData: <T extends DashboardWizardDataStoreKey>(key: T) => NoInfer<DashboardWizardDataStore[T]>;
33
+ setStoreData: <T extends DashboardWizardDataStoreKey>(key: T, value: NoInfer<DashboardWizardDataStore[T]>) => void;
34
+ clearStoreData: <T extends DashboardWizardDataStoreKey>(key: T) => void;
35
+ getDataStore: () => Readonly<DashboardWizardDataStore>;
36
+ dispatchAutoAdvance: () => void;
37
+ }
38
+
39
+ @customElement('typo3-dashboard-wizard')
40
+ export class DashboardWizard extends LitElement {
41
+ @state() steps: WizardStepInterface[] = [];
42
+ @state() submissionService: SubmissionServiceInterface;
43
+
44
+ @query('typo3-backend-wizard') wizard!: Wizard;
45
+
46
+ private context: DashboardWizardContext;
47
+
48
+ protected override firstUpdated(_changedProperties: PropertyValues): void {
49
+ super.firstUpdated(_changedProperties);
50
+
51
+ this.context = {
52
+ wizard: this.wizard,
53
+ getStoreData: this.wizard.getStoreData.bind(this.wizard),
54
+ setStoreData: this.wizard.setStoreData.bind(this.wizard),
55
+ clearStoreData: this.wizard.clearStoreData.bind(this.wizard),
56
+ getDataStore: this.wizard.getDataStore.bind(this.wizard),
57
+ dispatchAutoAdvance: () => this.wizard.dispatchEvent(new AutoAdvanceEvent())
58
+ };
59
+
60
+ this.steps = [
61
+ new PresetStep(this.context),
62
+ new TitleStep(this.context),
63
+ ];
64
+
65
+ this.submissionService = new DashboardWizardSubmissionService(this.context);
66
+ }
67
+
68
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
69
+ return this;
70
+ }
71
+
72
+ protected override render(): TemplateResult {
73
+ return html`
74
+ <typo3-backend-wizard .steps="${this.steps}"
75
+ .submissionService="${this.submissionService}"
76
+ skip-summary
77
+ ></typo3-backend-wizard>
78
+ `;
79
+ }
80
+ }
81
+
82
+ declare global {
83
+ interface HTMLElementTagNameMap {
84
+ 'typo3-dashboard-wizard': DashboardWizard;
85
+ }
86
+ }
@@ -22,7 +22,7 @@ import '@typo3/backend/element/icon-element';
22
22
  import AjaxRequest from '@typo3/core/ajax/ajax-request';
23
23
  import ClientStorage from '@typo3/backend/storage/client';
24
24
  import { delay } from '@typo3/core/lit-helper';
25
- import Modal, { type ModalElement } from '@typo3/backend/modal';
25
+ import Modal, { type ModalElement, Size } from '@typo3/backend/modal';
26
26
  import { SeverityEnum } from '@typo3/backend/enum/severity';
27
27
  import { AjaxResponse } from '@typo3/core/ajax/ajax-response';
28
28
  import { Categories, type DataCategoriesInterface, type NewRecordWizardItemSelectedEventInterface } from '@typo3/backend/new-record-wizard';
@@ -49,7 +49,7 @@ interface DashboardInterface {
49
49
  widgetPositions: Record<number, DashboardWidgetPosition[]>
50
50
  }
51
51
 
52
- interface DashboardPresetInterface {
52
+ export interface DashboardPresetInterface {
53
53
  identifier: string;
54
54
  title: string;
55
55
  description: string;
@@ -154,7 +154,7 @@ class DashboardWidgetRefreshEvent extends Event {
154
154
  }
155
155
  }
156
156
 
157
- class DashboardAddEvent extends Event {
157
+ export class DashboardAddEvent extends Event {
158
158
  static readonly eventName = 'typo3:dashboard:dashboard:add';
159
159
 
160
160
  constructor(
@@ -446,6 +446,8 @@ export class Dashboard extends LitElement {
446
446
  this.mql = window.matchMedia('(prefers-reduced-motion: reduce)');
447
447
  this.mqListener(this.mql);
448
448
  this.mql.addEventListener('change', this.mqListener);
449
+
450
+ document.addEventListener(DashboardAddEvent.eventName, this.relayAddDashboardEvent);
449
451
  }
450
452
 
451
453
  public override disconnectedCallback() {
@@ -456,6 +458,8 @@ export class Dashboard extends LitElement {
456
458
 
457
459
  this.mql?.removeEventListener('change', this.mqListener);
458
460
  this.mql = null;
461
+
462
+ document.removeEventListener(DashboardAddEvent.eventName, this.relayAddDashboardEvent);
459
463
  }
460
464
 
461
465
  protected readonly mqListener = (mql: MediaQueryList|MediaQueryListEvent): void => {
@@ -496,6 +500,16 @@ export class Dashboard extends LitElement {
496
500
  `;
497
501
  }
498
502
 
503
+ /**
504
+ * Relays the cross-frame add broadcast (dispatched on `document` by the
505
+ * wizard finisher) onto the element, so it is handled alongside the other
506
+ * dashboard events. A fresh event is created because an in-flight event
507
+ * cannot be re-dispatched.
508
+ */
509
+ private readonly relayAddDashboardEvent = (event: DashboardAddEvent): void => {
510
+ this.dispatchEvent(new DashboardAddEvent(event.preset, event.title));
511
+ };
512
+
499
513
  private async load(): Promise<void> {
500
514
  this.loading = true;
501
515
  this.dashboards = await this.fetchDashboards();
@@ -514,11 +528,6 @@ export class Dashboard extends LitElement {
514
528
  }
515
529
  }
516
530
 
517
- private async fetchPresets(): Promise<DashboardPresetInterface[]> {
518
- const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_presets_get);
519
- return Object.values(data);
520
- }
521
-
522
531
  private async fetchCategories(): Promise<Categories> {
523
532
  const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_categories_get);
524
533
  return Categories.fromData(data as DataCategoriesInterface);
@@ -538,79 +547,19 @@ export class Dashboard extends LitElement {
538
547
  }
539
548
 
540
549
  private async createDashboard(): Promise<void> {
541
- const presets = await this.fetchPresets();
542
- const filteredPresets = presets.filter(preset => preset.showInWizard);
543
-
544
- const content = html`
545
- <form>
546
- <div class="form-group">
547
- <label class="form-label" for="dashboard-form-add-title">${labels.get('dashboard.title')}</label>
548
- <input class="form-control" id="dashboard-form-add-title" type="text" name="title" required="required">
549
- </div>
550
- <div class="dashboard-modal-items">
551
- ${repeat(filteredPresets, (preset: DashboardPresetInterface) => preset.identifier, (preset: DashboardPresetInterface, index: number) => html`
552
- <div class="dashboard-modal-item">
553
- <input
554
- type="radio"
555
- name="preset"
556
- value=${preset.identifier}
557
- class="dashboard-modal-item-checkbox"
558
- id="dashboard-form-add-preset-${preset.identifier}"
559
- ?checked=${index === 0}
560
- >
561
- <label for="dashboard-form-add-preset-${preset.identifier}" class="dashboard-modal-item-block">
562
- <span class="dashboard-modal-item-icon">
563
- <typo3-backend-icon identifier=${preset.icon} size="medium"></typo3-backend-icon>
564
- </span>
565
- <span class="dashboard-modal-item-details">
566
- <span class="dashboard-modal-item-title">${preset.title}</span>
567
- <span class="dashboard-modal-item-description">${preset.description}</span>
568
- </span>
569
- </label>
570
- </div>
571
- `)}
572
- </div>
573
- </form>
574
- `;
550
+ await topLevelModuleImport('@typo3/dashboard/dashboard-wizard.js');
575
551
 
576
552
  Modal.advanced({
577
553
  type: Modal.types.default,
578
554
  title: labels.get('dashboard.add'),
579
- size: Modal.sizes.medium,
580
- severity: SeverityEnum.notice,
581
- content,
582
- callback: (currentModal: ModalElement): void => {
583
-
584
- currentModal.addEventListener('typo3-modal-shown', (): void => {
585
- (currentModal.querySelector('#dashboard-form-add-title') as HTMLInputElement)?.focus();
586
- });
587
-
588
- currentModal.querySelector('form').addEventListener('submit', (e: Event): void => {
589
- e.preventDefault();
590
- const form = e.target as HTMLFormElement;
591
- const formData = new FormData(form);
592
- this.dispatchEvent(new DashboardAddEvent(
593
- formData.get('preset') as string,
594
- formData.get('title') as string,
595
- ));
596
- currentModal.hideModal();
597
- });
598
-
555
+ size: {
556
+ width: Size.default,
557
+ height: Size.small,
599
558
  },
600
- buttons: [
601
- {
602
- text: labels.get('dashboard.add.button.close'),
603
- btnClass: 'btn-default',
604
- name: 'cancel',
605
- trigger: (e, modal) => modal.hideModal(),
606
- },
607
- {
608
- text: labels.get('dashboard.add.button.ok'),
609
- btnClass: 'btn-primary',
610
- name: 'save',
611
- trigger: (e, modal) => modal.querySelector('form').requestSubmit(),
612
- },
613
- ]
559
+ severity: SeverityEnum.notice,
560
+ staticBackdrop: true,
561
+ content: html`<typo3-dashboard-wizard></typo3-dashboard-wizard>`,
562
+ buttons: [],
614
563
  });
615
564
  }
616
565
 
@@ -1566,6 +1515,9 @@ declare global {
1566
1515
  'typo3-dashboard': Dashboard;
1567
1516
  'typo3-dashboard-widget': DashboardWidget;
1568
1517
  }
1518
+ interface DocumentEventMap {
1519
+ [DashboardAddEvent.eventName]: DashboardAddEvent;
1520
+ }
1569
1521
  interface HTMLElementEventMap {
1570
1522
  [DashboardWidgetContentRenderedEvent.eventName]: DashboardWidgetContentRenderedEvent;
1571
1523
  [DashboardWidgetMoveIntendEvent.eventName]: DashboardWidgetMoveIntendEvent;
@@ -0,0 +1,43 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+
14
+ import type { SubmissionServiceInterface } from '@typo3/backend/wizard/finisher/submission-service-interface';
15
+ import type { FinisherResult } from '@typo3/backend/wizard/finisher/finisher-result';
16
+ import type { DashboardWizardContext } from '@typo3/dashboard/dashboard-wizard';
17
+ import { DashboardAddEvent } from '@typo3/dashboard/dashboard';
18
+ import labels from '~labels/dashboard.messages';
19
+
20
+ export class DashboardWizardSubmissionService implements SubmissionServiceInterface {
21
+
22
+ constructor(private readonly context: DashboardWizardContext) {
23
+ }
24
+
25
+ async execute(): Promise<FinisherResult> {
26
+ const { preset, title } = this.context.getDataStore();
27
+
28
+ return {
29
+ success: true,
30
+ finisher: {
31
+ identifier: 'event',
32
+ module: '@typo3/backend/wizard/finisher/event-finisher.js',
33
+ data: {
34
+ event: new DashboardAddEvent(preset ?? '', title ?? ''),
35
+ },
36
+ labels: {
37
+ successTitle: labels.get('dashboard.wizard.success.title'),
38
+ successDescription: labels.get('dashboard.wizard.success.message'),
39
+ }
40
+ }
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,164 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+
14
+ import { html, type TemplateResult } from 'lit';
15
+ import { live } from 'lit/directives/live.js';
16
+ import { Task, TaskStatus } from '@lit/task';
17
+ import type { WizardStepInterface } from '@typo3/backend/wizard/steps/wizard-step-interface';
18
+ import type { WizardStepValueInterface } from '@typo3/backend/wizard/steps/wizard-step-value-interface';
19
+ import type { WizardStepSummaryInterface } from '@typo3/backend/wizard/steps/wizard-step-summary-interface';
20
+ import type { WizardStepAfterRenderInterface } from '@typo3/backend/wizard/steps/wizard-step-after-render-interface';
21
+ import type { SummaryItem } from '@typo3/backend/wizard/steps/summary-item-interface';
22
+ import AjaxRequest from '@typo3/core/ajax/ajax-request';
23
+ import type { DashboardWizardContext } from '@typo3/dashboard/dashboard-wizard';
24
+ import type { DashboardPresetInterface } from '@typo3/dashboard/dashboard';
25
+ import labels from '~labels/dashboard.messages';
26
+
27
+ export class PresetStep implements WizardStepInterface, WizardStepValueInterface, WizardStepSummaryInterface, WizardStepAfterRenderInterface {
28
+ readonly key = 'preset';
29
+ readonly title = labels.get('dashboard.wizard.preset.title');
30
+ readonly autoAdvance = true;
31
+
32
+ private task: Task<[], DashboardPresetInterface[]>;
33
+ private selectedPreset: string | null = null;
34
+ private hasDispatchedAutoAdvance: boolean = false;
35
+
36
+ constructor(private readonly context: DashboardWizardContext) {
37
+ this.initPresetsTask();
38
+ }
39
+
40
+ public isComplete(): boolean {
41
+ return this.getValue() !== null;
42
+ }
43
+
44
+ public render(): TemplateResult {
45
+ if (this.task.status === TaskStatus.INITIAL) {
46
+ this.task.run();
47
+ }
48
+
49
+ return this.task.render({
50
+ complete: (presets: DashboardPresetInterface[]) => {
51
+ let shouldAutoAdvance = false;
52
+
53
+ // Initialize from store, otherwise preselect the first preset
54
+ if (this.getValue() === null) {
55
+ const storedValue = this.context.getStoreData(this.key);
56
+ if (storedValue != null) {
57
+ this.setValue(storedValue);
58
+ } else if (presets.length > 0) {
59
+ this.setValue(presets[0].identifier);
60
+ shouldAutoAdvance = presets.length === 1;
61
+ }
62
+ }
63
+
64
+ if (shouldAutoAdvance && !this.hasDispatchedAutoAdvance) {
65
+ this.hasDispatchedAutoAdvance = true;
66
+ this.context.dispatchAutoAdvance();
67
+ return this.context.wizard.renderLoader();
68
+ }
69
+
70
+ return html`
71
+ <div class="dashboard-wizard-preset">
72
+ <h2 class="h4">${labels.get('dashboard.wizard.preset.headline')}</h2>
73
+ <div class="form-check-card-container">
74
+ ${presets.map((preset: DashboardPresetInterface) => html`
75
+ <div class="form-check form-check-type-card">
76
+ <input
77
+ class="form-check-input"
78
+ type="radio"
79
+ name="preset"
80
+ id="preset-${preset.identifier}"
81
+ value="${preset.identifier}"
82
+ .checked="${live(this.getValue() === preset.identifier)}"
83
+ @change="${() => this.setValue(preset.identifier)}"
84
+ >
85
+ <label class="form-check-label" for="preset-${preset.identifier}">
86
+ <span class="form-check-label-header form-check-label-header-inherit">
87
+ <typo3-backend-icon identifier="${preset.icon}" size="small"></typo3-backend-icon>
88
+ ${preset.title}
89
+ </span>
90
+ <span class="form-check-label-body">${preset.description}</span>
91
+ </label>
92
+ </div>
93
+ `)}
94
+ </div>
95
+ </div>
96
+ `;
97
+ },
98
+ error: (error: unknown) => this.context.wizard.renderError(labels.get('dashboard.wizard.preset.loadError'), error),
99
+ pending: () => this.context.wizard.renderLoader()
100
+ });
101
+ }
102
+
103
+ public reset(): void {
104
+ this.selectedPreset = null;
105
+ this.context.clearStoreData(this.key);
106
+ this.initPresetsTask();
107
+ }
108
+
109
+ public getValue(): string {
110
+ return this.selectedPreset;
111
+ }
112
+
113
+ public setValue(value: string): void {
114
+ this.selectedPreset = value;
115
+ this.context.wizard.requestUpdate();
116
+ }
117
+
118
+ public beforeAdvance(): void {
119
+ this.context.setStoreData(this.key, this.getValue());
120
+ }
121
+
122
+ public getSummaryData(): SummaryItem[] {
123
+ const selectedIdentifier = this.context.getStoreData(this.key);
124
+ if (!selectedIdentifier || !this.task.value) {
125
+ return [];
126
+ }
127
+
128
+ const selectedPreset = this.task.value.find((preset: DashboardPresetInterface) => preset.identifier === selectedIdentifier);
129
+ if (!selectedPreset) {
130
+ return [];
131
+ }
132
+
133
+ return [{
134
+ label: this.title,
135
+ value: html`
136
+ <typo3-backend-icon identifier="${selectedPreset.icon}" size="small" class="me-1"></typo3-backend-icon>
137
+ ${selectedPreset.title}
138
+ `
139
+ }];
140
+ }
141
+
142
+ public async afterRender(): Promise<void> {
143
+ // Presets load asynchronously; signal readiness so the wizard moves focus
144
+ // (onto the selected preset) only once they are rendered.
145
+ try {
146
+ await this.task.taskComplete;
147
+ } catch {
148
+ // ignore — the error state is rendered by the step
149
+ }
150
+ }
151
+
152
+ private initPresetsTask(): void {
153
+ this.task = new Task(this.context.wizard, {
154
+ task: async (): Promise<DashboardPresetInterface[]> => {
155
+ const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_presets_get).get({ cache: 'no-cache' });
156
+ const data = await response.resolve();
157
+ return Object.values(data as Record<string, DashboardPresetInterface>).filter((preset: DashboardPresetInterface) => preset.showInWizard);
158
+ },
159
+ autoRun: false
160
+ });
161
+ }
162
+ }
163
+
164
+ export default PresetStep;
@@ -0,0 +1,90 @@
1
+ /*
2
+ * This file is part of the TYPO3 CMS project.
3
+ *
4
+ * It is free software; you can redistribute it and/or modify it under
5
+ * the terms of the GNU General Public License, either version 2
6
+ * of the License, or any later version.
7
+ *
8
+ * For the full copyright and license information, please read the
9
+ * LICENSE.txt file that was distributed with this source code.
10
+ *
11
+ * The TYPO3 project - inspiring people to share!
12
+ */
13
+
14
+ import { html, type TemplateResult } from 'lit';
15
+ import { live } from 'lit/directives/live.js';
16
+ import type { WizardStepInterface } from '@typo3/backend/wizard/steps/wizard-step-interface';
17
+ import type { WizardStepValueInterface } from '@typo3/backend/wizard/steps/wizard-step-value-interface';
18
+ import type { WizardStepSummaryInterface } from '@typo3/backend/wizard/steps/wizard-step-summary-interface';
19
+ import type { SummaryItem } from '@typo3/backend/wizard/steps/summary-item-interface';
20
+ import type { DashboardWizardContext } from '@typo3/dashboard/dashboard-wizard';
21
+ import labels from '~labels/dashboard.messages';
22
+
23
+ export class TitleStep implements WizardStepInterface, WizardStepValueInterface, WizardStepSummaryInterface {
24
+ readonly key = 'title';
25
+ readonly title = labels.get('dashboard.wizard.title.title');
26
+ readonly autoAdvance = false;
27
+
28
+ private titleValue: string = '';
29
+
30
+ constructor(private readonly context: DashboardWizardContext) {
31
+ }
32
+
33
+ public isComplete(): boolean {
34
+ return this.getValue().trim() !== '';
35
+ }
36
+
37
+ public render(): TemplateResult {
38
+ if (this.titleValue === '') {
39
+ this.titleValue = this.context.getStoreData(this.key) ?? '';
40
+ }
41
+
42
+ return html`
43
+ <div class="dashboard-wizard-title">
44
+ <div class="form-group">
45
+ <label class="form-label" for="dashboard-wizard-title-input">${labels.get('dashboard.title')}</label>
46
+ <input
47
+ type="text"
48
+ id="dashboard-wizard-title-input"
49
+ class="form-control"
50
+ required="required"
51
+ .value="${live(this.titleValue)}"
52
+ @input="${(event: InputEvent) => this.setValue((event.target as HTMLInputElement).value)}"
53
+ >
54
+ </div>
55
+ </div>
56
+ `;
57
+ }
58
+
59
+ public reset(): void {
60
+ this.titleValue = '';
61
+ this.context.clearStoreData(this.key);
62
+ }
63
+
64
+ public getValue(): string {
65
+ return this.titleValue;
66
+ }
67
+
68
+ public setValue(value: string): void {
69
+ this.titleValue = value;
70
+ this.context.wizard.requestUpdate();
71
+ }
72
+
73
+ public beforeAdvance(): void {
74
+ this.context.setStoreData(this.key, this.getValue());
75
+ }
76
+
77
+ public getSummaryData(): SummaryItem[] {
78
+ const title = this.context.getStoreData(this.key);
79
+ if (!title) {
80
+ return [];
81
+ }
82
+
83
+ return [{
84
+ label: this.title,
85
+ value: title
86
+ }];
87
+ }
88
+ }
89
+
90
+ export default TitleStep;
@@ -720,17 +720,30 @@ function extendModel<D extends object, T extends ModelData<D>>(
720
720
  modelToExtend.set(pathPrefix, modelExtension, disablePublishersOnSet);
721
721
  } else {
722
722
  const _modelExtension = { ...modelExtension } as Record<string, T | [] | Record<string, never>>;
723
- for (const key of Object.keys(_modelExtension)) {
724
- const path = (pathPrefix === '') ? key : pathPrefix + '.' + key;
725
723
 
726
- modelToExtend.on(path, 'core/formElement/somePropertyChanged');
724
+ // A "leaf map" is an object whose values are all scalars (e.g. select
725
+ // options or finisher recipients keyed by their value). Its keys may
726
+ // contain dots (e.g. email addresses used as recipient keys) and must
727
+ // therefore be stored as a whole, otherwise Model.set() would split the
728
+ // key into a nested object path and thus corrupt the data.
729
+ const isLeafMap = pathPrefix !== '' && Object.values(_modelExtension).every(
730
+ (value) => value === null || typeof value !== 'object'
731
+ );
727
732
 
728
- if (_modelExtension[key] !== null && (typeof (_modelExtension[key]) === 'object' || Array.isArray(_modelExtension[key]))) {
729
- extendModel(modelToExtend, _modelExtension[key], path, disablePublishersOnSet);
730
- } else if (pathPrefix === 'properties.options') {
731
- modelToExtend.set(pathPrefix, modelExtension, disablePublishersOnSet);
732
- } else {
733
- modelToExtend.set(path, _modelExtension[key], disablePublishersOnSet);
733
+ if (isLeafMap) {
734
+ modelToExtend.on(pathPrefix, 'core/formElement/somePropertyChanged');
735
+ modelToExtend.set(pathPrefix, modelExtension, disablePublishersOnSet);
736
+ } else {
737
+ for (const key of Object.keys(_modelExtension)) {
738
+ const path = (pathPrefix === '') ? key : pathPrefix + '.' + key;
739
+
740
+ modelToExtend.on(path, 'core/formElement/somePropertyChanged');
741
+
742
+ if (_modelExtension[key] !== null && (typeof (_modelExtension[key]) === 'object' || Array.isArray(_modelExtension[key]))) {
743
+ extendModel(modelToExtend, _modelExtension[key], path, disablePublishersOnSet);
744
+ } else {
745
+ modelToExtend.set(path, _modelExtension[key], disablePublishersOnSet);
746
+ }
734
747
  }
735
748
  }
736
749
  }