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