@acorex-platform/framework-client 0.0.10
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.
- package/README.md +7 -0
- package/auth/README.md +3 -0
- package/common/README.md +3 -0
- package/core/README.md +4 -0
- package/fesm2022/acorex-platform-framework-client-auth.mjs +1383 -0
- package/fesm2022/acorex-platform-framework-client-auth.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-common-common-settings.provider-CbzOYCnI.mjs +190 -0
- package/fesm2022/acorex-platform-framework-client-common-common-settings.provider-CbzOYCnI.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-common.mjs +9467 -0
- package/fesm2022/acorex-platform-framework-client-common.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-core.mjs +4655 -0
- package/fesm2022/acorex-platform-framework-client-core.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-domain.mjs +3189 -0
- package/fesm2022/acorex-platform-framework-client-domain.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-builder.mjs +3439 -0
- package/fesm2022/acorex-platform-framework-client-layout-builder.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-components-binding-expression-editor-popup.component-CWVU3wXL.mjs +134 -0
- package/fesm2022/acorex-platform-framework-client-layout-components-binding-expression-editor-popup.component-CWVU3wXL.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-components.mjs +9777 -0
- package/fesm2022/acorex-platform-framework-client-layout-components.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-designer.mjs +2799 -0
- package/fesm2022/acorex-platform-framework-client-layout-designer.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-entity-attachments-page.component-CxBxxpmA.mjs +376 -0
- package/fesm2022/acorex-platform-framework-client-layout-entity-attachments-page.component-CxBxxpmA.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-entity-file-list-popup.component-l0JAHKIH.mjs +100 -0
- package/fesm2022/acorex-platform-framework-client-layout-entity-file-list-popup.component-l0JAHKIH.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-entity.mjs +26299 -0
- package/fesm2022/acorex-platform-framework-client-layout-entity.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-views.mjs +2960 -0
- package/fesm2022/acorex-platform-framework-client-layout-views.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs +4711 -0
- package/fesm2022/acorex-platform-framework-client-layout-widget-core.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-button-widget-designer.component-CpkWwygb.mjs +72 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-button-widget-designer.component-CpkWwygb.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-image-preview.popup-C_EPAvCU.mjs +29 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-image-preview.popup-C_EPAvCU.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-page-widget-designer.component-CmtTq2Mt.mjs +172 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-page-widget-designer.component-CmtTq2Mt.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-DSx0E3NV.mjs +513 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-repeater-widget-column.component-DSx0E3NV.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-tabular-data-edit-popup.component-DdfcGV1C.mjs +274 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-tabular-data-edit-popup.component-DdfcGV1C.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-tabular-data-view-popup.component-CntopmDS.mjs +64 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-tabular-data-view-popup.component-CntopmDS.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-B5kSWxGk.mjs +34 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets-text-block-widget-designer.component-B5kSWxGk.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets.mjs +28553 -0
- package/fesm2022/acorex-platform-framework-client-layout-widgets.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-native.mjs +155 -0
- package/fesm2022/acorex-platform-framework-client-native.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-runtime.mjs +911 -0
- package/fesm2022/acorex-platform-framework-client-runtime.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-entity-master-create-view.component-DnTHwsDJ.mjs +160 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-entity-master-create-view.component-DnTHwsDJ.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-entity-master-modify-view.component-BpR9g6QS.mjs +121 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-entity-master-modify-view.component-BpR9g6QS.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-entity-master-single-view.component-D1a7UI71.mjs +236 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-entity-master-single-view.component-D1a7UI71.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-error-401.component-D--_MIar.mjs +48 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-error-401.component-D--_MIar.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-error-404.component-Byyqk8pH.mjs +42 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-error-404.component-Byyqk8pH.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-error-offline.component-DR6G8gPC.mjs +19 -0
- package/fesm2022/acorex-platform-framework-client-themes-default-error-offline.component-DR6G8gPC.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-default.mjs +4160 -0
- package/fesm2022/acorex-platform-framework-client-themes-default.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-icon-chooser-column.component-CXSGiTCK.mjs +55 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-icon-chooser-column.component-CXSGiTCK.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-icon-chooser-view.component-BY5clQ1J.mjs +57 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-icon-chooser-view.component-BY5clQ1J.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-settings.provider-DEL2YOTP.mjs +234 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-settings.provider-DEL2YOTP.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-theme-color-chooser-column.component-BXl8u4Yp.mjs +94 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-theme-color-chooser-column.component-BXl8u4Yp.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-theme-color-chooser-view.component-i2sOOkVj.mjs +86 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared-theme-color-chooser-view.component-i2sOOkVj.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared.mjs +2375 -0
- package/fesm2022/acorex-platform-framework-client-themes-shared.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client-workflow.mjs +2836 -0
- package/fesm2022/acorex-platform-framework-client-workflow.mjs.map +1 -0
- package/fesm2022/acorex-platform-framework-client.mjs +6 -0
- package/fesm2022/acorex-platform-framework-client.mjs.map +1 -0
- package/layout/builder/README.md +1578 -0
- package/layout/components/README.md +3 -0
- package/layout/designer/README.md +4 -0
- package/layout/entity/README.md +4 -0
- package/layout/views/README.md +3 -0
- package/layout/widget-core/README.md +4 -0
- package/layout/widgets/README.md +3 -0
- package/native/README.md +4 -0
- package/package.json +98 -0
- package/runtime/README.md +3 -0
- package/themes/default/README.md +3 -0
- package/themes/shared/README.md +3 -0
- package/types/acorex-platform-framework-client-auth.d.ts +690 -0
- package/types/acorex-platform-framework-client-common.d.ts +3287 -0
- package/types/acorex-platform-framework-client-core.d.ts +1849 -0
- package/types/acorex-platform-framework-client-domain.d.ts +1968 -0
- package/types/acorex-platform-framework-client-layout-builder.d.ts +1058 -0
- package/types/acorex-platform-framework-client-layout-components.d.ts +3362 -0
- package/types/acorex-platform-framework-client-layout-designer.d.ts +501 -0
- package/types/acorex-platform-framework-client-layout-entity.d.ts +4374 -0
- package/types/acorex-platform-framework-client-layout-views.d.ts +975 -0
- package/types/acorex-platform-framework-client-layout-widget-core.d.ts +1080 -0
- package/types/acorex-platform-framework-client-layout-widgets.d.ts +5263 -0
- package/types/acorex-platform-framework-client-native.d.ts +28 -0
- package/types/acorex-platform-framework-client-runtime.d.ts +301 -0
- package/types/acorex-platform-framework-client-themes-default.d.ts +889 -0
- package/types/acorex-platform-framework-client-themes-shared.d.ts +202 -0
- package/types/acorex-platform-framework-client-workflow.d.ts +1345 -0
- package/types/acorex-platform-framework-client.d.ts +2 -0
- package/workflow/README.md +4 -0
|
@@ -0,0 +1,3439 @@
|
|
|
1
|
+
import * as i5 from '@angular/common';
|
|
2
|
+
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { Injectable, inject, input, model, signal, computed, effect, output, viewChild, ChangeDetectionStrategy, Component, NgModule, EventEmitter, ElementRef, DestroyRef, untracked, Output } from '@angular/core';
|
|
5
|
+
import { provideCommandSetups, AXPCommandService } from '@acorex-platform/framework-client/runtime';
|
|
6
|
+
import { AXPopupService } from '@acorex/components/popup';
|
|
7
|
+
import * as i4 from '@acorex-platform/framework-client/core';
|
|
8
|
+
import { AXPHookService, resolveDialogActionShortcuts, DEFAULT_CANCEL_DIALOG_ACTION_SHORTCUTS, DEFAULT_SUBMIT_DIALOG_ACTION_SHORTCUTS, findOverlayContainerAncestor, getNestedVisibleOverlayPanes, getTopVisibleOverlayContainer, AXPExpressionEvaluatorService, AXPKeyboardShortcutRegistry, normalizeDialogActionShortcutChord, isDialogShortcutAllowedInEditableField, resolveConfiguredFooterActionShortcuts, AXPComponentSlotModule, AXPContextStore } from '@acorex-platform/framework-client/core';
|
|
9
|
+
export { DEFAULT_CANCEL_DIALOG_ACTION_SHORTCUTS, DEFAULT_SUBMIT_DIALOG_ACTION_SHORTCUTS } from '@acorex-platform/framework-client/core';
|
|
10
|
+
import * as i1 from '@acorex-platform/framework-client/layout/widget-core';
|
|
11
|
+
import { AXPWidgetSerializationHelper, AXPWidgetContainerComponent, AXPWidgetCoreModule, AXPWidgetRegistryService } from '@acorex-platform/framework-client/layout/widget-core';
|
|
12
|
+
import { cloneDeep, isNil, set, isEqual, merge } from 'lodash-es';
|
|
13
|
+
import { AXPPageStatus } from '@acorex-platform/framework-shared/widget-core';
|
|
14
|
+
import * as i2 from '@acorex/components/form';
|
|
15
|
+
import { AXFormComponent, AXFormModule } from '@acorex/components/form';
|
|
16
|
+
import { Subject, debounceTime, distinctUntilChanged, startWith } from 'rxjs';
|
|
17
|
+
import { AXP_PREVIEW_WIDGET_FIELD_COMMAND_KEY, AXPKeyboardShortcutPriority } from '@acorex-platform/framework-shared/core';
|
|
18
|
+
import { AXOverlayService } from '@acorex/cdk/overlay';
|
|
19
|
+
import * as i1$1 from '@acorex/components/button';
|
|
20
|
+
import { AXButtonModule } from '@acorex/components/button';
|
|
21
|
+
import * as i2$1 from '@acorex/components/decorators';
|
|
22
|
+
import { AXDecoratorModule } from '@acorex/components/decorators';
|
|
23
|
+
import * as i3 from '@acorex/components/loading';
|
|
24
|
+
import { AXLoadingModule } from '@acorex/components/loading';
|
|
25
|
+
import { AXBasePageComponent } from '@acorex/components/page';
|
|
26
|
+
import * as i6 from '@acorex/core/translation';
|
|
27
|
+
import { AXTranslationService, AXTranslationModule } from '@acorex/core/translation';
|
|
28
|
+
import { AXPUnsavedChangesConfirmService } from '@acorex-platform/framework-client/common';
|
|
29
|
+
import { AXP_ENTITY_DEFINITION_CRUD_SERVICE } from '@acorex-platform/framework-client/domain';
|
|
30
|
+
|
|
31
|
+
//#region ---- Dialog Action Shortcut Utilities (re-export) ----
|
|
32
|
+
//#endregion
|
|
33
|
+
|
|
34
|
+
/** Fallback {@link AXPDialogRef} when the popup is dismissed without a footer action (e.g. header close). */
|
|
35
|
+
function createDismissedDialogRef(context = () => ({})) {
|
|
36
|
+
return {
|
|
37
|
+
close: () => { },
|
|
38
|
+
context,
|
|
39
|
+
action: () => 'cancel',
|
|
40
|
+
setLoading: () => { },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
|
|
45
|
+
//#region ---- Imports ----
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region ---- Before open ----
|
|
48
|
+
/**
|
|
49
|
+
* Runs after dialog options and context are prepared and **before** footer customization and popup open.
|
|
50
|
+
* Listeners may mutate {@link AXPLayoutBuilderDialogBeforeOpenPayload.context} by reference.
|
|
51
|
+
*/
|
|
52
|
+
const AXP_LAYOUT_BUILDER_DIALOG_BEFORE_OPEN_HOOK_KEY = 'layout-builder.dialog.before-open';
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ---- Footer actions ----
|
|
55
|
+
/**
|
|
56
|
+
* Runs after builder-defined footer actions exist and **before** the dialog opens.
|
|
57
|
+
* Listeners receive the live `actions.footer.prefix` / `suffix` arrays (same references as the dialog)
|
|
58
|
+
* so they may push, splice, filter, or replace items. They may also mutate {@link AXPLayoutBuilderDialogFooterPayload.context} by reference.
|
|
59
|
+
*/
|
|
60
|
+
const AXP_LAYOUT_BUILDER_DIALOG_CONFIG_HOOK_KEY = 'layout-builder.dialog.config';
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ---- Context updates (after open) ----
|
|
63
|
+
/**
|
|
64
|
+
* Runs whenever the dialog layout builder context changes (debounced upstream), **after** the popup is visible.
|
|
65
|
+
* Use for side effects that depend on live context (for example values updated by widgets after render).
|
|
66
|
+
* Payload mirrors `AXPDialogRendererComponent` semantics: {@link AXPLayoutBuilderDialogContextChangedPayload.getContext},
|
|
67
|
+
* {@link AXPLayoutBuilderDialogContextChangedPayload.patchContext}, and optional loading state.
|
|
68
|
+
*/
|
|
69
|
+
const AXP_LAYOUT_BUILDER_DIALOG_CONTEXT_CHANGED_HOOK_KEY = 'layout-builder.dialog.context-changed';
|
|
70
|
+
//#endregion
|
|
71
|
+
|
|
72
|
+
class AXPLayoutConversionService {
|
|
73
|
+
constructor() {
|
|
74
|
+
//#region ---- Caching ----
|
|
75
|
+
this.widgetTreeCache = new Map();
|
|
76
|
+
this.formDefinitionCache = new Map();
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region ---- Public Methods ----
|
|
80
|
+
/**
|
|
81
|
+
* Convert AXPDynamicFormDefinition to AXPWidgetNode tree structure
|
|
82
|
+
* Groups become Fieldset Layouts with Form Field widgets as children
|
|
83
|
+
* Fields become Form Field widgets with Editor widgets as children
|
|
84
|
+
*/
|
|
85
|
+
convertFormDefinition(formDefinition) {
|
|
86
|
+
// Create cache key based on form definition content
|
|
87
|
+
const cacheKey = this.createFormDefinitionCacheKey(formDefinition);
|
|
88
|
+
// Check cache first
|
|
89
|
+
if (this.widgetTreeCache.has(cacheKey)) {
|
|
90
|
+
return this.widgetTreeCache.get(cacheKey);
|
|
91
|
+
}
|
|
92
|
+
// Generate widget tree
|
|
93
|
+
const widgetTree = {
|
|
94
|
+
type: 'grid-layout',
|
|
95
|
+
name: 'dynamic-form-container',
|
|
96
|
+
mode: formDefinition.mode, // Preserve form-level mode
|
|
97
|
+
options: {
|
|
98
|
+
title: 'Dynamic Form',
|
|
99
|
+
grid: {
|
|
100
|
+
default: {
|
|
101
|
+
columns: 1,
|
|
102
|
+
gap: '1rem',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
children: formDefinition.groups.map((group) => this.createGroupAsFieldsetWidget(group, formDefinition.mode)),
|
|
107
|
+
};
|
|
108
|
+
// Cache the result
|
|
109
|
+
this.widgetTreeCache.set(cacheKey, widgetTree);
|
|
110
|
+
return widgetTree;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Convert AXPWidgetNode tree back to AXPDynamicFormDefinition
|
|
114
|
+
* Parses Fieldset Layouts back to Groups
|
|
115
|
+
* Parses Form Field widgets back to Fields
|
|
116
|
+
*/
|
|
117
|
+
convertWidgetTreeToFormDefinition(widgetTree) {
|
|
118
|
+
// Create cache key based on widget tree content
|
|
119
|
+
const cacheKey = this.createWidgetTreeCacheKey(widgetTree);
|
|
120
|
+
// Check cache first
|
|
121
|
+
if (this.formDefinitionCache.has(cacheKey)) {
|
|
122
|
+
return this.formDefinitionCache.get(cacheKey);
|
|
123
|
+
}
|
|
124
|
+
// Parse widget tree
|
|
125
|
+
const groups = [];
|
|
126
|
+
if (widgetTree.children) {
|
|
127
|
+
widgetTree.children.forEach((child) => {
|
|
128
|
+
if (child.type === 'fieldset-layout') {
|
|
129
|
+
const group = this.extractGroupFromFieldset(child);
|
|
130
|
+
groups.push(group);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const formDefinition = { groups };
|
|
135
|
+
// Cache the result
|
|
136
|
+
this.formDefinitionCache.set(cacheKey, formDefinition);
|
|
137
|
+
return formDefinition;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Validate that a widget tree represents a valid dynamic form structure
|
|
141
|
+
*/
|
|
142
|
+
validateFormWidgetTree(widgetTree) {
|
|
143
|
+
if (!widgetTree || widgetTree.type !== 'grid-layout') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!widgetTree.children || widgetTree.children.length === 0) {
|
|
147
|
+
return true; // Empty form is valid
|
|
148
|
+
}
|
|
149
|
+
// Check that all children are fieldset-layout widgets
|
|
150
|
+
return widgetTree.children.every((child) => child.type === 'fieldset-layout' &&
|
|
151
|
+
child.children &&
|
|
152
|
+
child.children.every((formField) => formField.type === 'form-field' && formField.children && formField.children.length > 0));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Clear all caches
|
|
156
|
+
*/
|
|
157
|
+
clearCaches() {
|
|
158
|
+
this.widgetTreeCache.clear();
|
|
159
|
+
this.formDefinitionCache.clear();
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get cache statistics
|
|
163
|
+
*/
|
|
164
|
+
getCacheStats() {
|
|
165
|
+
return {
|
|
166
|
+
widgetTreeCacheSize: this.widgetTreeCache.size,
|
|
167
|
+
formDefinitionCacheSize: this.formDefinitionCache.size,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region ---- Private Methods ----
|
|
172
|
+
/**
|
|
173
|
+
* Convert a single group to Fieldset widget structure
|
|
174
|
+
*/
|
|
175
|
+
createGroupAsFieldsetWidget(group, formMode) {
|
|
176
|
+
// Determine columns count from layout or default to 1
|
|
177
|
+
const columnsCount = 1;
|
|
178
|
+
// Use group mode if set, otherwise inherit from form mode
|
|
179
|
+
const groupMode = group.mode || formMode;
|
|
180
|
+
return {
|
|
181
|
+
type: 'fieldset-layout',
|
|
182
|
+
name: group.name,
|
|
183
|
+
mode: groupMode,
|
|
184
|
+
options: {
|
|
185
|
+
title: group.title,
|
|
186
|
+
description: group.description,
|
|
187
|
+
cols: columnsCount,
|
|
188
|
+
look: group.look || 'container', // Default to 'container' if not specified
|
|
189
|
+
},
|
|
190
|
+
children: this.createFieldWidgets(group.parameters, columnsCount, groupMode),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Convert fields to Form Field widgets
|
|
195
|
+
*/
|
|
196
|
+
createFieldWidgets(fields, columnsCount, groupMode) {
|
|
197
|
+
return fields.map((field) => this.createFormFieldWidget(field, groupMode));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Convert a single field to Form Field widget with editor as child
|
|
201
|
+
*/
|
|
202
|
+
createFormFieldWidget(field, groupMode) {
|
|
203
|
+
// Use field mode if set, otherwise inherit from group mode
|
|
204
|
+
const fieldMode = field.mode || groupMode;
|
|
205
|
+
// Ensure the editor widget also has the mode set
|
|
206
|
+
const editorWidget = { ...field.widget };
|
|
207
|
+
if (!editorWidget.mode) {
|
|
208
|
+
editorWidget.mode = fieldMode;
|
|
209
|
+
}
|
|
210
|
+
const hintOpts = field.description != null &&
|
|
211
|
+
(typeof field.description !== 'string' || field.description.trim().length > 0)
|
|
212
|
+
? { hint: field.description, hintDisplayMode: 'note' }
|
|
213
|
+
: {};
|
|
214
|
+
return {
|
|
215
|
+
type: 'form-field',
|
|
216
|
+
name: field.path,
|
|
217
|
+
mode: fieldMode,
|
|
218
|
+
options: {
|
|
219
|
+
label: field.title,
|
|
220
|
+
badge: field.badge,
|
|
221
|
+
showLabel: true,
|
|
222
|
+
...hintOpts,
|
|
223
|
+
},
|
|
224
|
+
children: [editorWidget], // The editor widget becomes a child of form-field
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Extract group information from Fieldset Layout widget
|
|
229
|
+
*/
|
|
230
|
+
extractGroupFromFieldset(fieldsetNode) {
|
|
231
|
+
const columnsCount = fieldsetNode.options?.['cols'] || 1;
|
|
232
|
+
// Extract fields directly from fieldset children
|
|
233
|
+
const fields = [];
|
|
234
|
+
if (fieldsetNode.children) {
|
|
235
|
+
fieldsetNode.children.forEach((formField) => {
|
|
236
|
+
if (formField.type === 'form-field' && formField.children && formField.children.length > 0) {
|
|
237
|
+
const field = this.extractFieldFromFormWidget(formField);
|
|
238
|
+
if (field) {
|
|
239
|
+
fields.push(field);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
name: fieldsetNode.name || `group-${Date.now()}`,
|
|
246
|
+
title: fieldsetNode.options?.['title'],
|
|
247
|
+
description: fieldsetNode.options?.['description'],
|
|
248
|
+
parameters: fields,
|
|
249
|
+
mode: fieldsetNode.mode,
|
|
250
|
+
look: fieldsetNode.options?.['look'],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Extract field information from Form Field widget
|
|
255
|
+
*/
|
|
256
|
+
extractFieldFromFormWidget(formFieldNode) {
|
|
257
|
+
if (!formFieldNode.children || formFieldNode.children.length === 0) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const editorWidget = formFieldNode.children[0];
|
|
261
|
+
return {
|
|
262
|
+
path: formFieldNode.name || editorWidget.name || `field-${Date.now()}`,
|
|
263
|
+
title: formFieldNode.options?.['label'],
|
|
264
|
+
badge: formFieldNode.options?.['badge'],
|
|
265
|
+
description: formFieldNode.options?.['hint'],
|
|
266
|
+
widget: editorWidget,
|
|
267
|
+
mode: formFieldNode.mode,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Create cache key for form definition
|
|
272
|
+
*/
|
|
273
|
+
createFormDefinitionCacheKey(formDefinition) {
|
|
274
|
+
// Create a hash-like key instead of full JSON string
|
|
275
|
+
const keyParts = [];
|
|
276
|
+
keyParts.push(`groups:${formDefinition.groups.length}`);
|
|
277
|
+
formDefinition.groups.forEach((group, groupIndex) => {
|
|
278
|
+
// Include group.mode so view vs edit (or mixed) layouts do not share a cached widget tree.
|
|
279
|
+
const groupModePart = group.mode ?? '_';
|
|
280
|
+
keyParts.push(`g${groupIndex}:${group.name}:${group.parameters.length}:${groupModePart}`);
|
|
281
|
+
keyParts.push(`gL${groupIndex}:${JSON.stringify(group.title ?? null)}:${JSON.stringify(group.description ?? null)}`);
|
|
282
|
+
group.parameters.forEach((param, paramIndex) => {
|
|
283
|
+
// Field mode must be part of the key; otherwise metadata forms that only differ by
|
|
284
|
+
// view/edit (same paths and widget types) incorrectly reuse the first cached tree.
|
|
285
|
+
const fieldModePart = param.mode ?? '_';
|
|
286
|
+
keyParts.push(`p${groupIndex}.${paramIndex}:${param.path}:${param.widget.type}:${fieldModePart}`);
|
|
287
|
+
keyParts.push(`pL${groupIndex}.${paramIndex}:${JSON.stringify(param.title ?? null)}:${JSON.stringify(param.description ?? null)}:${JSON.stringify(param.badge ?? null)}`);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
if (formDefinition.mode) {
|
|
291
|
+
keyParts.push(`mode:${formDefinition.mode}`);
|
|
292
|
+
}
|
|
293
|
+
// Join with delimiter and create a shorter hash
|
|
294
|
+
const keyString = keyParts.join('|');
|
|
295
|
+
// If still too long, create a simple hash
|
|
296
|
+
if (keyString.length > 100) {
|
|
297
|
+
return this.createSimpleHash(keyString);
|
|
298
|
+
}
|
|
299
|
+
return keyString;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Create cache key for widget tree
|
|
303
|
+
*/
|
|
304
|
+
createWidgetTreeCacheKey(widgetTree) {
|
|
305
|
+
// Create a hash-like key instead of full JSON string
|
|
306
|
+
const keyParts = [];
|
|
307
|
+
keyParts.push(`type:${widgetTree.type}`);
|
|
308
|
+
if (widgetTree.name) {
|
|
309
|
+
keyParts.push(`name:${widgetTree.name}`);
|
|
310
|
+
}
|
|
311
|
+
if (widgetTree.children) {
|
|
312
|
+
keyParts.push(`children:${widgetTree.children.length}`);
|
|
313
|
+
widgetTree.children.forEach((child, index) => {
|
|
314
|
+
keyParts.push(`c${index}:${child.type}`);
|
|
315
|
+
if (child.children) {
|
|
316
|
+
keyParts.push(`cc${index}:${child.children.length}`);
|
|
317
|
+
child.children.forEach((grandChild, gIndex) => {
|
|
318
|
+
keyParts.push(`gc${index}.${gIndex}:${grandChild.type}`);
|
|
319
|
+
if (grandChild.children) {
|
|
320
|
+
keyParts.push(`gcc${index}.${gIndex}:${grandChild.children.length}`);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
// Join with delimiter and create a shorter hash
|
|
327
|
+
const keyString = keyParts.join('|');
|
|
328
|
+
// If still too long, create a simple hash
|
|
329
|
+
if (keyString.length > 100) {
|
|
330
|
+
return this.createSimpleHash(keyString);
|
|
331
|
+
}
|
|
332
|
+
return keyString;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Create a simple hash from a string
|
|
336
|
+
*/
|
|
337
|
+
createSimpleHash(str) {
|
|
338
|
+
let hash = 0;
|
|
339
|
+
if (str.length === 0)
|
|
340
|
+
return hash.toString();
|
|
341
|
+
for (let i = 0; i < str.length; i++) {
|
|
342
|
+
const char = str.charCodeAt(i);
|
|
343
|
+
hash = (hash << 5) - hash + char;
|
|
344
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
345
|
+
}
|
|
346
|
+
return Math.abs(hash).toString(36); // Convert to base36 for shorter string
|
|
347
|
+
}
|
|
348
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutConversionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
349
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutConversionService, providedIn: 'root' }); }
|
|
350
|
+
}
|
|
351
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutConversionService, decorators: [{
|
|
352
|
+
type: Injectable,
|
|
353
|
+
args: [{
|
|
354
|
+
providedIn: 'root',
|
|
355
|
+
}]
|
|
356
|
+
}] });
|
|
357
|
+
|
|
358
|
+
//#region ---- Dialog Close Confirmation Utilities ----
|
|
359
|
+
function normalizeDialogCloseConfirmation(value) {
|
|
360
|
+
if (value === undefined || value === false) {
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
if (value === true) {
|
|
364
|
+
return { enabled: true };
|
|
365
|
+
}
|
|
366
|
+
if (value.enabled === false) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
return { enabled: true, ...value };
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
372
|
+
|
|
373
|
+
//#region ---- Inheritance Utilities ----
|
|
374
|
+
/**
|
|
375
|
+
* Resolves inherited properties from context and local values
|
|
376
|
+
*/
|
|
377
|
+
function resolveInheritedProperties(context, localValues = {}) {
|
|
378
|
+
return {
|
|
379
|
+
mode: localValues.mode ?? context.mode ?? 'edit',
|
|
380
|
+
disabled: localValues.disabled ?? context.disabled,
|
|
381
|
+
readonly: localValues.readonly ?? context.readonly,
|
|
382
|
+
direction: localValues.direction ?? context.direction,
|
|
383
|
+
visible: localValues.visible ?? context.visible,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Merges inheritance context with local overrides
|
|
388
|
+
*/
|
|
389
|
+
function mergeInheritanceContext(parentContext, localOverrides = {}) {
|
|
390
|
+
return {
|
|
391
|
+
...parentContext,
|
|
392
|
+
...localOverrides,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Generates a random string for path/name generation
|
|
397
|
+
*/
|
|
398
|
+
function generateRandomId() {
|
|
399
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Converts label to a valid path/name
|
|
403
|
+
*/
|
|
404
|
+
function labelToPath(label) {
|
|
405
|
+
return label
|
|
406
|
+
.toLowerCase()
|
|
407
|
+
.replace(/[^a-z0-9\s]/g, '') // Remove special characters
|
|
408
|
+
.replace(/\s+/g, '_') // Replace spaces with underscores
|
|
409
|
+
.substring(0, 50); // Limit length
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Generates path for value widgets based on hierarchy
|
|
413
|
+
*/
|
|
414
|
+
function generateValueWidgetPath(widgetName, formFieldName, formFieldLabel) {
|
|
415
|
+
// Priority: widget name -> form field name -> generated from label -> random
|
|
416
|
+
if (widgetName) {
|
|
417
|
+
return widgetName;
|
|
418
|
+
}
|
|
419
|
+
if (formFieldName) {
|
|
420
|
+
return formFieldName;
|
|
421
|
+
}
|
|
422
|
+
if (formFieldLabel) {
|
|
423
|
+
return labelToPath(formFieldLabel);
|
|
424
|
+
}
|
|
425
|
+
return generateRandomId();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Collects default values from widget node tree and merges them into context
|
|
429
|
+
* Only sets values for paths that don't already exist in the context
|
|
430
|
+
* @param node - The widget node to process
|
|
431
|
+
* @param context - The context object to modify in place (cloned at top level only)
|
|
432
|
+
* @param isTopLevel - Internal flag to track if this is the top-level call
|
|
433
|
+
*/
|
|
434
|
+
function collectDefaultValues(node, context = {}, isTopLevel = true) {
|
|
435
|
+
// Clone context only at the top level, then modify in place for recursive calls
|
|
436
|
+
const result = isTopLevel ? cloneDeep(context) : context;
|
|
437
|
+
// Check if this node has a defaultValue and a path
|
|
438
|
+
// Note: We check for both node.defaultValue and also look in node.options.defaultValue as fallback
|
|
439
|
+
const defaultValue = node.defaultValue !== undefined ? node.defaultValue : node.options?.defaultValue;
|
|
440
|
+
if (defaultValue !== undefined && !isNil(defaultValue) && node.path) {
|
|
441
|
+
// Check if path exists in context using lodash get equivalent check
|
|
442
|
+
const currentValue = getNestedValue(result, node.path);
|
|
443
|
+
if (currentValue === undefined) {
|
|
444
|
+
// Clone the defaultValue to avoid reference issues (especially for Date objects)
|
|
445
|
+
const clonedValue = defaultValue instanceof Date ? new Date(defaultValue.getTime()) : cloneDeep(defaultValue);
|
|
446
|
+
set(result, node.path, clonedValue);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Recursively process children - pass result object to accumulate values in place
|
|
450
|
+
if (node.children && Array.isArray(node.children)) {
|
|
451
|
+
for (const child of node.children) {
|
|
452
|
+
collectDefaultValues(child, result, false);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Gets a nested value from an object using dot notation path
|
|
459
|
+
*/
|
|
460
|
+
function getNestedValue(obj, path) {
|
|
461
|
+
const keys = path.split('.');
|
|
462
|
+
let current = obj;
|
|
463
|
+
for (const key of keys) {
|
|
464
|
+
if (current === undefined || current === null || typeof current !== 'object') {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
current = current[key];
|
|
468
|
+
}
|
|
469
|
+
return current;
|
|
470
|
+
}
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region ---- Service Implementation ----
|
|
473
|
+
class AXPLayoutBuilderService {
|
|
474
|
+
constructor() {
|
|
475
|
+
this.popupService = inject(AXPopupService);
|
|
476
|
+
this.hookService = inject(AXPHookService, { optional: true }) ?? undefined;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Create a new layout builder
|
|
480
|
+
*/
|
|
481
|
+
create() {
|
|
482
|
+
return new LayoutBuilder(this.popupService, this.hookService);
|
|
483
|
+
}
|
|
484
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutBuilderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
485
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutBuilderService, providedIn: 'root' }); }
|
|
486
|
+
}
|
|
487
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutBuilderService, decorators: [{
|
|
488
|
+
type: Injectable,
|
|
489
|
+
args: [{
|
|
490
|
+
providedIn: 'root',
|
|
491
|
+
}]
|
|
492
|
+
}] });
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region ---- Layout Builder Implementation ----
|
|
495
|
+
/**
|
|
496
|
+
* Main layout builder - Single Responsibility: Layout orchestration
|
|
497
|
+
* Open/Closed: Extensible through container delegates
|
|
498
|
+
*/
|
|
499
|
+
class LayoutBuilder {
|
|
500
|
+
constructor(popupService, hookService) {
|
|
501
|
+
this.popupService = popupService;
|
|
502
|
+
this.hookService = hookService;
|
|
503
|
+
this.root = {
|
|
504
|
+
children: [],
|
|
505
|
+
mode: 'edit',
|
|
506
|
+
type: 'flex-layout',
|
|
507
|
+
};
|
|
508
|
+
this.inheritanceContext = {
|
|
509
|
+
mode: 'edit',
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// Predefined layout containers at root level - delegate pattern required
|
|
513
|
+
grid(delegate) {
|
|
514
|
+
const container = new GridContainerBuilder();
|
|
515
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
516
|
+
if (delegate) {
|
|
517
|
+
delegate(container);
|
|
518
|
+
}
|
|
519
|
+
//this.state.children!.push(container.build());
|
|
520
|
+
this.root = container.build();
|
|
521
|
+
return this;
|
|
522
|
+
}
|
|
523
|
+
flex(delegate) {
|
|
524
|
+
const container = new FlexContainerBuilder();
|
|
525
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
526
|
+
if (delegate) {
|
|
527
|
+
delegate(container);
|
|
528
|
+
}
|
|
529
|
+
const built = container.build();
|
|
530
|
+
// Step/dialog content usually calls flex() once; replace the default empty flex root instead of nesting flex inside flex.
|
|
531
|
+
if (this.root.type === 'flex-layout' &&
|
|
532
|
+
(!this.root.children || this.root.children.length === 0) &&
|
|
533
|
+
!this.root.options) {
|
|
534
|
+
this.root = built;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
if (!this.root.children) {
|
|
538
|
+
this.root.children = [];
|
|
539
|
+
}
|
|
540
|
+
this.root.children.push(built);
|
|
541
|
+
}
|
|
542
|
+
return this;
|
|
543
|
+
}
|
|
544
|
+
panel(delegate) {
|
|
545
|
+
const container = new PanelContainerBuilder();
|
|
546
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
547
|
+
if (delegate) {
|
|
548
|
+
delegate(container);
|
|
549
|
+
}
|
|
550
|
+
this.root.children.push(container.build());
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
page(delegate) {
|
|
554
|
+
const container = new PageContainerBuilder();
|
|
555
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
556
|
+
if (delegate) {
|
|
557
|
+
delegate(container);
|
|
558
|
+
}
|
|
559
|
+
this.root.children.push(container.build());
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
tabset(delegate) {
|
|
563
|
+
const container = new TabsetContainerBuilder();
|
|
564
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
565
|
+
if (delegate) {
|
|
566
|
+
delegate(container);
|
|
567
|
+
}
|
|
568
|
+
this.root.children.push(container.build());
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
fieldset(delegate) {
|
|
572
|
+
const container = new FieldsetContainerBuilder();
|
|
573
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
574
|
+
if (delegate) {
|
|
575
|
+
delegate(container);
|
|
576
|
+
}
|
|
577
|
+
this.root.children.push(container.build());
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
dialog(delegate) {
|
|
581
|
+
if (!this.popupService) {
|
|
582
|
+
throw new Error('LayoutBuilder requires AXPopupService to create dialogs. Please inject it in the service constructor.');
|
|
583
|
+
}
|
|
584
|
+
const container = new DialogContainerBuilder(this.popupService, this.hookService);
|
|
585
|
+
if (delegate) {
|
|
586
|
+
delegate(container);
|
|
587
|
+
}
|
|
588
|
+
return container;
|
|
589
|
+
}
|
|
590
|
+
stepWizard(delegate) {
|
|
591
|
+
const wizard = new StepWizardBuilder();
|
|
592
|
+
wizard.withInheritanceContext(this.inheritanceContext);
|
|
593
|
+
if (delegate) {
|
|
594
|
+
delegate(wizard);
|
|
595
|
+
}
|
|
596
|
+
this.root.children.push(wizard.build());
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
formField(label, delegate) {
|
|
600
|
+
const field = new FormFieldBuilder(label);
|
|
601
|
+
field.withInheritanceContext(this.inheritanceContext);
|
|
602
|
+
if (delegate) {
|
|
603
|
+
delegate(field);
|
|
604
|
+
}
|
|
605
|
+
this.root.children.push(field.build());
|
|
606
|
+
return this;
|
|
607
|
+
}
|
|
608
|
+
build() {
|
|
609
|
+
const r = this.root;
|
|
610
|
+
const node = {
|
|
611
|
+
type: r.type,
|
|
612
|
+
...(r.mode !== undefined ? { mode: r.mode } : {}),
|
|
613
|
+
...(r.children !== undefined ? { children: r.children } : {}),
|
|
614
|
+
...(r.options !== undefined ? { options: r.options } : {}),
|
|
615
|
+
...(r.name !== undefined ? { name: r.name } : {}),
|
|
616
|
+
...(r.path !== undefined ? { path: r.path } : {}),
|
|
617
|
+
...(r.visible !== undefined ? { visible: r.visible } : {}),
|
|
618
|
+
...(r.defaultValue !== undefined ? { defaultValue: r.defaultValue } : {}),
|
|
619
|
+
...(r.triggers !== undefined ? { triggers: r.triggers } : {}),
|
|
620
|
+
...(r.meta !== undefined ? { meta: r.meta } : {}),
|
|
621
|
+
...(r.valueTransforms !== undefined ? { valueTransforms: r.valueTransforms } : {}),
|
|
622
|
+
};
|
|
623
|
+
return node;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Converts the built widget node to JSON string
|
|
627
|
+
* @param options - Serialization options
|
|
628
|
+
* @returns JSON string representation of the widget node
|
|
629
|
+
*/
|
|
630
|
+
toJson(options) {
|
|
631
|
+
const node = this.build();
|
|
632
|
+
return AXPWidgetSerializationHelper.toJson(node, options);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region ---- Container Builder Implementation ----
|
|
637
|
+
/**
|
|
638
|
+
* Abstract base container builder - Open/Closed Principle
|
|
639
|
+
* Provides common functionality for all container types
|
|
640
|
+
*/
|
|
641
|
+
class BaseContainerBuilder {
|
|
642
|
+
constructor(containerType) {
|
|
643
|
+
this.containerState = {
|
|
644
|
+
mode: 'edit',
|
|
645
|
+
type: 'flex-layout',
|
|
646
|
+
children: [],
|
|
647
|
+
};
|
|
648
|
+
this.inheritanceContext = {};
|
|
649
|
+
this.containerState.type = containerType;
|
|
650
|
+
}
|
|
651
|
+
// Base methods shared by all containers
|
|
652
|
+
ensureChildren() {
|
|
653
|
+
this.containerState.children = this.containerState.children || [];
|
|
654
|
+
}
|
|
655
|
+
addWidget(type, options) {
|
|
656
|
+
const child = new WidgetBuilder();
|
|
657
|
+
child.type(type);
|
|
658
|
+
// For value widgets, ensure path is provided
|
|
659
|
+
const widgetOptions = options ?? {};
|
|
660
|
+
const path = widgetOptions.path;
|
|
661
|
+
const name = widgetOptions.name;
|
|
662
|
+
if (this.isValueWidget(type) && !path) {
|
|
663
|
+
throw new Error(`Value widget '${type}' requires a 'path' property`);
|
|
664
|
+
}
|
|
665
|
+
// Set name and path in widget state, not options
|
|
666
|
+
if (name) {
|
|
667
|
+
child.name(name);
|
|
668
|
+
}
|
|
669
|
+
if (path) {
|
|
670
|
+
child.path(path);
|
|
671
|
+
}
|
|
672
|
+
// Remove name and path from options
|
|
673
|
+
const { name: _, path: __, ...cleanOptions } = widgetOptions;
|
|
674
|
+
// IMPORTANT: Apply inheritance context BEFORE setting options
|
|
675
|
+
// This ensures that inherited properties (readonly, disabled, etc.) are applied first
|
|
676
|
+
// Then user-provided options will override them if explicitly set
|
|
677
|
+
child.withInheritanceContext(this.inheritanceContext);
|
|
678
|
+
child.options(cleanOptions);
|
|
679
|
+
this.ensureChildren();
|
|
680
|
+
this.containerState.children.push(child.build());
|
|
681
|
+
return this;
|
|
682
|
+
}
|
|
683
|
+
isValueWidget(type) {
|
|
684
|
+
const valueWidgetTypes = [
|
|
685
|
+
'text-editor',
|
|
686
|
+
'large-text-editor',
|
|
687
|
+
'rich-text-editor',
|
|
688
|
+
'password-editor',
|
|
689
|
+
'number-editor',
|
|
690
|
+
'select-editor',
|
|
691
|
+
'lookup-editor',
|
|
692
|
+
'entity-definition-provider-editor',
|
|
693
|
+
'selection-list-editor',
|
|
694
|
+
'date-time-editor',
|
|
695
|
+
'toggle-editor',
|
|
696
|
+
'color-editor',
|
|
697
|
+
];
|
|
698
|
+
return valueWidgetTypes.includes(type);
|
|
699
|
+
}
|
|
700
|
+
build() {
|
|
701
|
+
const result = {
|
|
702
|
+
type: this.containerState.type,
|
|
703
|
+
children: this.containerState.children,
|
|
704
|
+
options: this.containerState.options,
|
|
705
|
+
mode: this.containerState.mode,
|
|
706
|
+
visible: this.containerState.visible,
|
|
707
|
+
};
|
|
708
|
+
// Add name with _form_field suffix for form fields
|
|
709
|
+
if (this.containerState.type === 'form-field') {
|
|
710
|
+
if (this.containerState.name) {
|
|
711
|
+
result.name = this.containerState.name;
|
|
712
|
+
}
|
|
713
|
+
else if (this.containerState.options?.['label']) {
|
|
714
|
+
result.name = labelToPath(this.containerState.options['label']) + '_form_field';
|
|
715
|
+
}
|
|
716
|
+
// Form fields don't have path
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
// Other containers can have name and path
|
|
720
|
+
if (this.containerState.name) {
|
|
721
|
+
result.name = this.containerState.name;
|
|
722
|
+
}
|
|
723
|
+
if (this.containerState.path) {
|
|
724
|
+
result.path = this.containerState.path;
|
|
725
|
+
}
|
|
726
|
+
if (this.containerState.defaultValue !== undefined) {
|
|
727
|
+
result.defaultValue = this.containerState.defaultValue;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Base container builder mixin - Interface Segregation Principle
|
|
735
|
+
* Provides common container operations
|
|
736
|
+
*/
|
|
737
|
+
class BaseContainerMixin extends BaseContainerBuilder {
|
|
738
|
+
name(name) {
|
|
739
|
+
this.containerState.name = name;
|
|
740
|
+
return this;
|
|
741
|
+
}
|
|
742
|
+
path(path) {
|
|
743
|
+
this.containerState.path = path;
|
|
744
|
+
return this;
|
|
745
|
+
}
|
|
746
|
+
mode(mode) {
|
|
747
|
+
this.containerState.mode = mode;
|
|
748
|
+
this.inheritanceContext.mode = mode;
|
|
749
|
+
return this;
|
|
750
|
+
}
|
|
751
|
+
visible(condition) {
|
|
752
|
+
if (!this.containerState.options)
|
|
753
|
+
this.containerState.options = {};
|
|
754
|
+
this.containerState.options['visible'] = condition;
|
|
755
|
+
this.inheritanceContext.visible = condition;
|
|
756
|
+
return this;
|
|
757
|
+
}
|
|
758
|
+
disabled(condition) {
|
|
759
|
+
if (!this.containerState.options)
|
|
760
|
+
this.containerState.options = {};
|
|
761
|
+
this.containerState.options['disabled'] = condition;
|
|
762
|
+
this.inheritanceContext.disabled = condition;
|
|
763
|
+
return this;
|
|
764
|
+
}
|
|
765
|
+
readonly(condition) {
|
|
766
|
+
if (!this.containerState.options)
|
|
767
|
+
this.containerState.options = {};
|
|
768
|
+
this.containerState.options['readonly'] = condition;
|
|
769
|
+
this.inheritanceContext.readonly = condition;
|
|
770
|
+
return this;
|
|
771
|
+
}
|
|
772
|
+
direction(direction) {
|
|
773
|
+
if (!this.containerState.options)
|
|
774
|
+
this.containerState.options = {};
|
|
775
|
+
this.containerState.options['direction'] = direction;
|
|
776
|
+
this.inheritanceContext.direction = direction;
|
|
777
|
+
return this;
|
|
778
|
+
}
|
|
779
|
+
// Inheritance context methods
|
|
780
|
+
withInheritanceContext(context) {
|
|
781
|
+
this.inheritanceContext = mergeInheritanceContext(context);
|
|
782
|
+
return this;
|
|
783
|
+
}
|
|
784
|
+
getInheritanceContext() {
|
|
785
|
+
return { ...this.inheritanceContext };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Layout container mixin - Interface Segregation Principle
|
|
790
|
+
* Provides layout-specific operations
|
|
791
|
+
*/
|
|
792
|
+
class LayoutContainerMixin extends BaseContainerMixin {
|
|
793
|
+
layout(value) {
|
|
794
|
+
// Map layout intent to grid item sizing so containers like `form-field`
|
|
795
|
+
// can span multiple columns/rows inside grid/fieldset layouts.
|
|
796
|
+
if (!this.containerState.options)
|
|
797
|
+
this.containerState.options = {};
|
|
798
|
+
if (typeof value === 'number') {
|
|
799
|
+
this.containerState.options.colSpan = value;
|
|
800
|
+
}
|
|
801
|
+
else if (value) {
|
|
802
|
+
const positions = value.positions;
|
|
803
|
+
if (positions) {
|
|
804
|
+
const placement = positions?.lg ?? positions?.xl ?? positions?.xxl ?? positions?.md ?? positions?.sm;
|
|
805
|
+
if (placement) {
|
|
806
|
+
const opts = this.containerState.options;
|
|
807
|
+
if (placement.colSpan != null)
|
|
808
|
+
opts.colSpan = placement.colSpan;
|
|
809
|
+
if (placement.colStart != null)
|
|
810
|
+
opts.colStart = placement.colStart;
|
|
811
|
+
if (placement.colEnd != null)
|
|
812
|
+
opts.colEnd = placement.colEnd;
|
|
813
|
+
if (placement.rowSpan != null)
|
|
814
|
+
opts.rowSpan = placement.rowSpan;
|
|
815
|
+
if (placement.rowStart != null)
|
|
816
|
+
opts.rowStart = placement.rowStart;
|
|
817
|
+
if (placement.rowEnd != null)
|
|
818
|
+
opts.rowEnd = placement.rowEnd;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return this;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Child container mixin - Interface Segregation Principle
|
|
827
|
+
* Provides child container management
|
|
828
|
+
*/
|
|
829
|
+
class ChildContainerMixin extends LayoutContainerMixin {
|
|
830
|
+
grid(delegate) {
|
|
831
|
+
const container = new GridContainerBuilder();
|
|
832
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
833
|
+
if (delegate) {
|
|
834
|
+
delegate(container);
|
|
835
|
+
}
|
|
836
|
+
this.ensureChildren();
|
|
837
|
+
this.containerState.children.push(container.build());
|
|
838
|
+
return this;
|
|
839
|
+
}
|
|
840
|
+
flex(delegate) {
|
|
841
|
+
const container = new FlexContainerBuilder();
|
|
842
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
843
|
+
if (delegate) {
|
|
844
|
+
delegate(container);
|
|
845
|
+
}
|
|
846
|
+
this.ensureChildren();
|
|
847
|
+
this.containerState.children.push(container.build());
|
|
848
|
+
return this;
|
|
849
|
+
}
|
|
850
|
+
panel(delegate) {
|
|
851
|
+
const container = new PanelContainerBuilder();
|
|
852
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
853
|
+
if (delegate) {
|
|
854
|
+
delegate(container);
|
|
855
|
+
}
|
|
856
|
+
this.ensureChildren();
|
|
857
|
+
this.containerState.children.push(container.build());
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
page(delegate) {
|
|
861
|
+
const container = new PageContainerBuilder();
|
|
862
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
863
|
+
if (delegate) {
|
|
864
|
+
delegate(container);
|
|
865
|
+
}
|
|
866
|
+
this.ensureChildren();
|
|
867
|
+
this.containerState.children.push(container.build());
|
|
868
|
+
return this;
|
|
869
|
+
}
|
|
870
|
+
tabset(delegate) {
|
|
871
|
+
const container = new TabsetContainerBuilder();
|
|
872
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
873
|
+
if (delegate) {
|
|
874
|
+
delegate(container);
|
|
875
|
+
}
|
|
876
|
+
this.ensureChildren();
|
|
877
|
+
this.containerState.children.push(container.build());
|
|
878
|
+
return this;
|
|
879
|
+
}
|
|
880
|
+
fieldset(delegate) {
|
|
881
|
+
const container = new FieldsetContainerBuilder();
|
|
882
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
883
|
+
if (delegate) {
|
|
884
|
+
delegate(container);
|
|
885
|
+
}
|
|
886
|
+
this.ensureChildren();
|
|
887
|
+
this.containerState.children.push(container.build());
|
|
888
|
+
return this;
|
|
889
|
+
}
|
|
890
|
+
dialog(delegate) {
|
|
891
|
+
const container = new DialogContainerBuilder(); // Will use inject() fallback
|
|
892
|
+
if (delegate) {
|
|
893
|
+
delegate(container);
|
|
894
|
+
}
|
|
895
|
+
return container;
|
|
896
|
+
}
|
|
897
|
+
stepWizard(delegate) {
|
|
898
|
+
const wizard = new StepWizardBuilder();
|
|
899
|
+
wizard.withInheritanceContext(this.inheritanceContext);
|
|
900
|
+
if (delegate) {
|
|
901
|
+
delegate(wizard);
|
|
902
|
+
}
|
|
903
|
+
this.ensureChildren();
|
|
904
|
+
this.containerState.children.push(wizard.build());
|
|
905
|
+
return this;
|
|
906
|
+
}
|
|
907
|
+
formField(label, delegate) {
|
|
908
|
+
const field = new FormFieldBuilder(label);
|
|
909
|
+
field.withInheritanceContext(this.inheritanceContext);
|
|
910
|
+
if (delegate) {
|
|
911
|
+
delegate(field);
|
|
912
|
+
}
|
|
913
|
+
this.ensureChildren();
|
|
914
|
+
this.containerState.children.push(field.build());
|
|
915
|
+
return this;
|
|
916
|
+
}
|
|
917
|
+
dynamicForm(definition) {
|
|
918
|
+
// Get conversion service using inject() - works in any context in Angular 14+
|
|
919
|
+
let conversionService;
|
|
920
|
+
try {
|
|
921
|
+
conversionService = inject(AXPLayoutConversionService);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
// Fallback: create service instance if inject() fails (shouldn't happen in normal usage)
|
|
925
|
+
conversionService = new AXPLayoutConversionService();
|
|
926
|
+
}
|
|
927
|
+
// Convert form definition to widget node
|
|
928
|
+
const widgetNode = conversionService.convertFormDefinition(definition);
|
|
929
|
+
// Add the widget node's children to this container
|
|
930
|
+
// The converted widget node has a root grid-layout, we want its children (fieldsets)
|
|
931
|
+
if (widgetNode.children && widgetNode.children.length > 0) {
|
|
932
|
+
this.ensureChildren();
|
|
933
|
+
this.containerState.children.push(...widgetNode.children);
|
|
934
|
+
}
|
|
935
|
+
return this;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Widget container mixin - Interface Segregation Principle
|
|
940
|
+
* Provides widget creation operations
|
|
941
|
+
*/
|
|
942
|
+
class WidgetContainerMixin extends ChildContainerMixin {
|
|
943
|
+
textBox(options) {
|
|
944
|
+
this.addWidget('text-editor', options);
|
|
945
|
+
return this;
|
|
946
|
+
}
|
|
947
|
+
largeTextBox(options) {
|
|
948
|
+
this.addWidget('large-text-editor', options);
|
|
949
|
+
return this;
|
|
950
|
+
}
|
|
951
|
+
richText(options) {
|
|
952
|
+
this.addWidget('rich-text-editor', options);
|
|
953
|
+
return this;
|
|
954
|
+
}
|
|
955
|
+
passwordBox(options) {
|
|
956
|
+
this.addWidget('password-editor', options);
|
|
957
|
+
return this;
|
|
958
|
+
}
|
|
959
|
+
numberBox(options) {
|
|
960
|
+
this.addWidget('number-editor', options);
|
|
961
|
+
return this;
|
|
962
|
+
}
|
|
963
|
+
selectBox(options) {
|
|
964
|
+
this.addWidget('select-editor', options);
|
|
965
|
+
return this;
|
|
966
|
+
}
|
|
967
|
+
lookupBox(options) {
|
|
968
|
+
this.addWidget('lookup-editor', options);
|
|
969
|
+
return this;
|
|
970
|
+
}
|
|
971
|
+
selectionList(options) {
|
|
972
|
+
this.addWidget('selection-list-editor', options);
|
|
973
|
+
return this;
|
|
974
|
+
}
|
|
975
|
+
dateTimeBox(options) {
|
|
976
|
+
this.addWidget('date-time-editor', options);
|
|
977
|
+
return this;
|
|
978
|
+
}
|
|
979
|
+
toggleSwitch(options) {
|
|
980
|
+
this.addWidget('toggle-editor', options);
|
|
981
|
+
return this;
|
|
982
|
+
}
|
|
983
|
+
colorBox(options) {
|
|
984
|
+
this.addWidget('color-editor', options);
|
|
985
|
+
return this;
|
|
986
|
+
}
|
|
987
|
+
list(delegate) {
|
|
988
|
+
const container = new ListWidgetBuilder();
|
|
989
|
+
container.withInheritanceContext(this.inheritanceContext);
|
|
990
|
+
if (delegate) {
|
|
991
|
+
delegate(container);
|
|
992
|
+
}
|
|
993
|
+
this.ensureChildren();
|
|
994
|
+
this.containerState.children.push(container.build());
|
|
995
|
+
return this;
|
|
996
|
+
}
|
|
997
|
+
customWidget(type, optionsOrDelegate, delegate) {
|
|
998
|
+
const child = new WidgetBuilder();
|
|
999
|
+
child.type(type);
|
|
1000
|
+
// Determine if second parameter is delegate or options
|
|
1001
|
+
const isDelegate = typeof optionsOrDelegate === 'function';
|
|
1002
|
+
const options = isDelegate ? undefined : optionsOrDelegate;
|
|
1003
|
+
const actualDelegate = isDelegate ? optionsOrDelegate : delegate;
|
|
1004
|
+
// Apply inheritance context BEFORE setting options or calling delegate
|
|
1005
|
+
child.withInheritanceContext(this.inheritanceContext);
|
|
1006
|
+
// If options are provided (non-delegate overload), handle them
|
|
1007
|
+
if (!isDelegate && options !== undefined) {
|
|
1008
|
+
const widgetOptions = options;
|
|
1009
|
+
const path = widgetOptions.path;
|
|
1010
|
+
const name = widgetOptions.name;
|
|
1011
|
+
// Set name and path in widget state, not options
|
|
1012
|
+
if (name) {
|
|
1013
|
+
child.name(name);
|
|
1014
|
+
}
|
|
1015
|
+
if (path) {
|
|
1016
|
+
child.path(path);
|
|
1017
|
+
}
|
|
1018
|
+
// Remove name and path from options
|
|
1019
|
+
const { name: _, path: __, ...cleanOptions } = widgetOptions;
|
|
1020
|
+
child.options(cleanOptions);
|
|
1021
|
+
}
|
|
1022
|
+
// If delegate is provided, call it to allow setting path, options, and other control methods
|
|
1023
|
+
if (actualDelegate) {
|
|
1024
|
+
actualDelegate(child);
|
|
1025
|
+
}
|
|
1026
|
+
// Build the widget to check if path is set (for value widgets)
|
|
1027
|
+
const builtWidget = child.build();
|
|
1028
|
+
// Check if value widget requires path
|
|
1029
|
+
const valueWidgetTypes = [
|
|
1030
|
+
'text-editor',
|
|
1031
|
+
'large-text-editor',
|
|
1032
|
+
'rich-text-editor',
|
|
1033
|
+
'password-editor',
|
|
1034
|
+
'number-editor',
|
|
1035
|
+
'select-editor',
|
|
1036
|
+
'lookup-editor',
|
|
1037
|
+
'entity-definition-provider-editor',
|
|
1038
|
+
'selection-list-editor',
|
|
1039
|
+
'date-time-editor',
|
|
1040
|
+
'toggle-editor',
|
|
1041
|
+
'color-editor',
|
|
1042
|
+
];
|
|
1043
|
+
if (valueWidgetTypes.includes(type) && !builtWidget.path) {
|
|
1044
|
+
throw new Error(`Value widget '${type}' requires a 'path' property`);
|
|
1045
|
+
}
|
|
1046
|
+
this.ensureChildren();
|
|
1047
|
+
this.containerState.children.push(builtWidget);
|
|
1048
|
+
return this;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Flex Container Builder - Liskov Substitution Principle
|
|
1053
|
+
* Extends WidgetContainerMixin to inherit all common functionality
|
|
1054
|
+
*/
|
|
1055
|
+
class FlexContainerBuilder extends WidgetContainerMixin {
|
|
1056
|
+
constructor() {
|
|
1057
|
+
super('flex-layout');
|
|
1058
|
+
}
|
|
1059
|
+
setOptions(options) {
|
|
1060
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1061
|
+
return this;
|
|
1062
|
+
}
|
|
1063
|
+
// Individual fluent methods for Flex
|
|
1064
|
+
setDirection(direction) {
|
|
1065
|
+
return this.setOptions({ flexDirection: direction });
|
|
1066
|
+
}
|
|
1067
|
+
setWrap(wrap) {
|
|
1068
|
+
return this.setOptions({ flexWrap: wrap });
|
|
1069
|
+
}
|
|
1070
|
+
setJustifyContent(justify) {
|
|
1071
|
+
return this.setOptions({ justifyContent: justify });
|
|
1072
|
+
}
|
|
1073
|
+
setAlignItems(align) {
|
|
1074
|
+
return this.setOptions({ alignItems: align });
|
|
1075
|
+
}
|
|
1076
|
+
setGap(gap) {
|
|
1077
|
+
return this.setOptions({ gap });
|
|
1078
|
+
}
|
|
1079
|
+
setBackgroundColor(color) {
|
|
1080
|
+
return this.setOptions({ backgroundColor: color });
|
|
1081
|
+
}
|
|
1082
|
+
setPadding(padding) {
|
|
1083
|
+
return this.setOptions({ spacing: { padding } });
|
|
1084
|
+
}
|
|
1085
|
+
setMargin(margin) {
|
|
1086
|
+
return this.setOptions({ spacing: { margin } });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Grid Container Builder - Liskov Substitution Principle
|
|
1091
|
+
* Extends WidgetContainerMixin to inherit all common functionality
|
|
1092
|
+
*/
|
|
1093
|
+
/**
|
|
1094
|
+
* Extracts flat grid-item options from AXPGridLayoutOptions for grid-item-layout widget.
|
|
1095
|
+
* Uses first available breakpoint (lg, xl, md, sm).
|
|
1096
|
+
*/
|
|
1097
|
+
/**
|
|
1098
|
+
* Deep-merges grid breakpoint buckets so sequential fluent calls (e.g. setColumns then setGap)
|
|
1099
|
+
* do not wipe sibling keys under options.grid.default.
|
|
1100
|
+
*/
|
|
1101
|
+
function mergeAXPGridContainerOptions(prev, patch) {
|
|
1102
|
+
const next = { ...(prev ?? {}), ...patch };
|
|
1103
|
+
if (prev?.grid?.default || patch.grid?.default) {
|
|
1104
|
+
next.grid = {
|
|
1105
|
+
...(prev?.grid ?? {}),
|
|
1106
|
+
...(patch.grid ?? {}),
|
|
1107
|
+
default: {
|
|
1108
|
+
...(prev?.grid?.default ?? {}),
|
|
1109
|
+
...(patch.grid?.default ?? {}),
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
return next;
|
|
1114
|
+
}
|
|
1115
|
+
function toGridItemOptions(layoutOptions) {
|
|
1116
|
+
if (!layoutOptions?.positions)
|
|
1117
|
+
return { colSpan: 12 };
|
|
1118
|
+
const positions = layoutOptions.positions;
|
|
1119
|
+
const placement = positions['lg'] ?? positions['xl'] ?? positions['xxl'] ?? positions['md'] ?? positions['sm'];
|
|
1120
|
+
if (!placement)
|
|
1121
|
+
return { colSpan: 12 };
|
|
1122
|
+
const opts = {};
|
|
1123
|
+
if (placement['colSpan'] != null)
|
|
1124
|
+
opts['colSpan'] = placement['colSpan'];
|
|
1125
|
+
if (placement['colStart'] != null)
|
|
1126
|
+
opts['colStart'] = placement['colStart'];
|
|
1127
|
+
if (placement['colEnd'] != null)
|
|
1128
|
+
opts['colEnd'] = placement['colEnd'];
|
|
1129
|
+
if (placement['rowSpan'] != null)
|
|
1130
|
+
opts['rowSpan'] = placement['rowSpan'];
|
|
1131
|
+
if (placement['rowStart'] != null)
|
|
1132
|
+
opts['rowStart'] = placement['rowStart'];
|
|
1133
|
+
if (placement['rowEnd'] != null)
|
|
1134
|
+
opts['rowEnd'] = placement['rowEnd'];
|
|
1135
|
+
if (Object.keys(opts).length === 0)
|
|
1136
|
+
opts['colSpan'] = 12;
|
|
1137
|
+
return opts;
|
|
1138
|
+
}
|
|
1139
|
+
class GridContainerBuilder extends WidgetContainerMixin {
|
|
1140
|
+
constructor() {
|
|
1141
|
+
super('grid-layout');
|
|
1142
|
+
}
|
|
1143
|
+
setOptions(options) {
|
|
1144
|
+
this.containerState.options = mergeAXPGridContainerOptions(this.containerState.options, options);
|
|
1145
|
+
return this;
|
|
1146
|
+
}
|
|
1147
|
+
item(layoutOptions, delegate) {
|
|
1148
|
+
const fieldset = new FieldsetContainerBuilder();
|
|
1149
|
+
fieldset.withInheritanceContext(this.inheritanceContext);
|
|
1150
|
+
delegate(fieldset);
|
|
1151
|
+
const fieldsetNode = fieldset.build();
|
|
1152
|
+
const gridItemOptions = toGridItemOptions(layoutOptions);
|
|
1153
|
+
const gridItemNode = {
|
|
1154
|
+
type: 'grid-item-layout',
|
|
1155
|
+
options: gridItemOptions,
|
|
1156
|
+
children: [fieldsetNode],
|
|
1157
|
+
};
|
|
1158
|
+
this.ensureChildren();
|
|
1159
|
+
this.containerState.children.push(gridItemNode);
|
|
1160
|
+
return this;
|
|
1161
|
+
}
|
|
1162
|
+
// Individual fluent methods for Grid
|
|
1163
|
+
setColumns(columns) {
|
|
1164
|
+
return this.setOptions({ grid: { default: { columns } } });
|
|
1165
|
+
}
|
|
1166
|
+
setRows(rows) {
|
|
1167
|
+
return this.setOptions({ grid: { default: { rows } } });
|
|
1168
|
+
}
|
|
1169
|
+
setGap(gap) {
|
|
1170
|
+
return this.setOptions({ grid: { default: { gap } } });
|
|
1171
|
+
}
|
|
1172
|
+
setJustifyItems(justify) {
|
|
1173
|
+
return this.setOptions({ grid: { default: { justifyItems: justify } } });
|
|
1174
|
+
}
|
|
1175
|
+
setAlignItems(align) {
|
|
1176
|
+
return this.setOptions({ grid: { default: { alignItems: align } } });
|
|
1177
|
+
}
|
|
1178
|
+
setAutoFlow(flow) {
|
|
1179
|
+
return this.setOptions({ grid: { default: { autoFlow: flow } } });
|
|
1180
|
+
}
|
|
1181
|
+
setBackgroundColor(color) {
|
|
1182
|
+
return this.setOptions({ backgroundColor: color });
|
|
1183
|
+
}
|
|
1184
|
+
setPadding(padding) {
|
|
1185
|
+
return this.setOptions({ spacing: { padding } });
|
|
1186
|
+
}
|
|
1187
|
+
setMargin(margin) {
|
|
1188
|
+
return this.setOptions({ spacing: { margin } });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Panel Container Builder - Liskov Substitution Principle
|
|
1193
|
+
* Extends WidgetContainerMixin to inherit all common functionality
|
|
1194
|
+
*/
|
|
1195
|
+
class PanelContainerBuilder extends WidgetContainerMixin {
|
|
1196
|
+
constructor() {
|
|
1197
|
+
super('panel-layout');
|
|
1198
|
+
}
|
|
1199
|
+
setOptions(options) {
|
|
1200
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1201
|
+
return this;
|
|
1202
|
+
}
|
|
1203
|
+
// Individual fluent methods for Panel
|
|
1204
|
+
setCaption(caption) {
|
|
1205
|
+
return this.setOptions({ caption });
|
|
1206
|
+
}
|
|
1207
|
+
setIcon(icon) {
|
|
1208
|
+
return this.setOptions({ icon });
|
|
1209
|
+
}
|
|
1210
|
+
setLook(look) {
|
|
1211
|
+
return this.setOptions({ look });
|
|
1212
|
+
}
|
|
1213
|
+
setShowHeader(show) {
|
|
1214
|
+
return this.setOptions({ showHeader: show });
|
|
1215
|
+
}
|
|
1216
|
+
setCollapsed(collapsed) {
|
|
1217
|
+
return this.setOptions({ collapsed });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Page Container Builder - Liskov Substitution Principle
|
|
1222
|
+
* Extends WidgetContainerMixin to inherit all common functionality
|
|
1223
|
+
*/
|
|
1224
|
+
class PageContainerBuilder extends WidgetContainerMixin {
|
|
1225
|
+
constructor() {
|
|
1226
|
+
super('page-layout');
|
|
1227
|
+
}
|
|
1228
|
+
setOptions(options) {
|
|
1229
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1230
|
+
return this;
|
|
1231
|
+
}
|
|
1232
|
+
// Individual fluent methods for Page
|
|
1233
|
+
setBackgroundColor(color) {
|
|
1234
|
+
return this.setOptions({ backgroundColor: color });
|
|
1235
|
+
}
|
|
1236
|
+
setTheme(theme) {
|
|
1237
|
+
return this.setOptions({ theme });
|
|
1238
|
+
}
|
|
1239
|
+
setHasHeader(hasHeader) {
|
|
1240
|
+
return this.setOptions({ hasHeader });
|
|
1241
|
+
}
|
|
1242
|
+
setHasFooter(hasFooter) {
|
|
1243
|
+
return this.setOptions({ hasFooter });
|
|
1244
|
+
}
|
|
1245
|
+
setDirection(direction) {
|
|
1246
|
+
return this.setOptions({ direction });
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Tabset Container Builder - Liskov Substitution Principle
|
|
1251
|
+
* Extends WidgetContainerMixin to inherit all common functionality
|
|
1252
|
+
*/
|
|
1253
|
+
class TabsetContainerBuilder extends WidgetContainerMixin {
|
|
1254
|
+
constructor() {
|
|
1255
|
+
super('tabset-layout');
|
|
1256
|
+
}
|
|
1257
|
+
setOptions(options) {
|
|
1258
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1259
|
+
return this;
|
|
1260
|
+
}
|
|
1261
|
+
// Individual fluent methods for Tabset
|
|
1262
|
+
setLook(look) {
|
|
1263
|
+
return this.setOptions({ look });
|
|
1264
|
+
}
|
|
1265
|
+
setOrientation(orientation) {
|
|
1266
|
+
return this.setOptions({ orientation });
|
|
1267
|
+
}
|
|
1268
|
+
setActiveIndex(index) {
|
|
1269
|
+
return this.setOptions({ activeIndex: index });
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Form Field Builder - Liskov Substitution Principle
|
|
1274
|
+
* Can only contain ONE widget with automatic path generation
|
|
1275
|
+
*/
|
|
1276
|
+
class FormFieldBuilder extends LayoutContainerMixin {
|
|
1277
|
+
constructor(label) {
|
|
1278
|
+
super('form-field');
|
|
1279
|
+
this.hasWidget = false;
|
|
1280
|
+
this.containerState.options = { label, showLabel: true };
|
|
1281
|
+
}
|
|
1282
|
+
setOptions(options) {
|
|
1283
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1284
|
+
return this;
|
|
1285
|
+
}
|
|
1286
|
+
setLabel(label) {
|
|
1287
|
+
return this.setOptions({ label });
|
|
1288
|
+
}
|
|
1289
|
+
setShowLabel(showLabel) {
|
|
1290
|
+
return this.setOptions({ showLabel });
|
|
1291
|
+
}
|
|
1292
|
+
defaultValue(value) {
|
|
1293
|
+
this.containerState.defaultValue = value;
|
|
1294
|
+
this.inheritanceContext.defaultValue = value;
|
|
1295
|
+
if (this.childWidget) {
|
|
1296
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1297
|
+
}
|
|
1298
|
+
return this;
|
|
1299
|
+
}
|
|
1300
|
+
// Single widget methods with automatic path generation
|
|
1301
|
+
addSingleWidget(type, options) {
|
|
1302
|
+
if (this.hasWidget) {
|
|
1303
|
+
throw new Error('Form field can only contain one widget');
|
|
1304
|
+
}
|
|
1305
|
+
const formFieldName = this.containerState.name;
|
|
1306
|
+
const formFieldPath = this.containerState.path; // Get explicit path from form field
|
|
1307
|
+
const formFieldLabel = this.containerState.options?.['label'];
|
|
1308
|
+
const widgetName = options?.name;
|
|
1309
|
+
// Generate widget path: explicit path -> widget name -> form field name -> label -> random
|
|
1310
|
+
let widgetPath;
|
|
1311
|
+
if (formFieldPath) {
|
|
1312
|
+
widgetPath = formFieldPath; // Use explicit form field path first
|
|
1313
|
+
}
|
|
1314
|
+
else if (widgetName) {
|
|
1315
|
+
widgetPath = widgetName;
|
|
1316
|
+
}
|
|
1317
|
+
else if (formFieldName) {
|
|
1318
|
+
widgetPath = formFieldName; // Use form field name as default path
|
|
1319
|
+
}
|
|
1320
|
+
else if (formFieldLabel) {
|
|
1321
|
+
widgetPath = labelToPath(formFieldLabel);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
widgetPath = generateRandomId();
|
|
1325
|
+
}
|
|
1326
|
+
const finalName = widgetName || formFieldName || widgetPath;
|
|
1327
|
+
const child = new WidgetBuilder();
|
|
1328
|
+
child.type(type);
|
|
1329
|
+
child.name(finalName);
|
|
1330
|
+
child.path(widgetPath);
|
|
1331
|
+
// Extract extended properties from options (triggers, meta, valueTransforms, mode, visible, defaultValue)
|
|
1332
|
+
const { name: _, triggers, meta, valueTransforms, mode: extendedMode, visible: extendedVisible, defaultValue: extendedDefaultValue, children: extendedChildren, ...cleanOptions } = (options || {});
|
|
1333
|
+
child.withInheritanceContext(this.inheritanceContext);
|
|
1334
|
+
child.options(cleanOptions);
|
|
1335
|
+
// Apply extended properties if provided
|
|
1336
|
+
if (extendedMode !== undefined) {
|
|
1337
|
+
child.mode(extendedMode);
|
|
1338
|
+
}
|
|
1339
|
+
if (extendedVisible !== undefined) {
|
|
1340
|
+
child.visible(extendedVisible);
|
|
1341
|
+
}
|
|
1342
|
+
if (extendedDefaultValue !== undefined) {
|
|
1343
|
+
child.defaultValue(extendedDefaultValue);
|
|
1344
|
+
}
|
|
1345
|
+
// Set triggers, meta, and valueTransforms directly on widgetState
|
|
1346
|
+
// These are part of AXPWidgetNode but not handled by WidgetBuilder methods
|
|
1347
|
+
if (triggers !== undefined) {
|
|
1348
|
+
child.widgetState.triggers = triggers;
|
|
1349
|
+
}
|
|
1350
|
+
if (meta !== undefined) {
|
|
1351
|
+
child.widgetState.meta = meta;
|
|
1352
|
+
}
|
|
1353
|
+
if (valueTransforms !== undefined) {
|
|
1354
|
+
child.widgetState.valueTransforms = valueTransforms;
|
|
1355
|
+
}
|
|
1356
|
+
if (extendedChildren !== undefined) {
|
|
1357
|
+
child.widgetState.children = extendedChildren;
|
|
1358
|
+
}
|
|
1359
|
+
// IMPORTANT: Store the widget builder, don't build it yet!
|
|
1360
|
+
// This allows properties set after this method (like disabled, readonly) to be applied
|
|
1361
|
+
this.childWidget = child;
|
|
1362
|
+
this.hasWidget = true;
|
|
1363
|
+
return this;
|
|
1364
|
+
}
|
|
1365
|
+
textBox(options) {
|
|
1366
|
+
return this.addSingleWidget('text-editor', options);
|
|
1367
|
+
}
|
|
1368
|
+
largeTextBox(options) {
|
|
1369
|
+
return this.addSingleWidget('large-text-editor', options);
|
|
1370
|
+
}
|
|
1371
|
+
richText(options) {
|
|
1372
|
+
return this.addSingleWidget('rich-text-editor', options);
|
|
1373
|
+
}
|
|
1374
|
+
passwordBox(options) {
|
|
1375
|
+
return this.addSingleWidget('password-editor', options);
|
|
1376
|
+
}
|
|
1377
|
+
numberBox(options) {
|
|
1378
|
+
return this.addSingleWidget('number-editor', options);
|
|
1379
|
+
}
|
|
1380
|
+
selectBox(options) {
|
|
1381
|
+
return this.addSingleWidget('select-editor', options);
|
|
1382
|
+
}
|
|
1383
|
+
lookupBox(options) {
|
|
1384
|
+
return this.addSingleWidget('lookup-editor', options);
|
|
1385
|
+
}
|
|
1386
|
+
selectionList(options) {
|
|
1387
|
+
return this.addSingleWidget('selection-list-editor', options);
|
|
1388
|
+
}
|
|
1389
|
+
dateTimeBox(options) {
|
|
1390
|
+
return this.addSingleWidget('date-time-editor', options);
|
|
1391
|
+
}
|
|
1392
|
+
toggleSwitch(options) {
|
|
1393
|
+
return this.addSingleWidget('toggle-editor', options);
|
|
1394
|
+
}
|
|
1395
|
+
colorBox(options) {
|
|
1396
|
+
return this.addSingleWidget('color-editor', options);
|
|
1397
|
+
}
|
|
1398
|
+
customWidget(type, options) {
|
|
1399
|
+
return this.addSingleWidget(type, options);
|
|
1400
|
+
}
|
|
1401
|
+
// Override property setters to propagate changes to child widget
|
|
1402
|
+
disabled(condition) {
|
|
1403
|
+
super.disabled(condition);
|
|
1404
|
+
if (this.childWidget) {
|
|
1405
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1406
|
+
}
|
|
1407
|
+
return this;
|
|
1408
|
+
}
|
|
1409
|
+
readonly(condition) {
|
|
1410
|
+
super.readonly(condition);
|
|
1411
|
+
if (this.childWidget) {
|
|
1412
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1413
|
+
}
|
|
1414
|
+
return this;
|
|
1415
|
+
}
|
|
1416
|
+
visible(condition) {
|
|
1417
|
+
super.visible(condition);
|
|
1418
|
+
if (this.childWidget) {
|
|
1419
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1420
|
+
}
|
|
1421
|
+
return this;
|
|
1422
|
+
}
|
|
1423
|
+
direction(direction) {
|
|
1424
|
+
super.direction(direction);
|
|
1425
|
+
if (this.childWidget) {
|
|
1426
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1427
|
+
}
|
|
1428
|
+
return this;
|
|
1429
|
+
}
|
|
1430
|
+
mode(mode) {
|
|
1431
|
+
super.mode(mode);
|
|
1432
|
+
if (this.childWidget) {
|
|
1433
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1434
|
+
}
|
|
1435
|
+
return this;
|
|
1436
|
+
}
|
|
1437
|
+
// Override withInheritanceContext to pass it to the child widget if it exists
|
|
1438
|
+
withInheritanceContext(context) {
|
|
1439
|
+
// Call parent implementation first
|
|
1440
|
+
super.withInheritanceContext(context);
|
|
1441
|
+
// If we have a child widget, update its inheritance context too
|
|
1442
|
+
if (this.childWidget) {
|
|
1443
|
+
this.childWidget.withInheritanceContext(this.inheritanceContext);
|
|
1444
|
+
}
|
|
1445
|
+
return this;
|
|
1446
|
+
}
|
|
1447
|
+
// Override build() to build the child widget at the last moment
|
|
1448
|
+
build() {
|
|
1449
|
+
// Build the child widget and add it to children before building the form field
|
|
1450
|
+
if (this.childWidget) {
|
|
1451
|
+
this.ensureChildren();
|
|
1452
|
+
this.containerState.children.push(this.childWidget.build());
|
|
1453
|
+
}
|
|
1454
|
+
// Call parent build
|
|
1455
|
+
return super.build();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Fieldset Container Builder - Liskov Substitution Principle
|
|
1460
|
+
* Extends LayoutContainerMixin to inherit layout functionality
|
|
1461
|
+
* Specialized for form fields only
|
|
1462
|
+
*/
|
|
1463
|
+
class FieldsetContainerBuilder extends LayoutContainerMixin {
|
|
1464
|
+
constructor() {
|
|
1465
|
+
super('fieldset-layout');
|
|
1466
|
+
this.containerState.options = {};
|
|
1467
|
+
}
|
|
1468
|
+
setOptions(options) {
|
|
1469
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1470
|
+
return this;
|
|
1471
|
+
}
|
|
1472
|
+
// Individual fluent methods for Fieldset
|
|
1473
|
+
setTitle(title) {
|
|
1474
|
+
return this.setOptions({ title });
|
|
1475
|
+
}
|
|
1476
|
+
setDescription(description) {
|
|
1477
|
+
return this.setOptions({ description });
|
|
1478
|
+
}
|
|
1479
|
+
setIcon(icon) {
|
|
1480
|
+
return this.setOptions({ icon });
|
|
1481
|
+
}
|
|
1482
|
+
setCollapsible(collapsible) {
|
|
1483
|
+
return this.setOptions({ collapsible });
|
|
1484
|
+
}
|
|
1485
|
+
setIsOpen(isOpen) {
|
|
1486
|
+
return this.setOptions({ isOpen });
|
|
1487
|
+
}
|
|
1488
|
+
setLook(look) {
|
|
1489
|
+
return this.setOptions({ look });
|
|
1490
|
+
}
|
|
1491
|
+
setShowHeader(showHeader) {
|
|
1492
|
+
return this.setOptions({ showHeader });
|
|
1493
|
+
}
|
|
1494
|
+
setCols(cols) {
|
|
1495
|
+
return this.setOptions({ cols });
|
|
1496
|
+
}
|
|
1497
|
+
// Only form fields are allowed in fieldset
|
|
1498
|
+
formField(label, delegate) {
|
|
1499
|
+
const field = new FormFieldBuilder(label);
|
|
1500
|
+
field.withInheritanceContext(this.inheritanceContext);
|
|
1501
|
+
if (delegate) {
|
|
1502
|
+
delegate(field);
|
|
1503
|
+
}
|
|
1504
|
+
this.ensureChildren();
|
|
1505
|
+
this.containerState.children.push(field.build());
|
|
1506
|
+
return this;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* List Widget Builder - Liskov Substitution Principle
|
|
1511
|
+
* Extends WidgetContainerMixin to inherit all common functionality
|
|
1512
|
+
*/
|
|
1513
|
+
class ListWidgetBuilder extends WidgetContainerMixin {
|
|
1514
|
+
constructor() {
|
|
1515
|
+
super('data-list');
|
|
1516
|
+
}
|
|
1517
|
+
setOptions(options) {
|
|
1518
|
+
this.containerState.options = { ...this.containerState.options, ...options };
|
|
1519
|
+
return this;
|
|
1520
|
+
}
|
|
1521
|
+
// Individual fluent methods for List Widget
|
|
1522
|
+
setDataSource(dataSource) {
|
|
1523
|
+
return this.setOptions({ dataSource });
|
|
1524
|
+
}
|
|
1525
|
+
setColumns(columns) {
|
|
1526
|
+
return this.setOptions({ columns });
|
|
1527
|
+
}
|
|
1528
|
+
// Event handlers
|
|
1529
|
+
setOnRowClick(handler) {
|
|
1530
|
+
return this.setOptions({ onRowClick: handler });
|
|
1531
|
+
}
|
|
1532
|
+
setOnRowDoubleClick(handler) {
|
|
1533
|
+
return this.setOptions({ onRowDoubleClick: handler });
|
|
1534
|
+
}
|
|
1535
|
+
setOnSelectionChange(handler) {
|
|
1536
|
+
return this.setOptions({ onSelectionChange: handler });
|
|
1537
|
+
}
|
|
1538
|
+
setOnRowCommand(handler) {
|
|
1539
|
+
return this.setOptions({ onRowCommand: handler });
|
|
1540
|
+
}
|
|
1541
|
+
// Table features
|
|
1542
|
+
setPaging(paging) {
|
|
1543
|
+
return this.setOptions({ paging });
|
|
1544
|
+
}
|
|
1545
|
+
setShowHeader(show) {
|
|
1546
|
+
return this.setOptions({ showHeader: show });
|
|
1547
|
+
}
|
|
1548
|
+
setShowFooter(show) {
|
|
1549
|
+
return this.setOptions({ showFooter: show });
|
|
1550
|
+
}
|
|
1551
|
+
setFixHeader(fix) {
|
|
1552
|
+
return this.setOptions({ fixHeader: fix });
|
|
1553
|
+
}
|
|
1554
|
+
setFixFooter(fix) {
|
|
1555
|
+
return this.setOptions({ fixFooter: fix });
|
|
1556
|
+
}
|
|
1557
|
+
setFetchDataMode(mode) {
|
|
1558
|
+
return this.setOptions({ fetchDataMode: mode });
|
|
1559
|
+
}
|
|
1560
|
+
setParentField(field) {
|
|
1561
|
+
return this.setOptions({ parentField: field });
|
|
1562
|
+
}
|
|
1563
|
+
setMinHeight(height) {
|
|
1564
|
+
return this.setOptions({ minHeight: height });
|
|
1565
|
+
}
|
|
1566
|
+
// Selection & Index
|
|
1567
|
+
setShowIndex(show) {
|
|
1568
|
+
return this.setOptions({ showIndex: show });
|
|
1569
|
+
}
|
|
1570
|
+
setAllowSelection(allow) {
|
|
1571
|
+
return this.setOptions({ allowSelection: allow });
|
|
1572
|
+
}
|
|
1573
|
+
// Commands
|
|
1574
|
+
setPrimaryCommands(commands) {
|
|
1575
|
+
return this.setOptions({ primaryCommands: commands });
|
|
1576
|
+
}
|
|
1577
|
+
setSecondaryCommands(commands) {
|
|
1578
|
+
return this.setOptions({ secondaryCommands: commands });
|
|
1579
|
+
}
|
|
1580
|
+
// Loading
|
|
1581
|
+
setLoading(loading) {
|
|
1582
|
+
return this.setOptions({ loading });
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Dialog Container Builder - Specialized for dialog functionality
|
|
1587
|
+
* Uses composition instead of inheritance for cleaner separation
|
|
1588
|
+
*/
|
|
1589
|
+
class DialogContainerBuilder {
|
|
1590
|
+
constructor(popupService, hookService) {
|
|
1591
|
+
this.dialogState = {
|
|
1592
|
+
type: 'flex-layout', // This will be overridden when content layout exists
|
|
1593
|
+
children: [],
|
|
1594
|
+
mode: 'edit',
|
|
1595
|
+
dialogOptions: {
|
|
1596
|
+
title: '',
|
|
1597
|
+
size: 'md',
|
|
1598
|
+
closeButton: false,
|
|
1599
|
+
},
|
|
1600
|
+
actions: {
|
|
1601
|
+
footer: {
|
|
1602
|
+
prefix: [],
|
|
1603
|
+
suffix: [],
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1606
|
+
};
|
|
1607
|
+
if (popupService) {
|
|
1608
|
+
this.popupService = popupService;
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
this.popupService = inject(AXPopupService);
|
|
1612
|
+
}
|
|
1613
|
+
this.hookService = hookService ?? inject(AXPHookService, { optional: true }) ?? undefined;
|
|
1614
|
+
}
|
|
1615
|
+
setOptions(options) {
|
|
1616
|
+
this.dialogState.dialogOptions = { ...this.dialogState.dialogOptions, ...options };
|
|
1617
|
+
return this;
|
|
1618
|
+
}
|
|
1619
|
+
// Individual fluent methods for Dialog
|
|
1620
|
+
setTitle(title) {
|
|
1621
|
+
return this.setOptions({ title });
|
|
1622
|
+
}
|
|
1623
|
+
setMessage(message) {
|
|
1624
|
+
return this.setOptions({ message });
|
|
1625
|
+
}
|
|
1626
|
+
setSize(size) {
|
|
1627
|
+
return this.setOptions({ size });
|
|
1628
|
+
}
|
|
1629
|
+
setCloseButton(closeButton) {
|
|
1630
|
+
return this.setOptions({ closeButton });
|
|
1631
|
+
}
|
|
1632
|
+
setContext(context) {
|
|
1633
|
+
return this.setOptions({ context });
|
|
1634
|
+
}
|
|
1635
|
+
content(delegate) {
|
|
1636
|
+
if (delegate) {
|
|
1637
|
+
// Create a flex container directly instead of through LayoutBuilder
|
|
1638
|
+
const flexContainer = new FlexContainerBuilder();
|
|
1639
|
+
flexContainer.setDirection('column');
|
|
1640
|
+
flexContainer.setGap('10px');
|
|
1641
|
+
delegate(flexContainer);
|
|
1642
|
+
this.contentLayout = flexContainer.build();
|
|
1643
|
+
}
|
|
1644
|
+
return this;
|
|
1645
|
+
}
|
|
1646
|
+
setActions(delegate) {
|
|
1647
|
+
if (delegate) {
|
|
1648
|
+
const actionBuilder = new ActionBuilder(this);
|
|
1649
|
+
delegate(actionBuilder);
|
|
1650
|
+
}
|
|
1651
|
+
return this;
|
|
1652
|
+
}
|
|
1653
|
+
onAction(handler) {
|
|
1654
|
+
this.dialogState.dialogOptions ??= {
|
|
1655
|
+
title: '',
|
|
1656
|
+
size: 'md',
|
|
1657
|
+
closeButton: false,
|
|
1658
|
+
};
|
|
1659
|
+
this.dialogState.dialogOptions.onAction = handler;
|
|
1660
|
+
return this;
|
|
1661
|
+
}
|
|
1662
|
+
confirmCloseWhenDirty(options) {
|
|
1663
|
+
return this.setOptions({ confirmCloseWhenDirty: options ?? true });
|
|
1664
|
+
}
|
|
1665
|
+
addCustomAction(action) {
|
|
1666
|
+
// Add to actions based on position
|
|
1667
|
+
const position = action.position || 'suffix';
|
|
1668
|
+
if (position === 'prefix') {
|
|
1669
|
+
if (!this.dialogState.actions.footer.prefix) {
|
|
1670
|
+
this.dialogState.actions.footer.prefix = [];
|
|
1671
|
+
}
|
|
1672
|
+
this.dialogState.actions.footer.prefix.push(action);
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
if (!this.dialogState.actions.footer.suffix) {
|
|
1676
|
+
this.dialogState.actions.footer.suffix = [];
|
|
1677
|
+
}
|
|
1678
|
+
this.dialogState.actions.footer.suffix.push(action);
|
|
1679
|
+
}
|
|
1680
|
+
return this;
|
|
1681
|
+
}
|
|
1682
|
+
// Build method to create dialog node
|
|
1683
|
+
build() {
|
|
1684
|
+
// If we have content layout, use it directly to avoid extra wrapper
|
|
1685
|
+
if (this.contentLayout) {
|
|
1686
|
+
return {
|
|
1687
|
+
...this.contentLayout,
|
|
1688
|
+
// Add dialog-specific properties
|
|
1689
|
+
options: {
|
|
1690
|
+
...this.contentLayout.options,
|
|
1691
|
+
...this.dialogState.dialogOptions,
|
|
1692
|
+
},
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
// Fallback to dialog state structure if no content
|
|
1696
|
+
const result = {
|
|
1697
|
+
...this.dialogState,
|
|
1698
|
+
children: [],
|
|
1699
|
+
};
|
|
1700
|
+
// Add dialog-specific properties
|
|
1701
|
+
if (this.dialogState.dialogOptions) {
|
|
1702
|
+
result.options = { ...result.options, ...this.dialogState.dialogOptions };
|
|
1703
|
+
}
|
|
1704
|
+
return result;
|
|
1705
|
+
}
|
|
1706
|
+
// Dialog-specific methods
|
|
1707
|
+
async show() {
|
|
1708
|
+
const dialogNode = this.build();
|
|
1709
|
+
// Import the dialog renderer component dynamically
|
|
1710
|
+
const { AXPDialogRendererComponent } = await Promise.resolve().then(function () { return dialogRenderer_component; });
|
|
1711
|
+
this.dialogState.dialogOptions ??= {};
|
|
1712
|
+
if (this.dialogState.dialogOptions.context == null || typeof this.dialogState.dialogOptions.context !== 'object') {
|
|
1713
|
+
this.dialogState.dialogOptions.context = {};
|
|
1714
|
+
}
|
|
1715
|
+
const initialContext = this.dialogState.dialogOptions.context;
|
|
1716
|
+
this.dialogState.actions ??= { footer: { prefix: [], suffix: [] } };
|
|
1717
|
+
this.dialogState.actions.footer ??= { prefix: [], suffix: [] };
|
|
1718
|
+
this.dialogState.actions.footer.prefix ??= [];
|
|
1719
|
+
this.dialogState.actions.footer.suffix ??= [];
|
|
1720
|
+
const hookService = this.hookService;
|
|
1721
|
+
if (hookService) {
|
|
1722
|
+
const beforePayload = {
|
|
1723
|
+
context: initialContext,
|
|
1724
|
+
dialogOptions: this.dialogState.dialogOptions,
|
|
1725
|
+
};
|
|
1726
|
+
await hookService.runAsync(AXP_LAYOUT_BUILDER_DIALOG_BEFORE_OPEN_HOOK_KEY, beforePayload);
|
|
1727
|
+
}
|
|
1728
|
+
const confirmCloseWhenDirty = normalizeDialogCloseConfirmation(this.dialogState.dialogOptions?.confirmCloseWhenDirty);
|
|
1729
|
+
// Create dialog configuration
|
|
1730
|
+
const dialogConfig = {
|
|
1731
|
+
title: this.dialogState.dialogOptions?.title || '',
|
|
1732
|
+
//TODO: why we need message?
|
|
1733
|
+
//message: this.dialogState.dialogOptions?.message,
|
|
1734
|
+
context: initialContext,
|
|
1735
|
+
definition: dialogNode,
|
|
1736
|
+
metadata: this.dialogState.dialogOptions.metadata,
|
|
1737
|
+
actions: this.dialogState.actions,
|
|
1738
|
+
onAction: this.dialogState.dialogOptions?.onAction,
|
|
1739
|
+
confirmCloseWhenDirty,
|
|
1740
|
+
};
|
|
1741
|
+
//
|
|
1742
|
+
if (hookService) {
|
|
1743
|
+
await hookService.runAsync(AXP_LAYOUT_BUILDER_DIALOG_CONFIG_HOOK_KEY, dialogConfig);
|
|
1744
|
+
}
|
|
1745
|
+
// The Promise resolves when user clicks an action button
|
|
1746
|
+
return new Promise(async (resolve) => {
|
|
1747
|
+
let flag = false;
|
|
1748
|
+
await this.popupService.open(AXPDialogRendererComponent, {
|
|
1749
|
+
title: dialogConfig.title,
|
|
1750
|
+
size: this.dialogState.dialogOptions?.size || 'md',
|
|
1751
|
+
closeButton: this.dialogState.dialogOptions?.closeButton || false,
|
|
1752
|
+
closeOnBackdropClick: false,
|
|
1753
|
+
draggable: false,
|
|
1754
|
+
data: {
|
|
1755
|
+
config: dialogConfig,
|
|
1756
|
+
callBack: (result) => {
|
|
1757
|
+
flag = true;
|
|
1758
|
+
resolve(result);
|
|
1759
|
+
},
|
|
1760
|
+
},
|
|
1761
|
+
});
|
|
1762
|
+
if (!flag) {
|
|
1763
|
+
resolve(createDismissedDialogRef());
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
//#endregion
|
|
1769
|
+
//#region ---- Widget Builder Implementation ----
|
|
1770
|
+
/**
|
|
1771
|
+
* Widget Builder - Single Responsibility Principle
|
|
1772
|
+
* Handles individual widget configuration and building
|
|
1773
|
+
*/
|
|
1774
|
+
class WidgetBuilder {
|
|
1775
|
+
constructor(name) {
|
|
1776
|
+
this.widgetState = {
|
|
1777
|
+
type: 'widget',
|
|
1778
|
+
options: {},
|
|
1779
|
+
children: [],
|
|
1780
|
+
};
|
|
1781
|
+
this.inheritanceContext = {};
|
|
1782
|
+
if (name) {
|
|
1783
|
+
this.widgetState.name = name;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
type(type) {
|
|
1787
|
+
this.widgetState.type = type;
|
|
1788
|
+
return this;
|
|
1789
|
+
}
|
|
1790
|
+
name(name) {
|
|
1791
|
+
this.widgetState.name = name;
|
|
1792
|
+
if (!this.widgetState.path) {
|
|
1793
|
+
this.widgetState.path = name;
|
|
1794
|
+
}
|
|
1795
|
+
return this;
|
|
1796
|
+
}
|
|
1797
|
+
path(path) {
|
|
1798
|
+
this.widgetState.path = path;
|
|
1799
|
+
return this;
|
|
1800
|
+
}
|
|
1801
|
+
options(options) {
|
|
1802
|
+
// Extract defaultValue from options - it's an exceptional parameter that updates context, not a widget option
|
|
1803
|
+
if (options && options.defaultValue !== undefined) {
|
|
1804
|
+
this.widgetState.defaultValue = options.defaultValue;
|
|
1805
|
+
// Remove defaultValue from options since it's stored on the node, not in options
|
|
1806
|
+
const { defaultValue: _, ...cleanOptions } = options;
|
|
1807
|
+
// Merge clean options instead of replacing to preserve inherited properties
|
|
1808
|
+
this.widgetState.options = { ...this.widgetState.options, ...cleanOptions };
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
// Merge options instead of replacing to preserve inherited properties
|
|
1812
|
+
this.widgetState.options = { ...this.widgetState.options, ...options };
|
|
1813
|
+
}
|
|
1814
|
+
return this;
|
|
1815
|
+
}
|
|
1816
|
+
layout(value) {
|
|
1817
|
+
if (typeof value === 'number') {
|
|
1818
|
+
this.widgetState.layout = {
|
|
1819
|
+
positions: {
|
|
1820
|
+
sm: { colSpan: 12 },
|
|
1821
|
+
md: { colSpan: 12 },
|
|
1822
|
+
lg: { colSpan: value },
|
|
1823
|
+
xl: { colSpan: value },
|
|
1824
|
+
xxl: { colSpan: value },
|
|
1825
|
+
},
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
this.widgetState.layout = value;
|
|
1830
|
+
}
|
|
1831
|
+
return this;
|
|
1832
|
+
}
|
|
1833
|
+
mode(mode) {
|
|
1834
|
+
this.widgetState.mode = mode;
|
|
1835
|
+
this.inheritanceContext.mode = mode;
|
|
1836
|
+
return this;
|
|
1837
|
+
}
|
|
1838
|
+
visible(condition) {
|
|
1839
|
+
if (!this.widgetState.options) {
|
|
1840
|
+
this.widgetState.options = {};
|
|
1841
|
+
}
|
|
1842
|
+
this.widgetState.options['visible'] = condition;
|
|
1843
|
+
this.widgetState.visible = condition;
|
|
1844
|
+
this.inheritanceContext.visible = condition;
|
|
1845
|
+
return this;
|
|
1846
|
+
}
|
|
1847
|
+
disabled(condition) {
|
|
1848
|
+
if (!this.widgetState.options) {
|
|
1849
|
+
this.widgetState.options = {};
|
|
1850
|
+
}
|
|
1851
|
+
this.widgetState.options['disabled'] = condition;
|
|
1852
|
+
this.inheritanceContext.disabled = condition;
|
|
1853
|
+
return this;
|
|
1854
|
+
}
|
|
1855
|
+
defaultValue(defaultValue) {
|
|
1856
|
+
this.widgetState.defaultValue = defaultValue;
|
|
1857
|
+
return this;
|
|
1858
|
+
}
|
|
1859
|
+
readonly(condition) {
|
|
1860
|
+
if (!this.widgetState.options) {
|
|
1861
|
+
this.widgetState.options = {};
|
|
1862
|
+
}
|
|
1863
|
+
this.widgetState.options['readonly'] = condition;
|
|
1864
|
+
this.inheritanceContext.readonly = condition;
|
|
1865
|
+
return this;
|
|
1866
|
+
}
|
|
1867
|
+
direction(direction) {
|
|
1868
|
+
if (!this.widgetState.options) {
|
|
1869
|
+
this.widgetState.options = {};
|
|
1870
|
+
}
|
|
1871
|
+
this.widgetState.options['direction'] = direction;
|
|
1872
|
+
this.inheritanceContext.direction = direction;
|
|
1873
|
+
return this;
|
|
1874
|
+
}
|
|
1875
|
+
children(children) {
|
|
1876
|
+
this.widgetState.children = children;
|
|
1877
|
+
return this;
|
|
1878
|
+
}
|
|
1879
|
+
// Inheritance context methods
|
|
1880
|
+
withInheritanceContext(context) {
|
|
1881
|
+
this.inheritanceContext = mergeInheritanceContext(context);
|
|
1882
|
+
// Apply inherited properties to widget state
|
|
1883
|
+
const resolved = resolveInheritedProperties(context, this.inheritanceContext);
|
|
1884
|
+
// Always apply inherited properties (remove the conditions that check if already set)
|
|
1885
|
+
// This allows properties to be updated when inheritance context changes
|
|
1886
|
+
if (resolved.mode) {
|
|
1887
|
+
this.widgetState.mode = resolved.mode;
|
|
1888
|
+
}
|
|
1889
|
+
if (!this.widgetState.options)
|
|
1890
|
+
this.widgetState.options = {};
|
|
1891
|
+
if (resolved.disabled !== undefined) {
|
|
1892
|
+
this.widgetState.options['disabled'] = resolved.disabled;
|
|
1893
|
+
}
|
|
1894
|
+
if (resolved.readonly !== undefined) {
|
|
1895
|
+
this.widgetState.options['readonly'] = resolved.readonly;
|
|
1896
|
+
}
|
|
1897
|
+
if (resolved.direction !== undefined) {
|
|
1898
|
+
this.widgetState.options['direction'] = resolved.direction;
|
|
1899
|
+
}
|
|
1900
|
+
if (resolved.visible !== undefined) {
|
|
1901
|
+
this.widgetState.options['visible'] = resolved.visible;
|
|
1902
|
+
this.widgetState.visible = resolved.visible;
|
|
1903
|
+
}
|
|
1904
|
+
if (context.defaultValue !== undefined) {
|
|
1905
|
+
this.widgetState.defaultValue = context.defaultValue;
|
|
1906
|
+
}
|
|
1907
|
+
return this;
|
|
1908
|
+
}
|
|
1909
|
+
getInheritanceContext() {
|
|
1910
|
+
return { ...this.inheritanceContext };
|
|
1911
|
+
}
|
|
1912
|
+
build() {
|
|
1913
|
+
const node = {
|
|
1914
|
+
name: this.widgetState.name,
|
|
1915
|
+
type: this.widgetState.type,
|
|
1916
|
+
options: this.widgetState.options,
|
|
1917
|
+
mode: this.widgetState.mode,
|
|
1918
|
+
path: this.widgetState.path,
|
|
1919
|
+
defaultValue: this.widgetState.defaultValue,
|
|
1920
|
+
children: this.widgetState.children,
|
|
1921
|
+
};
|
|
1922
|
+
// Add extended properties if they exist
|
|
1923
|
+
if (this.widgetState.triggers !== undefined) {
|
|
1924
|
+
node.triggers = this.widgetState.triggers;
|
|
1925
|
+
}
|
|
1926
|
+
if (this.widgetState.meta !== undefined) {
|
|
1927
|
+
node.meta = this.widgetState.meta;
|
|
1928
|
+
}
|
|
1929
|
+
if (this.widgetState.valueTransforms !== undefined) {
|
|
1930
|
+
node.valueTransforms = this.widgetState.valueTransforms;
|
|
1931
|
+
}
|
|
1932
|
+
if (this.widgetState.visible !== undefined) {
|
|
1933
|
+
node.visible = this.widgetState.visible;
|
|
1934
|
+
}
|
|
1935
|
+
return node;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
//#region ---- Action Builder Implementation ----
|
|
1939
|
+
class ActionBuilder {
|
|
1940
|
+
constructor(dialogBuilder) {
|
|
1941
|
+
this.dialogBuilder = dialogBuilder;
|
|
1942
|
+
}
|
|
1943
|
+
cancel(text, options) {
|
|
1944
|
+
if (!this.dialogBuilder['dialogState'].actions.footer.suffix) {
|
|
1945
|
+
this.dialogBuilder['dialogState'].actions.footer.suffix = [];
|
|
1946
|
+
}
|
|
1947
|
+
this.dialogBuilder['dialogState'].actions.footer.suffix.push({
|
|
1948
|
+
title: text || '@general:actions.cancel.title',
|
|
1949
|
+
color: 'default',
|
|
1950
|
+
command: { name: 'cancel' },
|
|
1951
|
+
shortcuts: resolveDialogActionShortcuts(DEFAULT_CANCEL_DIALOG_ACTION_SHORTCUTS, options?.shortcuts),
|
|
1952
|
+
});
|
|
1953
|
+
return this;
|
|
1954
|
+
}
|
|
1955
|
+
submit(text, options) {
|
|
1956
|
+
if (!this.dialogBuilder['dialogState'].actions.footer.suffix) {
|
|
1957
|
+
this.dialogBuilder['dialogState'].actions.footer.suffix = [];
|
|
1958
|
+
}
|
|
1959
|
+
this.dialogBuilder['dialogState'].actions.footer.suffix.push({
|
|
1960
|
+
title: text || '@general:actions.submit.title',
|
|
1961
|
+
color: 'primary',
|
|
1962
|
+
command: { name: 'submit', options: { validate: true } },
|
|
1963
|
+
shortcuts: resolveDialogActionShortcuts(DEFAULT_SUBMIT_DIALOG_ACTION_SHORTCUTS, options?.shortcuts),
|
|
1964
|
+
});
|
|
1965
|
+
return this;
|
|
1966
|
+
}
|
|
1967
|
+
custom(action, options) {
|
|
1968
|
+
const item = {
|
|
1969
|
+
...action,
|
|
1970
|
+
shortcuts: resolveDialogActionShortcuts(action.shortcuts ?? [], options?.shortcuts),
|
|
1971
|
+
};
|
|
1972
|
+
const position = item.position ?? 'suffix';
|
|
1973
|
+
if (position === 'prefix') {
|
|
1974
|
+
if (!this.dialogBuilder['dialogState'].actions.footer.prefix) {
|
|
1975
|
+
this.dialogBuilder['dialogState'].actions.footer.prefix = [];
|
|
1976
|
+
}
|
|
1977
|
+
this.dialogBuilder['dialogState'].actions.footer.prefix.push(item);
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
if (!this.dialogBuilder['dialogState'].actions.footer.suffix) {
|
|
1981
|
+
this.dialogBuilder['dialogState'].actions.footer.suffix = [];
|
|
1982
|
+
}
|
|
1983
|
+
this.dialogBuilder['dialogState'].actions.footer.suffix.push(item);
|
|
1984
|
+
}
|
|
1985
|
+
return this;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region ---- Step Wizard Builder Implementation ----
|
|
1990
|
+
/**
|
|
1991
|
+
* Step Builder - Builds individual steps for step wizard
|
|
1992
|
+
*/
|
|
1993
|
+
class StepBuilder {
|
|
1994
|
+
constructor(id, title) {
|
|
1995
|
+
this.id = id;
|
|
1996
|
+
this.title = title;
|
|
1997
|
+
this.skippable = false;
|
|
1998
|
+
this.inheritanceContext = {};
|
|
1999
|
+
}
|
|
2000
|
+
setIcon(icon) {
|
|
2001
|
+
this.icon = icon;
|
|
2002
|
+
return this;
|
|
2003
|
+
}
|
|
2004
|
+
setDescription(description) {
|
|
2005
|
+
this.description = description;
|
|
2006
|
+
return this;
|
|
2007
|
+
}
|
|
2008
|
+
setSkippable(skippable) {
|
|
2009
|
+
this.skippable = skippable;
|
|
2010
|
+
return this;
|
|
2011
|
+
}
|
|
2012
|
+
content(delegate) {
|
|
2013
|
+
if (delegate) {
|
|
2014
|
+
// Create LayoutBuilder in injection context (will be called from component)
|
|
2015
|
+
const layoutBuilder = new LayoutBuilder();
|
|
2016
|
+
// ✅ IMPORTANT: Propagate inheritance context to content
|
|
2017
|
+
layoutBuilder.inheritanceContext = { ...this.inheritanceContext };
|
|
2018
|
+
delegate(layoutBuilder);
|
|
2019
|
+
this.contentNode = layoutBuilder.build();
|
|
2020
|
+
}
|
|
2021
|
+
return this;
|
|
2022
|
+
}
|
|
2023
|
+
withInheritanceContext(context) {
|
|
2024
|
+
this.inheritanceContext = mergeInheritanceContext(context);
|
|
2025
|
+
return this;
|
|
2026
|
+
}
|
|
2027
|
+
build() {
|
|
2028
|
+
if (!this.contentNode) {
|
|
2029
|
+
throw new Error(`Step '${this.id}' must have content`);
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
id: this.id,
|
|
2033
|
+
title: this.title,
|
|
2034
|
+
icon: this.icon,
|
|
2035
|
+
description: this.description,
|
|
2036
|
+
skippable: this.skippable,
|
|
2037
|
+
content: this.contentNode,
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Step Wizard Builder - Builds step wizard widget
|
|
2043
|
+
*/
|
|
2044
|
+
class StepWizardBuilder extends LayoutContainerMixin {
|
|
2045
|
+
constructor() {
|
|
2046
|
+
super('step-wizard');
|
|
2047
|
+
this.steps = [];
|
|
2048
|
+
this.linear = true;
|
|
2049
|
+
this.wizardDirection = 'horizontal';
|
|
2050
|
+
}
|
|
2051
|
+
setLinear(linear) {
|
|
2052
|
+
this.linear = linear;
|
|
2053
|
+
return this;
|
|
2054
|
+
}
|
|
2055
|
+
setDirection(direction) {
|
|
2056
|
+
this.wizardDirection = direction;
|
|
2057
|
+
return this;
|
|
2058
|
+
}
|
|
2059
|
+
setLook(look) {
|
|
2060
|
+
this.look = look;
|
|
2061
|
+
return this;
|
|
2062
|
+
}
|
|
2063
|
+
setShowActions(show) {
|
|
2064
|
+
this.showActions = show;
|
|
2065
|
+
return this;
|
|
2066
|
+
}
|
|
2067
|
+
setActions(actions) {
|
|
2068
|
+
this.wizardActions = actions;
|
|
2069
|
+
return this;
|
|
2070
|
+
}
|
|
2071
|
+
setGuards(guards) {
|
|
2072
|
+
this.guards = guards;
|
|
2073
|
+
return this;
|
|
2074
|
+
}
|
|
2075
|
+
setEvents(events) {
|
|
2076
|
+
this.events = events;
|
|
2077
|
+
return this;
|
|
2078
|
+
}
|
|
2079
|
+
step(id, title, delegate) {
|
|
2080
|
+
const stepBuilder = new StepBuilder(id, title);
|
|
2081
|
+
// ✅ IMPORTANT: Propagate inheritance context to step
|
|
2082
|
+
stepBuilder.withInheritanceContext(this.inheritanceContext);
|
|
2083
|
+
if (delegate) {
|
|
2084
|
+
delegate(stepBuilder);
|
|
2085
|
+
}
|
|
2086
|
+
this.steps.push(stepBuilder.build());
|
|
2087
|
+
return this;
|
|
2088
|
+
}
|
|
2089
|
+
build() {
|
|
2090
|
+
// ✅ Validation
|
|
2091
|
+
if (this.steps.length === 0) {
|
|
2092
|
+
throw new Error('StepWizard must have at least one step');
|
|
2093
|
+
}
|
|
2094
|
+
// ✅ Check duplicate IDs
|
|
2095
|
+
const ids = this.steps.map((s) => s.id);
|
|
2096
|
+
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
2097
|
+
if (duplicates.length > 0) {
|
|
2098
|
+
throw new Error(`Duplicate step IDs found: ${duplicates.join(', ')}`);
|
|
2099
|
+
}
|
|
2100
|
+
const definition = {
|
|
2101
|
+
steps: this.steps,
|
|
2102
|
+
linear: this.linear,
|
|
2103
|
+
direction: this.wizardDirection,
|
|
2104
|
+
look: this.look,
|
|
2105
|
+
// Do not auto-detect based on dialog context; keep explicit or undefined
|
|
2106
|
+
showActions: this.showActions,
|
|
2107
|
+
actions: this.wizardActions,
|
|
2108
|
+
guards: this.guards,
|
|
2109
|
+
events: this.events,
|
|
2110
|
+
};
|
|
2111
|
+
const node = {
|
|
2112
|
+
name: this.containerState.name,
|
|
2113
|
+
type: 'step-wizard',
|
|
2114
|
+
options: { definition },
|
|
2115
|
+
mode: this.containerState.mode,
|
|
2116
|
+
};
|
|
2117
|
+
return node;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
class AXPLayoutRendererComponent {
|
|
2122
|
+
constructor() {
|
|
2123
|
+
this.conversionService = inject(AXPLayoutConversionService);
|
|
2124
|
+
/**
|
|
2125
|
+
* RxJS subjects for context management
|
|
2126
|
+
*/
|
|
2127
|
+
this.contextUpdateSubject = new Subject();
|
|
2128
|
+
this.contextChangeSubject = new Subject();
|
|
2129
|
+
//#region ---- Inputs ----
|
|
2130
|
+
/**
|
|
2131
|
+
* Form definition containing groups and fields OR widget tree
|
|
2132
|
+
*/
|
|
2133
|
+
this.layout = input.required(...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
2134
|
+
/**
|
|
2135
|
+
* Form context/model data
|
|
2136
|
+
*/
|
|
2137
|
+
this.context = model({}, ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
|
|
2138
|
+
/**
|
|
2139
|
+
* Form appearance and density styling (normal, compact, spacious)
|
|
2140
|
+
*/
|
|
2141
|
+
this.look = input('fieldset', ...(ngDevMode ? [{ debugName: "look" }] : /* istanbul ignore next */ []));
|
|
2142
|
+
/**
|
|
2143
|
+
* Default form mode. Can be overridden by section/group and field.
|
|
2144
|
+
*/
|
|
2145
|
+
this.mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
2146
|
+
//#endregion
|
|
2147
|
+
//#region ---- Widget Tree Conversion ----
|
|
2148
|
+
this.widgetTree = signal(null, ...(ngDevMode ? [{ debugName: "widgetTree" }] : /* istanbul ignore next */ []));
|
|
2149
|
+
/**
|
|
2150
|
+
* Prefer explicit {@link AXPWidgetNode.mode} on the root node (e.g. dialog flex `mode('view')`)
|
|
2151
|
+
* so nested widgets resolve view vs edit correctly; fall back to the layout `mode` input.
|
|
2152
|
+
*/
|
|
2153
|
+
this.effectiveRenderMode = computed(() => this.widgetTree()?.mode ?? this.mode(), ...(ngDevMode ? [{ debugName: "effectiveRenderMode" }] : /* istanbul ignore next */ []));
|
|
2154
|
+
/**
|
|
2155
|
+
* Convert layout data to widget tree when inputs change
|
|
2156
|
+
*/
|
|
2157
|
+
this.conversionEffect = effect(() => {
|
|
2158
|
+
const inputData = this.layout();
|
|
2159
|
+
// Convert to widget tree
|
|
2160
|
+
let tree;
|
|
2161
|
+
if (this.isFormDefinition(inputData)) {
|
|
2162
|
+
// Convert form definition to widget tree
|
|
2163
|
+
tree = this.conversionService.convertFormDefinition(inputData);
|
|
2164
|
+
}
|
|
2165
|
+
else if (this.isWidgetNode(inputData)) {
|
|
2166
|
+
// Use widget tree directly
|
|
2167
|
+
tree = inputData;
|
|
2168
|
+
}
|
|
2169
|
+
else {
|
|
2170
|
+
console.warn('AXPLayoutRendererComponent: Invalid layout input. Expected AXPDynamicFormDefinition or AXPWidgetNode.', inputData);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
// Update widget tree only if changed (Angular effect already prevents unnecessary runs)
|
|
2174
|
+
const prev = this.widgetTree();
|
|
2175
|
+
if (!isEqual(prev, tree)) {
|
|
2176
|
+
this.widgetTree.set(tree);
|
|
2177
|
+
}
|
|
2178
|
+
}, ...(ngDevMode ? [{ debugName: "conversionEffect" }] : /* istanbul ignore next */ []));
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region ---- Outputs ----
|
|
2181
|
+
/**
|
|
2182
|
+
* Emitted when context change is initiated
|
|
2183
|
+
*/
|
|
2184
|
+
this.contextInitiated = output();
|
|
2185
|
+
/**
|
|
2186
|
+
* Emitted when form becomes valid/invalid
|
|
2187
|
+
*/
|
|
2188
|
+
this.validityChange = output();
|
|
2189
|
+
//#endregion
|
|
2190
|
+
//#region ---- Properties ----
|
|
2191
|
+
this.form = viewChild(AXFormComponent, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
|
|
2192
|
+
this.container = viewChild(AXPWidgetContainerComponent, ...(ngDevMode ? [{ debugName: "container" }] : /* istanbul ignore next */ []));
|
|
2193
|
+
/**
|
|
2194
|
+
* Internal context signal for reactivity
|
|
2195
|
+
*/
|
|
2196
|
+
this.internalContext = signal({}, ...(ngDevMode ? [{ debugName: "internalContext" }] : /* istanbul ignore next */ []));
|
|
2197
|
+
/**
|
|
2198
|
+
* Initial context for reset functionality
|
|
2199
|
+
*/
|
|
2200
|
+
this.initialContext = {};
|
|
2201
|
+
//#endregion
|
|
2202
|
+
//#region ---- Effects ----
|
|
2203
|
+
/**
|
|
2204
|
+
* Effect to sync context changes from external to internal (optimized with RxJS)
|
|
2205
|
+
*/
|
|
2206
|
+
this.#contextSyncEffect = effect(() => {
|
|
2207
|
+
const ctx = this.context() ?? {};
|
|
2208
|
+
this.contextUpdateSubject.next(ctx);
|
|
2209
|
+
}, ...(ngDevMode ? [{ debugName: "#contextSyncEffect" }] : /* istanbul ignore next */ []));
|
|
2210
|
+
/**
|
|
2211
|
+
* Effect to handle widget tree status changes
|
|
2212
|
+
*/
|
|
2213
|
+
this.#widgetStatusEffect = effect(() => {
|
|
2214
|
+
const widgetTree = this.widgetTree();
|
|
2215
|
+
if (widgetTree) {
|
|
2216
|
+
this.container()?.builderService.setStatus(AXPPageStatus.Rendered);
|
|
2217
|
+
}
|
|
2218
|
+
}, ...(ngDevMode ? [{ debugName: "#widgetStatusEffect" }] : /* istanbul ignore next */ []));
|
|
2219
|
+
}
|
|
2220
|
+
//#endregion
|
|
2221
|
+
//#region ---- Lifecycle Methods ----
|
|
2222
|
+
ngOnInit() {
|
|
2223
|
+
// Initialize internal context with input context
|
|
2224
|
+
const ctx = this.context() ?? {};
|
|
2225
|
+
this.internalContext.set(ctx);
|
|
2226
|
+
// Store initial context for reset functionality
|
|
2227
|
+
this.initialContext = cloneDeep(ctx);
|
|
2228
|
+
// Setup RxJS streams for context management
|
|
2229
|
+
this.setupContextStreams();
|
|
2230
|
+
}
|
|
2231
|
+
//#endregion
|
|
2232
|
+
//#region ---- Effects ----
|
|
2233
|
+
/**
|
|
2234
|
+
* Effect to sync context changes from external to internal (optimized with RxJS)
|
|
2235
|
+
*/
|
|
2236
|
+
#contextSyncEffect;
|
|
2237
|
+
/**
|
|
2238
|
+
* Effect to handle widget tree status changes
|
|
2239
|
+
*/
|
|
2240
|
+
#widgetStatusEffect;
|
|
2241
|
+
//#endregion
|
|
2242
|
+
//#region ---- Event Handlers ----
|
|
2243
|
+
/**
|
|
2244
|
+
* Handle context change events from widget container (optimized with RxJS)
|
|
2245
|
+
*/
|
|
2246
|
+
handleContextChanged(event) {
|
|
2247
|
+
if (event.state === 'initiated') {
|
|
2248
|
+
this.contextInitiated.emit(event.data);
|
|
2249
|
+
}
|
|
2250
|
+
else {
|
|
2251
|
+
this.contextChangeSubject.next(event.data ?? {});
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
//#endregion
|
|
2255
|
+
//#region ---- Public Methods ----
|
|
2256
|
+
/**
|
|
2257
|
+
* Get the form component instance
|
|
2258
|
+
*/
|
|
2259
|
+
getForm() {
|
|
2260
|
+
return this.form();
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Get the widget container component instance
|
|
2264
|
+
*/
|
|
2265
|
+
getContainer() {
|
|
2266
|
+
return this.container();
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Get current form context
|
|
2270
|
+
*/
|
|
2271
|
+
getContext() {
|
|
2272
|
+
return this.internalContext();
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Update form context programmatically
|
|
2276
|
+
*/
|
|
2277
|
+
updateContext(context) {
|
|
2278
|
+
this.internalContext.set(context);
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Get the current widget tree
|
|
2282
|
+
*/
|
|
2283
|
+
getWidgetTree() {
|
|
2284
|
+
return this.widgetTree();
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Validate the form
|
|
2288
|
+
*/
|
|
2289
|
+
async validate() {
|
|
2290
|
+
const form = this.form();
|
|
2291
|
+
if (form) {
|
|
2292
|
+
const isValid = await form.validate();
|
|
2293
|
+
this.validityChange.emit(isValid.result);
|
|
2294
|
+
return isValid;
|
|
2295
|
+
}
|
|
2296
|
+
return {
|
|
2297
|
+
result: false,
|
|
2298
|
+
messages: [],
|
|
2299
|
+
rules: [],
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Clear the form context
|
|
2304
|
+
*/
|
|
2305
|
+
clear() {
|
|
2306
|
+
// Clear internal context
|
|
2307
|
+
this.internalContext.set({});
|
|
2308
|
+
// Update the model signal
|
|
2309
|
+
this.context.set({});
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Reset the form to its initial state
|
|
2313
|
+
*/
|
|
2314
|
+
reset() {
|
|
2315
|
+
// Reset to initial context
|
|
2316
|
+
const resetContext = cloneDeep(this.initialContext);
|
|
2317
|
+
this.internalContext.set(resetContext);
|
|
2318
|
+
// Update the model signal
|
|
2319
|
+
this.context.set(resetContext);
|
|
2320
|
+
}
|
|
2321
|
+
//#endregion
|
|
2322
|
+
//#region ---- RxJS Stream Setup ----
|
|
2323
|
+
/**
|
|
2324
|
+
* Setup RxJS streams for context management
|
|
2325
|
+
*/
|
|
2326
|
+
setupContextStreams() {
|
|
2327
|
+
// Debounced context updates from external source
|
|
2328
|
+
this.contextUpdateSubject
|
|
2329
|
+
.pipe(debounceTime(16), // ~60fps
|
|
2330
|
+
distinctUntilChanged((prev, curr) => isEqual(prev, curr)), startWith(this.context() ?? {}))
|
|
2331
|
+
.subscribe((ctx) => {
|
|
2332
|
+
this.internalContext.set(ctx);
|
|
2333
|
+
});
|
|
2334
|
+
// Debounced context changes from widgets
|
|
2335
|
+
this.contextChangeSubject
|
|
2336
|
+
.pipe(debounceTime(16), // ~60fps
|
|
2337
|
+
distinctUntilChanged((prev, curr) => isEqual(prev, curr)))
|
|
2338
|
+
.subscribe((ctx) => {
|
|
2339
|
+
this.internalContext.set(ctx);
|
|
2340
|
+
// Update the model signal directly - it will emit change events automatically
|
|
2341
|
+
this.context.set(this.internalContext());
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
//#endregion
|
|
2345
|
+
//#region ---- Type Guards ----
|
|
2346
|
+
/**
|
|
2347
|
+
* Type guard to check if the input is a form definition
|
|
2348
|
+
*/
|
|
2349
|
+
isFormDefinition(data) {
|
|
2350
|
+
return data && typeof data === 'object' && 'groups' in data && Array.isArray(data.groups);
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Type guard to check if the input is a widget node
|
|
2354
|
+
*/
|
|
2355
|
+
isWidgetNode(data) {
|
|
2356
|
+
return data && typeof data === 'object' && 'type' in data && typeof data.type === 'string';
|
|
2357
|
+
}
|
|
2358
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2359
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPLayoutRendererComponent, isStandalone: true, selector: "axp-layout-renderer", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, look: { classPropertyName: "look", publicName: "look", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { context: "contextChange", contextInitiated: "contextInitiated", validityChange: "validityChange" }, viewQueries: [{ propertyName: "form", first: true, predicate: AXFormComponent, descendants: true, isSignal: true }, { propertyName: "container", first: true, predicate: AXPWidgetContainerComponent, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
2360
|
+
<ax-form>
|
|
2361
|
+
<axp-widgets-container [context]="internalContext()" (onContextChanged)="handleContextChanged($event)">
|
|
2362
|
+
@if (widgetTree()) {
|
|
2363
|
+
<ng-container
|
|
2364
|
+
axp-widget-renderer
|
|
2365
|
+
[node]="widgetTree()!"
|
|
2366
|
+
[mode]="effectiveRenderMode()"
|
|
2367
|
+
></ng-container>
|
|
2368
|
+
}
|
|
2369
|
+
</axp-widgets-container>
|
|
2370
|
+
</ax-form>
|
|
2371
|
+
`, isInline: true, styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: AXPWidgetCoreModule }, { kind: "component", type: i1.AXPWidgetContainerComponent, selector: "axp-widgets-container", inputs: ["context", "functions"], outputs: ["onContextChanged"] }, { kind: "directive", type: i1.AXPWidgetRendererDirective, selector: "[axp-widget-renderer]", inputs: ["parentNode", "index", "mode", "node"], outputs: ["onOptionsChanged", "onValueChanged", "onLoad"], exportAs: ["widgetRenderer"] }, { kind: "ngmodule", type: AXFormModule }, { kind: "component", type: i2.AXFormComponent, selector: "ax-form", inputs: ["disabled", "readonly", "labelMode", "look", "messageStyle", "updateOn", "inUserInteractionActive"], outputs: ["onValidate", "updateOnChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2372
|
+
}
|
|
2373
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPLayoutRendererComponent, decorators: [{
|
|
2374
|
+
type: Component,
|
|
2375
|
+
args: [{ selector: 'axp-layout-renderer', standalone: true, imports: [AXPWidgetCoreModule, AXFormModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2376
|
+
<ax-form>
|
|
2377
|
+
<axp-widgets-container [context]="internalContext()" (onContextChanged)="handleContextChanged($event)">
|
|
2378
|
+
@if (widgetTree()) {
|
|
2379
|
+
<ng-container
|
|
2380
|
+
axp-widget-renderer
|
|
2381
|
+
[node]="widgetTree()!"
|
|
2382
|
+
[mode]="effectiveRenderMode()"
|
|
2383
|
+
></ng-container>
|
|
2384
|
+
}
|
|
2385
|
+
</axp-widgets-container>
|
|
2386
|
+
</ax-form>
|
|
2387
|
+
`, styles: [":host{display:block;width:100%}\n"] }]
|
|
2388
|
+
}], propDecorators: { layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: true }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }, { type: i0.Output, args: ["contextChange"] }], look: [{ type: i0.Input, args: [{ isSignal: true, alias: "look", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], contextInitiated: [{ type: i0.Output, args: ["contextInitiated"] }], validityChange: [{ type: i0.Output, args: ["validityChange"] }], form: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXFormComponent), { isSignal: true }] }], container: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXPWidgetContainerComponent), { isSignal: true }] }] } });
|
|
2389
|
+
|
|
2390
|
+
class LayoutBuilderModule {
|
|
2391
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LayoutBuilderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
2392
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: LayoutBuilderModule, imports: [CommonModule, AXPLayoutRendererComponent], exports: [AXPLayoutRendererComponent] }); }
|
|
2393
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LayoutBuilderModule, providers: [
|
|
2394
|
+
AXPLayoutBuilderService,
|
|
2395
|
+
provideCommandSetups([
|
|
2396
|
+
{
|
|
2397
|
+
key: AXP_PREVIEW_WIDGET_FIELD_COMMAND_KEY,
|
|
2398
|
+
command: () => Promise.resolve().then(function () { return previewWidgetField_command; }).then((c) => c.AXPPreviewWidgetFieldCommand),
|
|
2399
|
+
},
|
|
2400
|
+
]),
|
|
2401
|
+
], imports: [CommonModule, AXPLayoutRendererComponent] }); }
|
|
2402
|
+
}
|
|
2403
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: LayoutBuilderModule, decorators: [{
|
|
2404
|
+
type: NgModule,
|
|
2405
|
+
args: [{
|
|
2406
|
+
imports: [CommonModule, AXPLayoutRendererComponent],
|
|
2407
|
+
providers: [
|
|
2408
|
+
AXPLayoutBuilderService,
|
|
2409
|
+
provideCommandSetups([
|
|
2410
|
+
{
|
|
2411
|
+
key: AXP_PREVIEW_WIDGET_FIELD_COMMAND_KEY,
|
|
2412
|
+
command: () => Promise.resolve().then(function () { return previewWidgetField_command; }).then((c) => c.AXPPreviewWidgetFieldCommand),
|
|
2413
|
+
},
|
|
2414
|
+
]),
|
|
2415
|
+
],
|
|
2416
|
+
exports: [AXPLayoutRendererComponent],
|
|
2417
|
+
}]
|
|
2418
|
+
}] });
|
|
2419
|
+
|
|
2420
|
+
//#region ---- Contracts barrel ----
|
|
2421
|
+
//#endregion
|
|
2422
|
+
|
|
2423
|
+
/**
|
|
2424
|
+
* Returns true when a nested overlay is open and Esc should close it first.
|
|
2425
|
+
* Returns false when only the dialog shell is on top.
|
|
2426
|
+
* Returns null when the Acorex overlay service does not expose open-state queries yet.
|
|
2427
|
+
*/
|
|
2428
|
+
function hasOpenNestedOverlayFromAcorexService(overlayService, dialogOverlay) {
|
|
2429
|
+
const service = overlayService;
|
|
2430
|
+
if (typeof service.hasOpenAnchoredOverlay === 'function') {
|
|
2431
|
+
if (!service.hasOpenAnchoredOverlay()) {
|
|
2432
|
+
return false;
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof service.hasOverlayAbove === 'function') {
|
|
2435
|
+
return service.hasOverlayAbove(dialogOverlay);
|
|
2436
|
+
}
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
if (typeof service.hasOverlayAbove === 'function') {
|
|
2440
|
+
return service.hasOverlayAbove(dialogOverlay);
|
|
2441
|
+
}
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* DOM fallback until {@link AXOverlayService} exposes open-overlay queries.
|
|
2446
|
+
*
|
|
2447
|
+
* Dialog popups use a centered `.ax-overlay-container` without `.ax-overlay-pane`.
|
|
2448
|
+
* Widget popovers (select, datetime, dropdown) use anchored containers with `.ax-overlay-pane`.
|
|
2449
|
+
*/
|
|
2450
|
+
function shouldDeferEscapeToNestedOverlayFromDom(document, dialogHost) {
|
|
2451
|
+
const dialogOverlay = findOverlayContainerAncestor(dialogHost);
|
|
2452
|
+
if (getNestedVisibleOverlayPanes(document, dialogOverlay).length > 0) {
|
|
2453
|
+
return true;
|
|
2454
|
+
}
|
|
2455
|
+
const topContainer = getTopVisibleOverlayContainer(document);
|
|
2456
|
+
if (!topContainer) {
|
|
2457
|
+
return false;
|
|
2458
|
+
}
|
|
2459
|
+
if (!dialogOverlay) {
|
|
2460
|
+
return topContainer.contains(dialogHost) === false;
|
|
2461
|
+
}
|
|
2462
|
+
return topContainer !== dialogOverlay;
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* When a nested overlay is above this dialog (widget popover, confirm box, etc.), footer shortcuts
|
|
2466
|
+
* should not run on the parent dialog.
|
|
2467
|
+
*/
|
|
2468
|
+
function shouldDeferDialogShortcutsToNestedOverlay(document, dialogHost, overlayService) {
|
|
2469
|
+
const dialogOverlay = findOverlayContainerAncestor(dialogHost);
|
|
2470
|
+
if (overlayService) {
|
|
2471
|
+
const fromService = hasOpenNestedOverlayFromAcorexService(overlayService, dialogOverlay);
|
|
2472
|
+
if (fromService !== null) {
|
|
2473
|
+
return fromService;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
return shouldDeferEscapeToNestedOverlayFromDom(document, dialogHost);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/**
|
|
2480
|
+
* Builds the `show()` resolve payload for layout-builder custom footer commands (fallback path).
|
|
2481
|
+
*/
|
|
2482
|
+
function buildLayoutBuilderCustomActionRef(createDialogRef, command) {
|
|
2483
|
+
return {
|
|
2484
|
+
...createDialogRef(command),
|
|
2485
|
+
action: () => command,
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Keeps legacy popup `data.context` / `data.action` side fields in sync for custom footer commands.
|
|
2490
|
+
*/
|
|
2491
|
+
function syncLegacyDialogDataSideFields(data, context, action) {
|
|
2492
|
+
if (!data) {
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
data.context = context;
|
|
2496
|
+
data.action = action;
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Cancel from an `onAction` handler should route through the dirty-close gate only when configured.
|
|
2500
|
+
* Otherwise preserve the original resolve-then-close sequence.
|
|
2501
|
+
*/
|
|
2502
|
+
function shouldRouteOnActionCancelThroughDismissGate(confirmCloseWhenDirtyEnabled) {
|
|
2503
|
+
return confirmCloseWhenDirtyEnabled === true;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/** Debounce after widget count stabilizes before re-evaluating footer actions. */
|
|
2507
|
+
const DIALOG_WIDGET_COUNT_STABLE_DELAY_MS = 350;
|
|
2508
|
+
class AXPDialogRendererComponent extends AXBasePageComponent {
|
|
2509
|
+
constructor() {
|
|
2510
|
+
super(...arguments);
|
|
2511
|
+
this.result = new EventEmitter();
|
|
2512
|
+
this.expressionEvaluator = inject(AXPExpressionEvaluatorService);
|
|
2513
|
+
this.commandService = inject(AXPCommandService);
|
|
2514
|
+
this.hookService = inject(AXPHookService, { optional: true });
|
|
2515
|
+
this.unsavedChangesConfirm = inject(AXPUnsavedChangesConfirmService);
|
|
2516
|
+
this.overlayService = inject(AXOverlayService);
|
|
2517
|
+
this.translationService = inject(AXTranslationService);
|
|
2518
|
+
this.document = inject(DOCUMENT);
|
|
2519
|
+
this.host = inject((ElementRef));
|
|
2520
|
+
this.shortcutRegistry = inject(AXPKeyboardShortcutRegistry);
|
|
2521
|
+
/** Ensures `show()` resolves once when the dialog closes (footer action or header close). */
|
|
2522
|
+
this.callbackInvoked = false;
|
|
2523
|
+
/** Skips dirty confirmation on the next {@link onClosing} (successful submit / programmatic close). */
|
|
2524
|
+
this.skipNextOnClosingDirtyCheck = false;
|
|
2525
|
+
/** Blocks re-entrant {@link onClosing} while the first close gate is still resolving. */
|
|
2526
|
+
this.closeGateInProgress = false;
|
|
2527
|
+
this.destroyRef = inject(DestroyRef);
|
|
2528
|
+
this.context = signal({}, ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
|
|
2529
|
+
// This will be set by the popup service automatically - same as dynamic-dialog
|
|
2530
|
+
this.callBack = () => { };
|
|
2531
|
+
this.isDialogLoading = signal(false, ...(ngDevMode ? [{ debugName: "isDialogLoading" }] : /* istanbul ignore next */ []));
|
|
2532
|
+
// Aggregated actions for footer rendering
|
|
2533
|
+
this.footerPrefix = signal([], ...(ngDevMode ? [{ debugName: "footerPrefix" }] : /* istanbul ignore next */ []));
|
|
2534
|
+
this.footerSuffix = signal([], ...(ngDevMode ? [{ debugName: "footerSuffix" }] : /* istanbul ignore next */ []));
|
|
2535
|
+
/**
|
|
2536
|
+
* Correlate layout context snapshots for distributed hooks (`layout-builder.dialog.context-changed`).
|
|
2537
|
+
*/
|
|
2538
|
+
this.contextChangedHooksSessionKey = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
2539
|
+
? crypto.randomUUID()
|
|
2540
|
+
: `layout-dialog-ctx-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2541
|
+
/**
|
|
2542
|
+
* Registers footer shortcuts on the platform registry at modal priority so they win over page
|
|
2543
|
+
* bindings (e.g. Ctrl+S on entity details) and stop propagation.
|
|
2544
|
+
*/
|
|
2545
|
+
this.#dialogShortcutsEffect = effect(() => {
|
|
2546
|
+
this.footerPrefix();
|
|
2547
|
+
this.footerSuffix();
|
|
2548
|
+
this.isDialogLoading();
|
|
2549
|
+
untracked(() => this.refreshDialogKeyboardShortcuts());
|
|
2550
|
+
}, ...(ngDevMode ? [{ debugName: "#dialogShortcutsEffect" }] : /* istanbul ignore next */ []));
|
|
2551
|
+
//#endregion
|
|
2552
|
+
//#region ---- View Accessors ----
|
|
2553
|
+
// Access the internal layout renderer to reach the widgets container injector
|
|
2554
|
+
this.layoutRenderer = viewChild(AXPLayoutRendererComponent, ...(ngDevMode ? [{ debugName: "layoutRenderer" }] : /* istanbul ignore next */ []));
|
|
2555
|
+
this.#widgetActionsEffect = effect(() => {
|
|
2556
|
+
this.aggregateAndEvaluateActions();
|
|
2557
|
+
if (!this.widgetCoreService) {
|
|
2558
|
+
const renderer = this.layoutRenderer();
|
|
2559
|
+
const container = renderer?.getContainer();
|
|
2560
|
+
this.widgetCoreService = container?.builderService ?? null;
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
this.widgetCoreService.registeredWidgetsCount();
|
|
2564
|
+
if (this.debounceTimer) {
|
|
2565
|
+
clearTimeout(this.debounceTimer);
|
|
2566
|
+
}
|
|
2567
|
+
this.debounceTimer = setTimeout(() => {
|
|
2568
|
+
this.aggregateAndEvaluateActions();
|
|
2569
|
+
}, DIALOG_WIDGET_COUNT_STABLE_DELAY_MS);
|
|
2570
|
+
}, ...(ngDevMode ? [{ debugName: "#widgetActionsEffect" }] : /* istanbul ignore next */ []));
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Registers footer shortcuts on the platform registry at modal priority so they win over page
|
|
2574
|
+
* bindings (e.g. Ctrl+S on entity details) and stop propagation.
|
|
2575
|
+
*/
|
|
2576
|
+
#dialogShortcutsEffect;
|
|
2577
|
+
//#endregion
|
|
2578
|
+
//#region ---- Lifecycle ----
|
|
2579
|
+
ngOnInit() {
|
|
2580
|
+
const initialContext = this.config?.context || {};
|
|
2581
|
+
this.context.set(initialContext);
|
|
2582
|
+
void this.invokeLayoutContextChangedHooks();
|
|
2583
|
+
}
|
|
2584
|
+
//#endregion
|
|
2585
|
+
//#region ---- Keyboard Shortcuts ----
|
|
2586
|
+
refreshDialogKeyboardShortcuts() {
|
|
2587
|
+
const shortcuts = this.buildDialogFooterShortcutBindings();
|
|
2588
|
+
const dialogTitle = this.config?.title;
|
|
2589
|
+
const scope = typeof dialogTitle === 'string' && dialogTitle.startsWith('@')
|
|
2590
|
+
? dialogTitle
|
|
2591
|
+
: '@general:keyboard-shortcuts.groups.dialog';
|
|
2592
|
+
this.shortcutRegistry.register({
|
|
2593
|
+
owner: this.destroyRef,
|
|
2594
|
+
elementRef: this.host,
|
|
2595
|
+
id: 'layout-builder-dialog-footer',
|
|
2596
|
+
priority: AXPKeyboardShortcutPriority.Modal,
|
|
2597
|
+
scope,
|
|
2598
|
+
when: () => this.isDialogShortcutRegistrationActive(),
|
|
2599
|
+
shortcuts,
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
isDialogShortcutRegistrationActive() {
|
|
2603
|
+
if (this.isDialogLoading()) {
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
if (this.unsavedChangesConfirm.isConfirmPending()) {
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
return !shouldDeferDialogShortcutsToNestedOverlay(this.document, this.host.nativeElement, this.overlayService);
|
|
2610
|
+
}
|
|
2611
|
+
buildDialogFooterShortcutBindings() {
|
|
2612
|
+
const shortcuts = [];
|
|
2613
|
+
const group = '@general:keyboard-shortcuts.groups.dialog';
|
|
2614
|
+
const actions = [...this.footerPrefixActions(), ...this.footerSuffixActions()];
|
|
2615
|
+
const confirmDirty = !!this.config?.confirmCloseWhenDirty?.enabled;
|
|
2616
|
+
for (const action of actions) {
|
|
2617
|
+
if (action.disabled || action.hidden || !action.shortcuts?.length) {
|
|
2618
|
+
continue;
|
|
2619
|
+
}
|
|
2620
|
+
for (const shortcut of action.shortcuts) {
|
|
2621
|
+
const chord = normalizeDialogActionShortcutChord(shortcut);
|
|
2622
|
+
if (confirmDirty && chord === 'escape') {
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
shortcuts.push({
|
|
2626
|
+
keys: [chord],
|
|
2627
|
+
title: action.title ?? '',
|
|
2628
|
+
group,
|
|
2629
|
+
allowInEditableFields: isDialogShortcutAllowedInEditableField(shortcut),
|
|
2630
|
+
when: () => this.isDialogFooterShortcutActive(action),
|
|
2631
|
+
handler: () => {
|
|
2632
|
+
void this.executeAction(action);
|
|
2633
|
+
},
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if (confirmDirty) {
|
|
2638
|
+
shortcuts.push({
|
|
2639
|
+
keys: ['escape'],
|
|
2640
|
+
title: '@general:actions.discard.title',
|
|
2641
|
+
group,
|
|
2642
|
+
allowInEditableFields: true,
|
|
2643
|
+
when: () => this.isDialogShortcutRegistrationActive(),
|
|
2644
|
+
handler: () => {
|
|
2645
|
+
this.requestDismiss();
|
|
2646
|
+
},
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
return shortcuts;
|
|
2650
|
+
}
|
|
2651
|
+
isDialogFooterShortcutActive(action) {
|
|
2652
|
+
if (!this.isDialogShortcutRegistrationActive()) {
|
|
2653
|
+
return false;
|
|
2654
|
+
}
|
|
2655
|
+
return !action.disabled && !action.hidden;
|
|
2656
|
+
}
|
|
2657
|
+
//#endregion
|
|
2658
|
+
//#region ---- Popup Close Gate ----
|
|
2659
|
+
/**
|
|
2660
|
+
* Popup shell hook — handles header **X** and **Esc** (when `closeButton` is enabled).
|
|
2661
|
+
* This is the only place that prompts for unsaved changes.
|
|
2662
|
+
*/
|
|
2663
|
+
async onClosing(e) {
|
|
2664
|
+
if (this.unsavedChangesConfirm.isConfirmPending() || this.closeGateInProgress) {
|
|
2665
|
+
e.cancel = true;
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
this.closeGateInProgress = true;
|
|
2669
|
+
try {
|
|
2670
|
+
if (this.skipNextOnClosingDirtyCheck) {
|
|
2671
|
+
this.skipNextOnClosingDirtyCheck = false;
|
|
2672
|
+
this.completeDismissResolve();
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (!this.config?.confirmCloseWhenDirty?.enabled) {
|
|
2676
|
+
this.completeDismissResolve();
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (!(await this.confirmDismissIfDirty())) {
|
|
2680
|
+
e.cancel = true;
|
|
2681
|
+
this.pendingDismissResolvePayload = undefined;
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
this.completeDismissResolve();
|
|
2685
|
+
}
|
|
2686
|
+
finally {
|
|
2687
|
+
this.closeGateInProgress = false;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Footer cancel and other in-app dismiss actions. Routes through `super.close()` so the
|
|
2692
|
+
* popup shell invokes {@link onClosing} exactly once (same path as **X** / **Esc**).
|
|
2693
|
+
*/
|
|
2694
|
+
requestDismiss(resolvePayload) {
|
|
2695
|
+
this.pendingDismissResolvePayload = resolvePayload ?? this.createDialogRef('cancel');
|
|
2696
|
+
super.close();
|
|
2697
|
+
}
|
|
2698
|
+
/** Invokes `show()` callback with the pending payload or a default cancel {@link AXPDialogRef}. */
|
|
2699
|
+
completeDismissResolve() {
|
|
2700
|
+
const payload = this.pendingDismissResolvePayload;
|
|
2701
|
+
this.pendingDismissResolvePayload = undefined;
|
|
2702
|
+
if (payload !== undefined) {
|
|
2703
|
+
this.resolveDialog(payload);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
if (!this.callbackInvoked) {
|
|
2707
|
+
this.resolveDialog(this.createDialogRef('cancel'));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
//#endregion
|
|
2711
|
+
//#region ---- Dirty State ----
|
|
2712
|
+
getWidgetContainer() {
|
|
2713
|
+
return this.layoutRenderer()?.getContainer();
|
|
2714
|
+
}
|
|
2715
|
+
#widgetActionsEffect;
|
|
2716
|
+
//#endregion
|
|
2717
|
+
handleContextChanged(event) {
|
|
2718
|
+
this.context.set(event);
|
|
2719
|
+
this.aggregateAndEvaluateActions();
|
|
2720
|
+
void this.invokeLayoutContextChangedHooks();
|
|
2721
|
+
}
|
|
2722
|
+
handleContextInitiated(event) {
|
|
2723
|
+
this.context.set(event);
|
|
2724
|
+
this.aggregateAndEvaluateActions();
|
|
2725
|
+
void this.invokeLayoutContextChangedHooks();
|
|
2726
|
+
}
|
|
2727
|
+
async invokeLayoutContextChangedHooks() {
|
|
2728
|
+
const meta = this.config?.metadata;
|
|
2729
|
+
if (!this.hookService) {
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const payload = {
|
|
2733
|
+
sessionKey: this.contextChangedHooksSessionKey,
|
|
2734
|
+
getContext: () => {
|
|
2735
|
+
const store = this.getWidgetContainer()?.contextService;
|
|
2736
|
+
return (store?.data() ?? this.context() ?? {});
|
|
2737
|
+
},
|
|
2738
|
+
metadata: meta,
|
|
2739
|
+
patchContext: (partial) => {
|
|
2740
|
+
const merged = merge({}, this.context(), partial);
|
|
2741
|
+
this.context.set(merged);
|
|
2742
|
+
this.layoutRenderer()?.updateContext(merged);
|
|
2743
|
+
},
|
|
2744
|
+
setLoading: (loading) => this.isDialogLoading.set(loading),
|
|
2745
|
+
};
|
|
2746
|
+
try {
|
|
2747
|
+
await this.hookService.runAsync(AXP_LAYOUT_BUILDER_DIALOG_CONTEXT_CHANGED_HOOK_KEY, payload);
|
|
2748
|
+
}
|
|
2749
|
+
catch {
|
|
2750
|
+
// Hook providers are best-effort; avoid breaking the dialog lifecycle.
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
footerPrefixActions() {
|
|
2754
|
+
return this.footerPrefix();
|
|
2755
|
+
}
|
|
2756
|
+
footerSuffixActions() {
|
|
2757
|
+
return this.footerSuffix();
|
|
2758
|
+
}
|
|
2759
|
+
isFormLoading() {
|
|
2760
|
+
return this.isDialogLoading();
|
|
2761
|
+
}
|
|
2762
|
+
isSubmitting() {
|
|
2763
|
+
return this.isDialogLoading();
|
|
2764
|
+
}
|
|
2765
|
+
async executeAction(action) {
|
|
2766
|
+
const cmd = this.resolveActionCommandName(action.command);
|
|
2767
|
+
if (this.shouldValidateBeforeAction(cmd)) {
|
|
2768
|
+
const isValid = await this.layoutRenderer()?.validate();
|
|
2769
|
+
if (!isValid?.result) {
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
//TODO: matin, why we need this? maybe we can remove it?
|
|
2774
|
+
if (cmd?.startsWith('widget:')) {
|
|
2775
|
+
const parsed = this.parseWidgetCommand(cmd);
|
|
2776
|
+
if (parsed.widgetName && parsed.action) {
|
|
2777
|
+
await this.invokeWidget(parsed.widgetName, parsed.action, {});
|
|
2778
|
+
await this.aggregateAndEvaluateActions();
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
if (cmd && this.commandService.exists(cmd)) {
|
|
2783
|
+
const dialogRef = this.createDialogRef(cmd);
|
|
2784
|
+
const integration = (this.config.metadata ?? {});
|
|
2785
|
+
try {
|
|
2786
|
+
const cmdResult = await this.commandService.execute(cmd, { dialogRef, integration });
|
|
2787
|
+
if (!cmdResult?.success) {
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
if (this.shouldKeepDialogOpenAfterCommandResult(cmdResult)) {
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
this.resolveDialog(cmdResult);
|
|
2794
|
+
await this.closeWithOptionalSkipValidate(cmdResult);
|
|
2795
|
+
}
|
|
2796
|
+
catch (error) {
|
|
2797
|
+
console.error('Error executing action', cmd, error);
|
|
2798
|
+
}
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
const context = this.context();
|
|
2802
|
+
const onAction = this.config?.onAction;
|
|
2803
|
+
// `onAction` return value is passed through to `show()` unchanged — see dialog-resolve.util.ts.
|
|
2804
|
+
if (onAction) {
|
|
2805
|
+
const dialogRef = this.createDialogRef(cmd);
|
|
2806
|
+
try {
|
|
2807
|
+
const result = await Promise.resolve(onAction(dialogRef));
|
|
2808
|
+
if (this.shouldKeepDialogOpenAfterCommandResult(result)) {
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
if (cmd === 'cancel' &&
|
|
2812
|
+
shouldRouteOnActionCancelThroughDismissGate(this.config?.confirmCloseWhenDirty?.enabled)) {
|
|
2813
|
+
this.isDialogLoading.set(false);
|
|
2814
|
+
this.requestDismiss(result);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
this.resolveDialog(result);
|
|
2818
|
+
await this.closeWithOptionalSkipValidate(result);
|
|
2819
|
+
}
|
|
2820
|
+
catch {
|
|
2821
|
+
// Handler threw: stay open for retry, actions remain clickable
|
|
2822
|
+
}
|
|
2823
|
+
finally {
|
|
2824
|
+
this.isDialogLoading.set(false);
|
|
2825
|
+
}
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
// Fallback: layout-builder custom footer commands (e.g. signature-apply, upload-image).
|
|
2829
|
+
// See dialog-resolve.util.ts — resolves {@link AXPDialogRef}; does not auto-close except cancel.
|
|
2830
|
+
const result = { context, action: cmd };
|
|
2831
|
+
this.dialogResult = result;
|
|
2832
|
+
syncLegacyDialogDataSideFields(this.data, result.context, result.action);
|
|
2833
|
+
const dialogRefPayload = buildLayoutBuilderCustomActionRef((command) => this.createDialogRef(command), cmd);
|
|
2834
|
+
if (cmd === 'cancel') {
|
|
2835
|
+
this.requestDismiss(dialogRefPayload);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
this.resolveDialog(dialogRefPayload);
|
|
2839
|
+
}
|
|
2840
|
+
isDialogDirty() {
|
|
2841
|
+
const confirmOptions = this.config?.confirmCloseWhenDirty;
|
|
2842
|
+
if (!confirmOptions?.enabled) {
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
const container = this.getWidgetContainer();
|
|
2846
|
+
if (!container?.isSavedCommitted()) {
|
|
2847
|
+
return false;
|
|
2848
|
+
}
|
|
2849
|
+
const store = container.contextService;
|
|
2850
|
+
if (typeof confirmOptions.isDirty === 'function') {
|
|
2851
|
+
return confirmOptions.isDirty(store.data(), store.saved());
|
|
2852
|
+
}
|
|
2853
|
+
return container.isFormDirty();
|
|
2854
|
+
}
|
|
2855
|
+
confirmDismissIfDirty() {
|
|
2856
|
+
const confirmOptions = this.config?.confirmCloseWhenDirty;
|
|
2857
|
+
if (!confirmOptions?.enabled || !this.isDialogDirty()) {
|
|
2858
|
+
return Promise.resolve(true);
|
|
2859
|
+
}
|
|
2860
|
+
return this.unsavedChangesConfirm.confirmIfDirty(true, {
|
|
2861
|
+
enabled: confirmOptions.enabled,
|
|
2862
|
+
title: confirmOptions.title,
|
|
2863
|
+
message: confirmOptions.message,
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
/** Whether the layout form should be validated before running this footer command. */
|
|
2867
|
+
shouldValidateBeforeAction(cmd) {
|
|
2868
|
+
if (!cmd || cmd === 'cancel' || cmd === 'entity-form-done' || cmd === 'entity-form-next-step') {
|
|
2869
|
+
return false;
|
|
2870
|
+
}
|
|
2871
|
+
if (cmd.startsWith('widget:')) {
|
|
2872
|
+
return false;
|
|
2873
|
+
}
|
|
2874
|
+
if (this.commandService.exists(cmd)) {
|
|
2875
|
+
return false;
|
|
2876
|
+
}
|
|
2877
|
+
return true;
|
|
2878
|
+
}
|
|
2879
|
+
/** True when a footer handler or command result asks to leave the dialog open (`keepDialogOpen` on the result or `result.data`). */
|
|
2880
|
+
shouldKeepDialogOpenAfterCommandResult(result) {
|
|
2881
|
+
if (!result || typeof result !== 'object') {
|
|
2882
|
+
return false;
|
|
2883
|
+
}
|
|
2884
|
+
const top = result;
|
|
2885
|
+
if (top.keepDialogOpen === true) {
|
|
2886
|
+
return true;
|
|
2887
|
+
}
|
|
2888
|
+
if (top.data != null && typeof top.data === 'object' && 'keepDialogOpen' in top.data) {
|
|
2889
|
+
return top.data.keepDialogOpen === true;
|
|
2890
|
+
}
|
|
2891
|
+
return false;
|
|
2892
|
+
}
|
|
2893
|
+
/** Resolves the dialog `show()` promise exactly once. */
|
|
2894
|
+
resolveDialog(result) {
|
|
2895
|
+
if (this.callbackInvoked) {
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
this.callbackInvoked = true;
|
|
2899
|
+
this.callBack(result);
|
|
2900
|
+
}
|
|
2901
|
+
createDialogRef(actionCmd) {
|
|
2902
|
+
return {
|
|
2903
|
+
close: (res) => {
|
|
2904
|
+
void this.closeWithOptionalSkipValidate(res);
|
|
2905
|
+
},
|
|
2906
|
+
context: () => this.context(),
|
|
2907
|
+
action: () => actionCmd,
|
|
2908
|
+
setLoading: (loading) => this.isDialogLoading.set(loading),
|
|
2909
|
+
patchContext: (partial) => {
|
|
2910
|
+
const merged = merge({}, this.context(), partial);
|
|
2911
|
+
this.context.set(merged);
|
|
2912
|
+
this.layoutRenderer()?.updateContext(merged);
|
|
2913
|
+
},
|
|
2914
|
+
invokeWidget: (widgetName, method, opts) => this.invokeWidget(widgetName, method, opts ?? {}),
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
async closeWithOptionalSkipValidate(result) {
|
|
2918
|
+
if (result && typeof result === 'object' && result.skipValidate) {
|
|
2919
|
+
this.result.emit(result);
|
|
2920
|
+
this.skipNextOnClosingDirtyCheck = true;
|
|
2921
|
+
this.pendingDismissResolvePayload = undefined;
|
|
2922
|
+
super.close(result);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
await this.close(result, { skipDirtyConfirm: true });
|
|
2926
|
+
}
|
|
2927
|
+
/** Resolves footer/widget action command to a string (e.g. `cancel`, `submit`, `widget:...`). */
|
|
2928
|
+
resolveActionCommandName(command) {
|
|
2929
|
+
if (typeof command === 'string') {
|
|
2930
|
+
return command;
|
|
2931
|
+
}
|
|
2932
|
+
if (command && typeof command === 'object' && 'name' in command) {
|
|
2933
|
+
return command.name;
|
|
2934
|
+
}
|
|
2935
|
+
return undefined;
|
|
2936
|
+
}
|
|
2937
|
+
parseWidgetCommand(cmd) {
|
|
2938
|
+
// Expected 'widget:<widgetName>.<action>'
|
|
2939
|
+
if (!cmd?.startsWith('widget:'))
|
|
2940
|
+
return {};
|
|
2941
|
+
const rest = cmd.slice('widget:'.length);
|
|
2942
|
+
const dot = rest.lastIndexOf('.');
|
|
2943
|
+
if (dot <= 0)
|
|
2944
|
+
return {};
|
|
2945
|
+
return { widgetName: rest.slice(0, dot), action: rest.slice(dot + 1) };
|
|
2946
|
+
}
|
|
2947
|
+
async invokeWidget(widgetName, apiMethod, opts) {
|
|
2948
|
+
if (!this.widgetCoreService)
|
|
2949
|
+
return;
|
|
2950
|
+
try {
|
|
2951
|
+
const widget = this.widgetCoreService.getWidget(widgetName);
|
|
2952
|
+
const api = widget?.api?.();
|
|
2953
|
+
const fn = api?.[apiMethod];
|
|
2954
|
+
if (typeof fn === 'function') {
|
|
2955
|
+
await Promise.resolve(fn({
|
|
2956
|
+
close: (result) => {
|
|
2957
|
+
void this.closeWithOptionalSkipValidate(result);
|
|
2958
|
+
},
|
|
2959
|
+
context: () => this.context(),
|
|
2960
|
+
setLoading: (loading) => {
|
|
2961
|
+
(opts.setLoading ?? ((v) => this.isDialogLoading.set(v)))(loading);
|
|
2962
|
+
},
|
|
2963
|
+
}));
|
|
2964
|
+
// Footer predicates (e.g. wizard step) must refresh when the widget advances outside executeAction (e.g. dialogRef.invokeWidget after entity-form continue).
|
|
2965
|
+
await this.aggregateAndEvaluateActions();
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
catch {
|
|
2969
|
+
//
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
async close(result, options) {
|
|
2973
|
+
if (options?.skipDirtyConfirm) {
|
|
2974
|
+
this.skipNextOnClosingDirtyCheck = true;
|
|
2975
|
+
this.pendingDismissResolvePayload = undefined;
|
|
2976
|
+
}
|
|
2977
|
+
if (result) {
|
|
2978
|
+
const isValid = await this.layoutRenderer()?.validate();
|
|
2979
|
+
if (isValid?.result) {
|
|
2980
|
+
this.result.emit(result);
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
super.close(result);
|
|
2984
|
+
}
|
|
2985
|
+
// --- Actions aggregation & evaluation ---
|
|
2986
|
+
async aggregateAndEvaluateActions() {
|
|
2987
|
+
const widgetActions = await this.collectWidgetActions();
|
|
2988
|
+
const dialogActions = this.collectDialogActionsFromConfig();
|
|
2989
|
+
const all = [...widgetActions, ...dialogActions];
|
|
2990
|
+
const evaluated = await this.evaluatePredicates(all);
|
|
2991
|
+
const visible = evaluated.filter((a) => a.hidden !== true);
|
|
2992
|
+
const prefix = visible.filter((a) => (a.zone ?? 'footer') === 'footer' && (a.placement ?? 'suffix') === 'prefix');
|
|
2993
|
+
const suffix = visible.filter((a) => (a.zone ?? 'footer') === 'footer' && (a.placement ?? 'suffix') === 'suffix');
|
|
2994
|
+
this.footerPrefix.set(prefix);
|
|
2995
|
+
this.footerSuffix.set(suffix);
|
|
2996
|
+
}
|
|
2997
|
+
async collectWidgetActions() {
|
|
2998
|
+
if (!this.widgetCoreService)
|
|
2999
|
+
return [];
|
|
3000
|
+
const names = this.widgetCoreService.listRegisteredWidgetNames();
|
|
3001
|
+
const actions = [];
|
|
3002
|
+
for (const name of names) {
|
|
3003
|
+
try {
|
|
3004
|
+
const widget = this.widgetCoreService.getWidget(name);
|
|
3005
|
+
const widgetActs = widget?.actions?.() ?? [];
|
|
3006
|
+
for (const action of widgetActs) {
|
|
3007
|
+
actions.push({
|
|
3008
|
+
...action,
|
|
3009
|
+
shortcuts: resolveConfiguredFooterActionShortcuts(this.resolveActionCommandName(action.command), action.shortcuts),
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
catch { }
|
|
3014
|
+
}
|
|
3015
|
+
return actions;
|
|
3016
|
+
}
|
|
3017
|
+
collectDialogActionsFromConfig() {
|
|
3018
|
+
const footer = this.config?.actions?.footer;
|
|
3019
|
+
const mapOne = (a, placement) => ({
|
|
3020
|
+
title: a.title,
|
|
3021
|
+
command: typeof a.command === 'string' ? a.command : a.command?.name,
|
|
3022
|
+
icon: a.icon,
|
|
3023
|
+
color: a.color,
|
|
3024
|
+
disabled: a.disabled,
|
|
3025
|
+
hidden: a.hidden,
|
|
3026
|
+
zone: 'footer',
|
|
3027
|
+
placement,
|
|
3028
|
+
scope: a.scope,
|
|
3029
|
+
predicateApiWidgetName: a.predicateApiWidgetName,
|
|
3030
|
+
shortcuts: resolveConfiguredFooterActionShortcuts(typeof a.command === 'string' ? a.command : a.command?.name, a.shortcuts),
|
|
3031
|
+
});
|
|
3032
|
+
const prefix = (footer?.prefix || []).map((a) => mapOne(a, 'prefix'));
|
|
3033
|
+
const suffix = (footer?.suffix || []).map((a) => mapOne(a, 'suffix'));
|
|
3034
|
+
return [...prefix, ...suffix];
|
|
3035
|
+
}
|
|
3036
|
+
async evaluatePredicates(actions) {
|
|
3037
|
+
const out = [];
|
|
3038
|
+
for (const a of actions) {
|
|
3039
|
+
const parsed = typeof a.command === 'string' ? this.parseWidgetCommand(a.command) : {};
|
|
3040
|
+
const widgetNameForApi = parsed.widgetName ?? a.predicateApiWidgetName;
|
|
3041
|
+
const api = widgetNameForApi ? await this.resolveApi(widgetNameForApi) : undefined;
|
|
3042
|
+
const scope = {
|
|
3043
|
+
api,
|
|
3044
|
+
widget: { name: widgetNameForApi },
|
|
3045
|
+
dialog: { context: this.context() },
|
|
3046
|
+
context: this.context(),
|
|
3047
|
+
};
|
|
3048
|
+
const disabled = await this.evalBool(a.disabled, scope);
|
|
3049
|
+
const hidden = await this.evalBool(a.hidden, scope);
|
|
3050
|
+
const resolvedTitle = (await this.evalActionTitle(a.title, scope)) ?? a.title;
|
|
3051
|
+
out.push({ ...a, disabled, hidden, title: resolvedTitle });
|
|
3052
|
+
}
|
|
3053
|
+
return out;
|
|
3054
|
+
}
|
|
3055
|
+
async evalBool(value, scope) {
|
|
3056
|
+
if (typeof value === 'boolean')
|
|
3057
|
+
return value;
|
|
3058
|
+
if (typeof value === 'string') {
|
|
3059
|
+
try {
|
|
3060
|
+
const result = await this.expressionEvaluator.evaluate(value, scope);
|
|
3061
|
+
return !!result;
|
|
3062
|
+
}
|
|
3063
|
+
catch {
|
|
3064
|
+
return false;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
return value;
|
|
3068
|
+
}
|
|
3069
|
+
/** Resolves footer action title when it contains {{ ... }} (e.g. wizard labels from dialog context). */
|
|
3070
|
+
async evalActionTitle(value, scope) {
|
|
3071
|
+
if (value == null || typeof value !== 'string' || !value.includes('{{')) {
|
|
3072
|
+
return value;
|
|
3073
|
+
}
|
|
3074
|
+
try {
|
|
3075
|
+
const result = await this.expressionEvaluator.evaluate(value, scope);
|
|
3076
|
+
if (typeof result === 'string' && result.length > 0) {
|
|
3077
|
+
return result;
|
|
3078
|
+
}
|
|
3079
|
+
if (result != null && result !== false) {
|
|
3080
|
+
return String(result);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
catch {
|
|
3084
|
+
//
|
|
3085
|
+
}
|
|
3086
|
+
return value;
|
|
3087
|
+
}
|
|
3088
|
+
async resolveApi(widgetName) {
|
|
3089
|
+
try {
|
|
3090
|
+
await this.widgetCoreService?.waitForWidget(widgetName, 2000);
|
|
3091
|
+
const widget = this.widgetCoreService?.getWidget(widgetName);
|
|
3092
|
+
return widget?.api?.();
|
|
3093
|
+
}
|
|
3094
|
+
catch {
|
|
3095
|
+
return undefined;
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDialogRendererComponent, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
3099
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AXPDialogRendererComponent, isStandalone: true, selector: "axp-dialog-renderer", outputs: { result: "result" }, providers: [AXPContextStore], viewQueries: [{ propertyName: "layoutRenderer", first: true, predicate: AXPLayoutRendererComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
|
|
3100
|
+
<axp-component-slot name="dialog-header" [context]="context()"></axp-component-slot>
|
|
3101
|
+
<div class="ax-p-4">
|
|
3102
|
+
<!-- @if (config.message) {
|
|
3103
|
+
<p class="ax-mb-4 ax-leading-relaxed">{{ config.message | translate | async }}</p>
|
|
3104
|
+
} -->
|
|
3105
|
+
<axp-layout-renderer
|
|
3106
|
+
[layout]="config.definition"
|
|
3107
|
+
[context]="context()"
|
|
3108
|
+
(contextChange)="handleContextChanged($event)"
|
|
3109
|
+
(contextInitiated)="handleContextInitiated($event)"
|
|
3110
|
+
>
|
|
3111
|
+
</axp-layout-renderer>
|
|
3112
|
+
</div>
|
|
3113
|
+
|
|
3114
|
+
<!-- Custom footer slot: if it has content, default footer is hidden -->
|
|
3115
|
+
<axp-component-slot name="dialog-footer" #footerSlot="slot" [context]="context()"></axp-component-slot>
|
|
3116
|
+
@if (footerSlot.isEmpty()) {
|
|
3117
|
+
<ax-footer>
|
|
3118
|
+
<ax-prefix>
|
|
3119
|
+
<axp-component-slot name="dialog-footer-prefix" [context]="context()"></axp-component-slot>
|
|
3120
|
+
@for (action of footerPrefixActions(); track $index) {
|
|
3121
|
+
<ax-button
|
|
3122
|
+
[disabled]="action.disabled || isFormLoading()"
|
|
3123
|
+
[text]="(action.title | translate | async)!"
|
|
3124
|
+
[look]="'outline'"
|
|
3125
|
+
[color]="action.color"
|
|
3126
|
+
(onClick)="executeAction(action)"
|
|
3127
|
+
>
|
|
3128
|
+
@if (isFormLoading()) {
|
|
3129
|
+
<ax-loading></ax-loading>
|
|
3130
|
+
}
|
|
3131
|
+
<ax-prefix>
|
|
3132
|
+
@if (action.icon) {
|
|
3133
|
+
<ax-icon [icon]="action.icon"></ax-icon>
|
|
3134
|
+
}
|
|
3135
|
+
</ax-prefix>
|
|
3136
|
+
</ax-button>
|
|
3137
|
+
}
|
|
3138
|
+
</ax-prefix>
|
|
3139
|
+
<ax-suffix>
|
|
3140
|
+
@for (action of footerSuffixActions(); track $index) {
|
|
3141
|
+
<ax-button
|
|
3142
|
+
[disabled]="action.disabled || isSubmitting()"
|
|
3143
|
+
[text]="(action.title | translate | async)!"
|
|
3144
|
+
[look]="'solid'"
|
|
3145
|
+
[color]="action.color"
|
|
3146
|
+
(onClick)="executeAction(action)"
|
|
3147
|
+
>
|
|
3148
|
+
@if (isFormLoading()) {
|
|
3149
|
+
<ax-loading></ax-loading>
|
|
3150
|
+
}
|
|
3151
|
+
@if (action.icon) {
|
|
3152
|
+
<ax-prefix>
|
|
3153
|
+
<ax-icon [icon]="action.icon"></ax-icon>
|
|
3154
|
+
</ax-prefix>
|
|
3155
|
+
}
|
|
3156
|
+
</ax-button>
|
|
3157
|
+
}
|
|
3158
|
+
<axp-component-slot name="dialog-footer-suffix" [context]="context()"></axp-component-slot>
|
|
3159
|
+
</ax-suffix>
|
|
3160
|
+
</ax-footer>
|
|
3161
|
+
}
|
|
3162
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: AXPLayoutRendererComponent, selector: "axp-layout-renderer", inputs: ["layout", "context", "look", "mode"], outputs: ["contextChange", "contextInitiated", "validityChange"] }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1$1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i2$1.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "component", type: i2$1.AXDecoratorGenericComponent, selector: "ax-footer, ax-header, ax-content, ax-divider, ax-form-hint, ax-prefix, ax-suffix, ax-text, ax-title, ax-subtitle, ax-placeholder, ax-overlay" }, { kind: "ngmodule", type: AXLoadingModule }, { kind: "component", type: i3.AXLoadingComponent, selector: "ax-loading", inputs: ["visible", "type", "context"], outputs: ["visibleChange"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "ngmodule", type: AXPComponentSlotModule }, { kind: "directive", type: i4.AXPComponentSlotDirective, selector: "axp-component-slot", inputs: ["name", "host", "context"], exportAs: ["slot"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i6.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3163
|
+
}
|
|
3164
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPDialogRendererComponent, decorators: [{
|
|
3165
|
+
type: Component,
|
|
3166
|
+
args: [{
|
|
3167
|
+
selector: 'axp-dialog-renderer',
|
|
3168
|
+
standalone: true,
|
|
3169
|
+
imports: [
|
|
3170
|
+
CommonModule,
|
|
3171
|
+
AXPLayoutRendererComponent,
|
|
3172
|
+
AXButtonModule,
|
|
3173
|
+
AXDecoratorModule,
|
|
3174
|
+
AXLoadingModule,
|
|
3175
|
+
AXTranslationModule,
|
|
3176
|
+
AXPComponentSlotModule,
|
|
3177
|
+
],
|
|
3178
|
+
providers: [AXPContextStore],
|
|
3179
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3180
|
+
template: `
|
|
3181
|
+
<axp-component-slot name="dialog-header" [context]="context()"></axp-component-slot>
|
|
3182
|
+
<div class="ax-p-4">
|
|
3183
|
+
<!-- @if (config.message) {
|
|
3184
|
+
<p class="ax-mb-4 ax-leading-relaxed">{{ config.message | translate | async }}</p>
|
|
3185
|
+
} -->
|
|
3186
|
+
<axp-layout-renderer
|
|
3187
|
+
[layout]="config.definition"
|
|
3188
|
+
[context]="context()"
|
|
3189
|
+
(contextChange)="handleContextChanged($event)"
|
|
3190
|
+
(contextInitiated)="handleContextInitiated($event)"
|
|
3191
|
+
>
|
|
3192
|
+
</axp-layout-renderer>
|
|
3193
|
+
</div>
|
|
3194
|
+
|
|
3195
|
+
<!-- Custom footer slot: if it has content, default footer is hidden -->
|
|
3196
|
+
<axp-component-slot name="dialog-footer" #footerSlot="slot" [context]="context()"></axp-component-slot>
|
|
3197
|
+
@if (footerSlot.isEmpty()) {
|
|
3198
|
+
<ax-footer>
|
|
3199
|
+
<ax-prefix>
|
|
3200
|
+
<axp-component-slot name="dialog-footer-prefix" [context]="context()"></axp-component-slot>
|
|
3201
|
+
@for (action of footerPrefixActions(); track $index) {
|
|
3202
|
+
<ax-button
|
|
3203
|
+
[disabled]="action.disabled || isFormLoading()"
|
|
3204
|
+
[text]="(action.title | translate | async)!"
|
|
3205
|
+
[look]="'outline'"
|
|
3206
|
+
[color]="action.color"
|
|
3207
|
+
(onClick)="executeAction(action)"
|
|
3208
|
+
>
|
|
3209
|
+
@if (isFormLoading()) {
|
|
3210
|
+
<ax-loading></ax-loading>
|
|
3211
|
+
}
|
|
3212
|
+
<ax-prefix>
|
|
3213
|
+
@if (action.icon) {
|
|
3214
|
+
<ax-icon [icon]="action.icon"></ax-icon>
|
|
3215
|
+
}
|
|
3216
|
+
</ax-prefix>
|
|
3217
|
+
</ax-button>
|
|
3218
|
+
}
|
|
3219
|
+
</ax-prefix>
|
|
3220
|
+
<ax-suffix>
|
|
3221
|
+
@for (action of footerSuffixActions(); track $index) {
|
|
3222
|
+
<ax-button
|
|
3223
|
+
[disabled]="action.disabled || isSubmitting()"
|
|
3224
|
+
[text]="(action.title | translate | async)!"
|
|
3225
|
+
[look]="'solid'"
|
|
3226
|
+
[color]="action.color"
|
|
3227
|
+
(onClick)="executeAction(action)"
|
|
3228
|
+
>
|
|
3229
|
+
@if (isFormLoading()) {
|
|
3230
|
+
<ax-loading></ax-loading>
|
|
3231
|
+
}
|
|
3232
|
+
@if (action.icon) {
|
|
3233
|
+
<ax-prefix>
|
|
3234
|
+
<ax-icon [icon]="action.icon"></ax-icon>
|
|
3235
|
+
</ax-prefix>
|
|
3236
|
+
}
|
|
3237
|
+
</ax-button>
|
|
3238
|
+
}
|
|
3239
|
+
<axp-component-slot name="dialog-footer-suffix" [context]="context()"></axp-component-slot>
|
|
3240
|
+
</ax-suffix>
|
|
3241
|
+
</ax-footer>
|
|
3242
|
+
}
|
|
3243
|
+
`,
|
|
3244
|
+
}]
|
|
3245
|
+
}], propDecorators: { result: [{
|
|
3246
|
+
type: Output
|
|
3247
|
+
}], layoutRenderer: [{ type: i0.ViewChild, args: [i0.forwardRef(() => AXPLayoutRendererComponent), { isSignal: true }] }] } });
|
|
3248
|
+
|
|
3249
|
+
var dialogRenderer_component = /*#__PURE__*/Object.freeze({
|
|
3250
|
+
__proto__: null,
|
|
3251
|
+
AXPDialogRendererComponent: AXPDialogRendererComponent
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
/**
|
|
3255
|
+
* `customWidget` only forwards keys from its options bag into the built node via `addSingleWidget`.
|
|
3256
|
+
* Designer / configurator persist `defaultValue` (and other extended fields) on the widget node root;
|
|
3257
|
+
* spreading `options` alone drops them, so preview never applied defaults.
|
|
3258
|
+
*/
|
|
3259
|
+
/**
|
|
3260
|
+
* Widget options are sometimes persisted with an extra nesting (`options.options`) when context
|
|
3261
|
+
* was merged incorrectly. Flatten so list/data-source resolution sees `dataSource` at the top level.
|
|
3262
|
+
*/
|
|
3263
|
+
function optionsBagForPreview(node) {
|
|
3264
|
+
const raw = (node.options ?? {});
|
|
3265
|
+
const inner = raw['options'];
|
|
3266
|
+
if (inner !== undefined && typeof inner === 'object' && !Array.isArray(inner)) {
|
|
3267
|
+
const { options: _nested, ...rest } = raw;
|
|
3268
|
+
return { ...rest, ...inner };
|
|
3269
|
+
}
|
|
3270
|
+
return { ...raw };
|
|
3271
|
+
}
|
|
3272
|
+
function extendedNodePropsForPreview(node) {
|
|
3273
|
+
const out = {};
|
|
3274
|
+
if (node.defaultValue !== undefined) {
|
|
3275
|
+
out['defaultValue'] = node.defaultValue;
|
|
3276
|
+
}
|
|
3277
|
+
if (node.triggers !== undefined) {
|
|
3278
|
+
out['triggers'] = node.triggers;
|
|
3279
|
+
}
|
|
3280
|
+
if (node.meta !== undefined) {
|
|
3281
|
+
out['meta'] = node.meta;
|
|
3282
|
+
}
|
|
3283
|
+
if (node.valueTransforms !== undefined) {
|
|
3284
|
+
out['valueTransforms'] = node.valueTransforms;
|
|
3285
|
+
}
|
|
3286
|
+
if (node.visible !== undefined) {
|
|
3287
|
+
out['visible'] = node.visible;
|
|
3288
|
+
}
|
|
3289
|
+
if (node.mode !== undefined) {
|
|
3290
|
+
out['mode'] = node.mode;
|
|
3291
|
+
}
|
|
3292
|
+
if (node.children !== undefined) {
|
|
3293
|
+
out['children'] = node.children;
|
|
3294
|
+
}
|
|
3295
|
+
return out;
|
|
3296
|
+
}
|
|
3297
|
+
//#endregion
|
|
3298
|
+
//#region ---- Command ----
|
|
3299
|
+
/**
|
|
3300
|
+
* Opens a dialog that previews a widget configuration (same behavior as the preview button on
|
|
3301
|
+
* `axp-widget-field-configurator`). Invoked from that component and from entity list actions.
|
|
3302
|
+
*/
|
|
3303
|
+
class AXPPreviewWidgetFieldCommand {
|
|
3304
|
+
constructor() {
|
|
3305
|
+
this.formBuilderService = inject(AXPLayoutBuilderService);
|
|
3306
|
+
this.widgetRegistry = inject(AXPWidgetRegistryService);
|
|
3307
|
+
this.translationService = inject(AXTranslationService);
|
|
3308
|
+
this.crudService = inject(AXP_ENTITY_DEFINITION_CRUD_SERVICE, { optional: true });
|
|
3309
|
+
}
|
|
3310
|
+
async execute(input) {
|
|
3311
|
+
try {
|
|
3312
|
+
const merged = this.mergeInvocation(input);
|
|
3313
|
+
const currentWidget = this.normalizeWidget(merged['widget'] ?? merged['interface']);
|
|
3314
|
+
if (!currentWidget?.type) {
|
|
3315
|
+
return {
|
|
3316
|
+
success: false,
|
|
3317
|
+
message: {
|
|
3318
|
+
text: (await this.translationService.translateAsync('@general:messages.invalid-data')) || 'Invalid data',
|
|
3319
|
+
},
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
const fieldName = String(merged['fieldName'] ?? merged['name'] ?? 'Field');
|
|
3323
|
+
const rawTitle = (merged['fieldTitle'] ?? merged['title']);
|
|
3324
|
+
const fieldTitleLabel = this.resolveFieldTitleLabel(rawTitle, fieldName);
|
|
3325
|
+
const dialogTitle = (await this.resolveWidgetDisplayTitle(currentWidget.type)) || currentWidget.type || fieldTitleLabel;
|
|
3326
|
+
const previewWidgetOptions = {
|
|
3327
|
+
...optionsBagForPreview(currentWidget),
|
|
3328
|
+
name: fieldName,
|
|
3329
|
+
...extendedNodePropsForPreview(currentWidget),
|
|
3330
|
+
};
|
|
3331
|
+
const dialogOutcome = await this.formBuilderService
|
|
3332
|
+
.create()
|
|
3333
|
+
.dialog((dialog) => {
|
|
3334
|
+
dialog
|
|
3335
|
+
.setTitle(dialogTitle)
|
|
3336
|
+
.setSize('md')
|
|
3337
|
+
.setCloseButton(true)
|
|
3338
|
+
.setContext({})
|
|
3339
|
+
.content((layoutBuilder) => {
|
|
3340
|
+
layoutBuilder.formField(fieldTitleLabel, (formField) => {
|
|
3341
|
+
formField.customWidget(currentWidget.type, previewWidgetOptions);
|
|
3342
|
+
});
|
|
3343
|
+
})
|
|
3344
|
+
.setActions((actions) => actions.cancel('@general:actions.close.title'));
|
|
3345
|
+
})
|
|
3346
|
+
.show();
|
|
3347
|
+
const cancelled = this.isCancelDialogOutcome(dialogOutcome);
|
|
3348
|
+
return {
|
|
3349
|
+
success: !cancelled,
|
|
3350
|
+
message: { text: '' },
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
catch (error) {
|
|
3354
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
3355
|
+
return {
|
|
3356
|
+
success: false,
|
|
3357
|
+
message: { text: message },
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
mergeInvocation(input) {
|
|
3362
|
+
const contextOptions = input.__context__?.options;
|
|
3363
|
+
const ctxData = input.__context__?.data;
|
|
3364
|
+
const { __context__: _ctx, ...rest } = input;
|
|
3365
|
+
return {
|
|
3366
|
+
...(ctxData ?? {}),
|
|
3367
|
+
...(contextOptions ?? {}),
|
|
3368
|
+
...rest,
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
normalizeWidget(raw) {
|
|
3372
|
+
if (raw == null)
|
|
3373
|
+
return null;
|
|
3374
|
+
if (typeof raw === 'string') {
|
|
3375
|
+
const t = raw.trim();
|
|
3376
|
+
return t ? { type: t, options: {} } : null;
|
|
3377
|
+
}
|
|
3378
|
+
if (typeof raw === 'object' && !Array.isArray(raw) && 'type' in raw) {
|
|
3379
|
+
const w = raw;
|
|
3380
|
+
return w.type ? cloneDeep(w) : null;
|
|
3381
|
+
}
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
resolveFieldTitleLabel(raw, fallback) {
|
|
3385
|
+
let source = fallback;
|
|
3386
|
+
if (raw !== undefined && raw !== null) {
|
|
3387
|
+
if (typeof raw === 'string') {
|
|
3388
|
+
if (raw.trim() !== '') {
|
|
3389
|
+
source = raw;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
else if (typeof raw === 'object' && !Array.isArray(raw) && Object.keys(raw).length > 0) {
|
|
3393
|
+
source = raw;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
return this.translationService.resolve(source);
|
|
3397
|
+
}
|
|
3398
|
+
isCancelDialogOutcome(outcome) {
|
|
3399
|
+
if (outcome == null) {
|
|
3400
|
+
return false;
|
|
3401
|
+
}
|
|
3402
|
+
const ref = outcome;
|
|
3403
|
+
if (typeof ref.action !== 'function') {
|
|
3404
|
+
return false;
|
|
3405
|
+
}
|
|
3406
|
+
return ref.action() === 'cancel';
|
|
3407
|
+
}
|
|
3408
|
+
async resolveWidgetDisplayTitle(widgetType) {
|
|
3409
|
+
const crud = this.crudService;
|
|
3410
|
+
if (crud) {
|
|
3411
|
+
const interfaces = await crud.listInterfaces();
|
|
3412
|
+
const iface = interfaces.find((d) => d.name === widgetType);
|
|
3413
|
+
return iface?.title ?? iface?.name;
|
|
3414
|
+
}
|
|
3415
|
+
const config = this.widgetRegistry.getOptional(widgetType);
|
|
3416
|
+
if (!config) {
|
|
3417
|
+
return undefined;
|
|
3418
|
+
}
|
|
3419
|
+
const resolved = this.translationService.resolve(config.title);
|
|
3420
|
+
return resolved || undefined;
|
|
3421
|
+
}
|
|
3422
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPPreviewWidgetFieldCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
3423
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPPreviewWidgetFieldCommand }); }
|
|
3424
|
+
}
|
|
3425
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXPPreviewWidgetFieldCommand, decorators: [{
|
|
3426
|
+
type: Injectable
|
|
3427
|
+
}] });
|
|
3428
|
+
|
|
3429
|
+
var previewWidgetField_command = /*#__PURE__*/Object.freeze({
|
|
3430
|
+
__proto__: null,
|
|
3431
|
+
AXPPreviewWidgetFieldCommand: AXPPreviewWidgetFieldCommand
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
/**
|
|
3435
|
+
* Generated bundle index. Do not edit.
|
|
3436
|
+
*/
|
|
3437
|
+
|
|
3438
|
+
export { AXPDialogRendererComponent, AXPLayoutBuilderService, AXPLayoutConversionService, AXPLayoutRendererComponent, AXPPreviewWidgetFieldCommand, AXP_LAYOUT_BUILDER_DIALOG_BEFORE_OPEN_HOOK_KEY, AXP_LAYOUT_BUILDER_DIALOG_CONFIG_HOOK_KEY, AXP_LAYOUT_BUILDER_DIALOG_CONTEXT_CHANGED_HOOK_KEY, LayoutBuilderModule, createDismissedDialogRef };
|
|
3439
|
+
//# sourceMappingURL=acorex-platform-framework-client-layout-builder.mjs.map
|