@siliconst/metaui 1.4.44 → 1.4.51
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/package.json +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost-dbg.js +24 -0
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.d.ts +24 -0
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.d.ts.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.js +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.ts +15 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost-dbg.js +34 -0
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.d.ts +34 -0
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.d.ts.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.js +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.ts +25 -1
- package/src/nz/co/siliconst/ui5/metaui/core/Engine-dbg.js +31 -1
- package/src/nz/co/siliconst/ui5/metaui/core/Engine-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/core/Engine.d.ts +2 -0
- package/src/nz/co/siliconst/ui5/metaui/core/Engine.d.ts.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/core/Engine.js +1 -1
- package/src/nz/co/siliconst/ui5/metaui/core/Engine.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/core/Engine.ts +38 -0
- package/src/nz/co/siliconst/ui5/metaui/core/StateManager-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/core/StateManager.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/core/StateManager.ts +1 -1
- package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.d.ts +1 -0
- package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.d.ts.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.ts +1 -0
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout-dbg.js +327 -67
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.d.ts +57 -15
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.d.ts.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.js +1 -1
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.ts +375 -65
- package/src/nz/co/siliconst/ui5/metaui/library-preload.js +5 -5
- package/src/nz/co/siliconst/ui5/metaui/library-preload.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin-dbg.js +10 -15
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin-dbg.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.d.ts +4 -0
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.d.ts.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.js +1 -1
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.js.map +1 -1
- package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.ts +10 -15
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file WizardLayout.ts
|
|
3
|
-
* @description
|
|
3
|
+
* @description MetaUI custom step-by-step wizard navigator.
|
|
4
|
+
*
|
|
5
|
+
* Architecture Decision:
|
|
6
|
+
* SAP's sap.m.Wizard in renderMode "Page" requires a NavContainer ancestor in the DOM,
|
|
7
|
+
* which MetaUI's GeneratorHost VBox does not provide. Using sap.m.Wizard in the default
|
|
8
|
+
* "Scroll" mode renders ALL steps simultaneously, which is not desired behavior.
|
|
9
|
+
*
|
|
10
|
+
* Solution: Implement a fully custom step navigator using plain VBox/HBox/Button controls.
|
|
11
|
+
* This gives MetaUI complete control over step visibility, per-step field validation,
|
|
12
|
+
* and the beforeLayoutSectionChange async event API — with no dependency on the broken
|
|
13
|
+
* Wizard container API.
|
|
4
14
|
*/
|
|
5
15
|
|
|
6
|
-
import Wizard from "sap/m/Wizard";
|
|
7
|
-
import WizardStep from "sap/m/WizardStep";
|
|
8
16
|
import SimpleForm from "sap/ui/layout/form/SimpleForm";
|
|
9
17
|
import Label from "sap/m/Label";
|
|
10
18
|
import VBox from "sap/m/VBox";
|
|
19
|
+
import HBox from "sap/m/HBox";
|
|
20
|
+
import Button from "sap/m/Button";
|
|
21
|
+
import ProgressIndicator from "sap/m/ProgressIndicator";
|
|
22
|
+
import MTitle from "sap/m/Title"; // sap.m.Title IS a Control → safe inside HBox.items
|
|
23
|
+
import CoreTitle from "sap/ui/core/Title"; // sap.ui.core.Title is an Element → SimpleForm section headers ONLY
|
|
24
|
+
import Text from "sap/m/Text";
|
|
11
25
|
import { IPropertyMetadata, ISchema, ILayoutElement } from "../interfaces/ISchema";
|
|
26
|
+
import { IPlugin } from "../interfaces/IPlugin";
|
|
12
27
|
import { ILayoutManager } from "../interfaces/ILayoutManager";
|
|
13
28
|
import { Engine } from "../core/Engine";
|
|
14
29
|
import { SchemaNormalizer } from "../core/SchemaNormalizer";
|
|
@@ -16,89 +31,377 @@ import { Logger } from "../utils/Logger";
|
|
|
16
31
|
import Control from "sap/ui/core/Control";
|
|
17
32
|
|
|
18
33
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
* Minimal structural contract for accessing the hosting GeneratorHost without importing
|
|
35
|
+
* it directly (which would create a circular dependency between WizardLayout ↔ GeneratorHost).
|
|
36
|
+
*/
|
|
37
|
+
interface IWizardCapableHost {
|
|
38
|
+
/** Returns the active StateManager. Used to extract a payload snapshot at step-change time. */
|
|
39
|
+
getStateManager(): { extractPayload(): Record<string, unknown> } | null;
|
|
40
|
+
/** Fires a UI5 event upward on the host control. */
|
|
41
|
+
fireEvent(eventName: string, params: Record<string, unknown>): void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Layout Manager that generates a custom paginated step navigator.
|
|
46
|
+
* Renders exactly one step at a time, validates current-step fields before advancing,
|
|
47
|
+
* and fires `beforeLayoutSectionChange` with a full async API before each step transition.
|
|
48
|
+
*
|
|
23
49
|
* @namespace nz.co.siliconst.ui5.metaui.layouts
|
|
24
50
|
* @public
|
|
25
51
|
*/
|
|
26
52
|
export class WizardLayout implements ILayoutManager {
|
|
27
53
|
/**
|
|
28
|
-
* Renders the
|
|
29
|
-
*
|
|
30
|
-
* @param schema The active JSON schema object.
|
|
31
|
-
* @param modelName The bound UI5 model name.
|
|
32
|
-
* @param engine The central orchestrator for
|
|
33
|
-
* @param onSubmit Hook executed when the
|
|
34
|
-
* @returns {Control}
|
|
54
|
+
* Renders the wizard as a custom step-by-step navigator.
|
|
55
|
+
*
|
|
56
|
+
* @param schema The active JSON schema object containing `uiLayout` step groups.
|
|
57
|
+
* @param modelName The bound UI5 model name for data bindings.
|
|
58
|
+
* @param engine The central Engine orchestrator for field generation and validation.
|
|
59
|
+
* @param onSubmit Hook executed when the user completes the final step successfully.
|
|
60
|
+
* @returns {Control} A VBox root container comprising the step header, content, and navigation.
|
|
35
61
|
*/
|
|
36
62
|
public render(schema: ISchema, modelName: string, engine: Engine, onSubmit?: () => void): Control {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
if (!schema.uiLayout || !Array.isArray(schema.uiLayout)) {
|
|
64
|
+
Logger.warn("[MetaUI WizardLayout] Missing 'uiLayout' array in schema. Cannot build step navigator.", "", "WizardLayout");
|
|
65
|
+
return new VBox();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Filter valid step-level elements. Both "Group" and "WizardStep" act as step containers.
|
|
69
|
+
const stepElements = schema.uiLayout.filter(
|
|
70
|
+
(e: ILayoutElement) => e.type === "Group" || e.type === "WizardStep"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (stepElements.length === 0) {
|
|
74
|
+
Logger.warn("[MetaUI WizardLayout] No Group or WizardStep elements found in uiLayout.", "", "WizardLayout");
|
|
75
|
+
return new VBox();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const totalSteps = stepElements.length;
|
|
79
|
+
|
|
80
|
+
// --- Phase 1: Build all step forms and track field binding paths per step ---
|
|
81
|
+
// Each step form is built eagerly so that plugins are registered in the Engine
|
|
82
|
+
// and are available for validation via engine.getPluginByPath() on any step.
|
|
83
|
+
const stepForms: SimpleForm[] = [];
|
|
84
|
+
// stepFieldPaths[i] = all binding paths registered when building step i's fields.
|
|
85
|
+
// Used by _validateStep to run targeted per-step validation.
|
|
86
|
+
const stepFieldPaths: string[][] = [];
|
|
87
|
+
|
|
88
|
+
stepElements.forEach((element: ILayoutElement) => {
|
|
89
|
+
const fieldPaths: string[] = [];
|
|
90
|
+
const form = this._buildStepForm(element, schema, modelName, engine, fieldPaths);
|
|
91
|
+
stepForms.push(form);
|
|
92
|
+
stepFieldPaths.push(fieldPaths);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- Phase 2: Build the navigator UI ---
|
|
96
|
+
// Navigation state. Lives in closure; mutated by navigateTo() on each step transition.
|
|
97
|
+
let currentStepIndex = 0;
|
|
98
|
+
|
|
99
|
+
// Header: step title (sap.m.Title — a proper Control) + step counter label + progress bar
|
|
100
|
+
const stepTitleControl = new MTitle({ level: "H3" });
|
|
101
|
+
const stepCounterLabel = new Text();
|
|
102
|
+
const progressBar = new ProgressIndicator({
|
|
103
|
+
showValue: false,
|
|
104
|
+
state: "None",
|
|
105
|
+
height: "0.5rem"
|
|
106
|
+
}).addStyleClass("metaUIWizardProgress");
|
|
107
|
+
|
|
108
|
+
// Content: single-child VBox that swaps its contents to the current step form
|
|
109
|
+
const contentArea = new VBox({ width: "100%" }).addStyleClass("metaUIWizardContent");
|
|
110
|
+
|
|
111
|
+
// Navigation: Previous (left) / Next|Complete (right)
|
|
112
|
+
const prevButton = new Button({
|
|
113
|
+
text: "Previous",
|
|
114
|
+
icon: "sap-icon://navigation-left-arrow",
|
|
115
|
+
enabled: false,
|
|
116
|
+
press: () => navigateTo(currentStepIndex - 1)
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const nextButton = new Button({
|
|
120
|
+
type: "Emphasized",
|
|
121
|
+
text: totalSteps === 1 ? "Complete" : "Next Step",
|
|
122
|
+
icon: "sap-icon://navigation-right-arrow",
|
|
123
|
+
iconFirst: false,
|
|
124
|
+
press: () => handleNextPress()
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// --- Phase 3: Navigation logic ---
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Physically transitions the navigator to the given step index.
|
|
131
|
+
* Updates the header, content, and navigation button states.
|
|
132
|
+
*
|
|
133
|
+
* @param index The target step index (0-based).
|
|
134
|
+
*/
|
|
135
|
+
const navigateTo = (index: number): void => {
|
|
136
|
+
if (index < 0 || index >= totalSteps) {
|
|
137
|
+
Logger.error("[MetaUI WizardLayout]", `Invalid navigation target: step ${index}`, "WizardLayout");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
currentStepIndex = index;
|
|
142
|
+
const element = stepElements[currentStepIndex];
|
|
143
|
+
const isLastStep = currentStepIndex === totalSteps - 1;
|
|
144
|
+
|
|
145
|
+
// Update step header
|
|
146
|
+
stepTitleControl.setText(element.label || `Step ${currentStepIndex + 1}`);
|
|
147
|
+
stepCounterLabel.setText(`Step ${currentStepIndex + 1} of ${totalSteps}`);
|
|
148
|
+
progressBar.setPercentValue(((currentStepIndex + 1) / totalSteps) * 100);
|
|
149
|
+
|
|
150
|
+
// Swap the content area to show only the current step's form
|
|
151
|
+
contentArea.removeAllItems();
|
|
152
|
+
contentArea.addItem(stepForms[currentStepIndex]);
|
|
153
|
+
|
|
154
|
+
// Update navigation controls
|
|
155
|
+
prevButton.setEnabled(currentStepIndex > 0);
|
|
156
|
+
nextButton.setText(isLastStep ? "Complete" : "Next Step");
|
|
157
|
+
nextButton.setIcon(isLastStep ? "sap-icon://accept" : "sap-icon://navigation-right-arrow");
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Called when the user presses the Next/Complete button.
|
|
162
|
+
* Executes the following pipeline in sequence:
|
|
163
|
+
* 1. Per-step field validation (synchronous, local).
|
|
164
|
+
* 2. If last step → fires onSubmit.
|
|
165
|
+
* 3. Otherwise → fires `beforeLayoutSectionChange` with full async API.
|
|
166
|
+
* 4. If not prevented → calls navigateTo(next).
|
|
167
|
+
*/
|
|
168
|
+
const handleNextPress = (): void => {
|
|
169
|
+
// Step 1: Validate only the current step's fields.
|
|
170
|
+
// This gives immediate visual feedback (red borders) on errors before any event fires.
|
|
171
|
+
if (!this._validateStep(stepFieldPaths[currentStepIndex], engine)) {
|
|
172
|
+
Logger.debug("[MetaUI WizardLayout]", `Step ${currentStepIndex} failed local validation. Blocking navigation.`, "WizardLayout");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Step 2: If this is the final step, treat Next as Complete.
|
|
177
|
+
if (currentStepIndex === totalSteps - 1) {
|
|
41
178
|
if (onSubmit) onSubmit();
|
|
179
|
+
return;
|
|
42
180
|
}
|
|
43
|
-
});
|
|
44
181
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return wizard;
|
|
48
|
-
}
|
|
182
|
+
// Step 3: Fire beforeLayoutSectionChange to give the consumer a pre-navigation hook.
|
|
183
|
+
const host = engine.host as IWizardCapableHost | null;
|
|
49
184
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
185
|
+
if (!host) {
|
|
186
|
+
// No upstream host to notify — advance directly.
|
|
187
|
+
navigateTo(currentStepIndex + 1);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
56
190
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
191
|
+
const sm = host.getStateManager();
|
|
192
|
+
const payload: Record<string, unknown> = sm ? sm.extractPayload() : {};
|
|
193
|
+
|
|
194
|
+
// Guard: prevents navigateTo() from being called more than once
|
|
195
|
+
// if both preventDefault + resumeNavigation are called by a consumer.
|
|
196
|
+
let isPrevented = false;
|
|
197
|
+
let isNavigated = false;
|
|
198
|
+
|
|
199
|
+
const advance = (): void => {
|
|
200
|
+
if (!isNavigated) {
|
|
201
|
+
isNavigated = true;
|
|
202
|
+
navigateTo(currentStepIndex + 1);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
host.fireEvent("beforeLayoutSectionChange", {
|
|
207
|
+
/**
|
|
208
|
+
* The 0-based index of the step the user is navigating AWAY FROM.
|
|
209
|
+
*/
|
|
210
|
+
stepIndex: currentStepIndex,
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A live snapshot of the full form payload at the moment of the transition.
|
|
214
|
+
*/
|
|
215
|
+
payload,
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Blocks navigation synchronously. Call this at the start of an async check to
|
|
219
|
+
* prevent the wizard from advancing while waiting for a backend response.
|
|
220
|
+
* Call resumeNavigation() after the check passes to allow the advance.
|
|
221
|
+
*/
|
|
222
|
+
preventDefault: () => {
|
|
223
|
+
isPrevented = true;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Blocks navigation AND paints a specific field with an error state.
|
|
228
|
+
* @param fieldPath The full binding path of the field to mark invalid.
|
|
229
|
+
* @param errorMessage The error message to display on the field.
|
|
230
|
+
*/
|
|
231
|
+
addError: (fieldPath: string, errorMessage: string) => {
|
|
232
|
+
isPrevented = true;
|
|
233
|
+
const plugin: IPlugin | undefined = engine.getPluginByPath(fieldPath);
|
|
234
|
+
if (plugin && typeof plugin.setVisualValidationState === "function") {
|
|
235
|
+
plugin.setVisualValidationState(false, errorMessage);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Explicitly allows navigation after a prior `preventDefault()` call.
|
|
241
|
+
* Intended for async consumer patterns (e.g., backend validation Promises).
|
|
242
|
+
* Calling this causes the wizard to advance to the next step immediately.
|
|
243
|
+
*/
|
|
244
|
+
resumeNavigation: () => {
|
|
245
|
+
advance();
|
|
71
246
|
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Step 4: If the consumer did not call preventDefault(), advance immediately.
|
|
250
|
+
if (!isPrevented) {
|
|
251
|
+
advance();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Initialize: show Step 0 with all state set correctly.
|
|
256
|
+
navigateTo(0);
|
|
257
|
+
|
|
258
|
+
// --- Phase 4: Assemble the root container ---
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
const navBar = new HBox({
|
|
261
|
+
width: "100%",
|
|
262
|
+
justifyContent: "SpaceBetween",
|
|
263
|
+
alignItems: "Center",
|
|
264
|
+
items: [prevButton, nextButton]
|
|
265
|
+
} as any).addStyleClass("sapUiSmallMarginTop");
|
|
266
|
+
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
268
|
+
const header = new VBox({
|
|
269
|
+
width: "100%",
|
|
270
|
+
items: [
|
|
271
|
+
new HBox({
|
|
272
|
+
justifyContent: "SpaceBetween",
|
|
273
|
+
alignItems: "Center",
|
|
274
|
+
width: "100%",
|
|
275
|
+
items: [stepTitleControl, stepCounterLabel]
|
|
276
|
+
} as any),
|
|
277
|
+
progressBar
|
|
278
|
+
]
|
|
279
|
+
}).addStyleClass("sapUiSmallMarginBottom");
|
|
280
|
+
|
|
281
|
+
return new VBox({
|
|
282
|
+
width: "100%",
|
|
283
|
+
items: [header, contentArea, navBar]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validates all fields registered during a specific step's form build.
|
|
289
|
+
*
|
|
290
|
+
* Routes error painting through `engine.reportSchemaError()` → `resolveVisualState()`
|
|
291
|
+
* → `plugin.setVisualValidationState()`. This is the single authoritative path for
|
|
292
|
+
* painting red borders in MetaUI, and correctly handles both default and
|
|
293
|
+
* `useMessageManager` modes.
|
|
294
|
+
*
|
|
295
|
+
* @param fieldPaths The list of binding paths collected during the step's form build.
|
|
296
|
+
* @param engine The Engine instance that holds the plugin registry and validation ledger.
|
|
297
|
+
* @returns {boolean} True if all fields are valid, false if any failed.
|
|
298
|
+
*/
|
|
299
|
+
private _validateStep(fieldPaths: string[], engine: Engine): boolean {
|
|
300
|
+
let allValid = true;
|
|
72
301
|
|
|
73
|
-
|
|
74
|
-
|
|
302
|
+
for (const path of fieldPaths) {
|
|
303
|
+
const plugin = engine.getPluginByPath(path);
|
|
304
|
+
if (!plugin) continue;
|
|
305
|
+
|
|
306
|
+
const result = plugin.validate();
|
|
307
|
+
|
|
308
|
+
if (!result.isValid) {
|
|
309
|
+
// Route through the Engine's validation ledger.
|
|
310
|
+
// reportSchemaError → resolveVisualState → plugin.setVisualValidationState
|
|
311
|
+
// This is the same path used by triggerSubmit and the PBRE.
|
|
312
|
+
engine.reportSchemaError(path, result.errorMessage || "This field is required.");
|
|
313
|
+
allValid = false;
|
|
75
314
|
} else {
|
|
76
|
-
|
|
315
|
+
// Clearing null removes the error from the ledger and resets the visual state.
|
|
316
|
+
engine.reportSchemaError(path, null);
|
|
77
317
|
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return allValid;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Constructs a SimpleForm for a single wizard step, recursively rendering all
|
|
325
|
+
* child layout elements and collecting their binding paths into `fieldPaths`.
|
|
326
|
+
*
|
|
327
|
+
* @param stepElement The top-level Group/WizardStep layout element for this step.
|
|
328
|
+
* @param schema The active JSON schema.
|
|
329
|
+
* @param modelName The UI5 JSONModel alias for data binding.
|
|
330
|
+
* @param engine The Engine instance for field generation.
|
|
331
|
+
* @param fieldPaths Collector array — all binding paths generated for this step are pushed here.
|
|
332
|
+
* @returns {SimpleForm} The assembled SimpleForm for this wizard step.
|
|
333
|
+
*/
|
|
334
|
+
private _buildStepForm(
|
|
335
|
+
stepElement: ILayoutElement,
|
|
336
|
+
schema: ISchema,
|
|
337
|
+
modelName: string,
|
|
338
|
+
engine: Engine,
|
|
339
|
+
fieldPaths: string[]
|
|
340
|
+
): SimpleForm {
|
|
341
|
+
const form = new SimpleForm({
|
|
342
|
+
editable: true,
|
|
343
|
+
layout: "ResponsiveGridLayout",
|
|
344
|
+
labelSpanXL: 4, labelSpanL: 4, labelSpanM: 4, labelSpanS: 12,
|
|
345
|
+
adjustLabelSpan: false,
|
|
346
|
+
emptySpanXL: 0, emptySpanL: 0, emptySpanM: 0, emptySpanS: 0,
|
|
347
|
+
columnsXL: 1, columnsL: 1, columnsM: 1,
|
|
348
|
+
singleContainerFullSize: false
|
|
78
349
|
});
|
|
79
350
|
|
|
80
|
-
|
|
351
|
+
if (stepElement.elements && Array.isArray(stepElement.elements)) {
|
|
352
|
+
stepElement.elements.forEach((childElement: ILayoutElement) => {
|
|
353
|
+
this._renderElementInForm(form, childElement, schema, modelName, engine, fieldPaths);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return form;
|
|
81
358
|
}
|
|
82
359
|
|
|
83
360
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @param
|
|
361
|
+
* Recursively renders a single layout element into a SimpleForm.
|
|
362
|
+
* For `Group` elements, adds a Title and recurses into children.
|
|
363
|
+
* For `Control` elements, resolves the schema scope, generates the plugin control,
|
|
364
|
+
* and pushes the binding path to `fieldPaths` for per-step validation tracking.
|
|
365
|
+
*
|
|
366
|
+
* @param form The target SimpleForm to append content to.
|
|
367
|
+
* @param element The layout element to render.
|
|
368
|
+
* @param schema The active JSON schema.
|
|
369
|
+
* @param modelName The UI5 JSONModel alias for data binding.
|
|
370
|
+
* @param engine The Engine instance for field generation.
|
|
371
|
+
* @param fieldPaths Collector array for binding path tracking.
|
|
90
372
|
*/
|
|
91
|
-
private _renderElementInForm(
|
|
373
|
+
private _renderElementInForm(
|
|
374
|
+
form: SimpleForm,
|
|
375
|
+
element: ILayoutElement,
|
|
376
|
+
schema: ISchema,
|
|
377
|
+
modelName: string,
|
|
378
|
+
engine: Engine,
|
|
379
|
+
fieldPaths: string[]
|
|
380
|
+
): void {
|
|
381
|
+
if (element.type === "Group") {
|
|
382
|
+
if (element.label) {
|
|
383
|
+
// sap.ui.core.Title (CoreTitle) is an Element, not a Control.
|
|
384
|
+
// It is ONLY valid inside a SimpleForm's content aggregation as a section header.
|
|
385
|
+
form.addContent(new CoreTitle({ text: element.label }));
|
|
386
|
+
}
|
|
387
|
+
if (element.elements && Array.isArray(element.elements)) {
|
|
388
|
+
element.elements.forEach((childElement: ILayoutElement) => {
|
|
389
|
+
this._renderElementInForm(form, childElement, schema, modelName, engine, fieldPaths);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
92
395
|
if (element.type === "Control") {
|
|
93
396
|
if (!element.scope || !element.scope.startsWith("#/properties/")) {
|
|
94
|
-
Logger.error(`[MetaUI] Invalid scope '${element.scope}'
|
|
397
|
+
Logger.error(`[MetaUI WizardLayout] Invalid scope '${element.scope}' — must start with '#/properties/'.`, "", "WizardLayout");
|
|
95
398
|
return;
|
|
96
399
|
}
|
|
97
|
-
|
|
400
|
+
|
|
98
401
|
const { meta, bindingPath, propKey } = SchemaNormalizer.resolveScope(schema, element.scope);
|
|
99
|
-
|
|
402
|
+
|
|
100
403
|
if (!meta) {
|
|
101
|
-
Logger.error(`[MetaUI] Property '${propKey}' not found in schema
|
|
404
|
+
Logger.error(`[MetaUI WizardLayout] Property '${propKey}' not found in schema.`, "", "WizardLayout");
|
|
102
405
|
return;
|
|
103
406
|
}
|
|
104
407
|
|
|
@@ -106,15 +409,21 @@ export class WizardLayout implements ILayoutManager {
|
|
|
106
409
|
const labelText = element.label || meta.ui?.label || propKey;
|
|
107
410
|
const label = new Label({
|
|
108
411
|
text: labelText,
|
|
109
|
-
|
|
412
|
+
// meta.required may be boolean or string[] at schema object level;
|
|
413
|
+
// Label.required only accepts boolean — normalize it.
|
|
414
|
+
required: meta.required === true
|
|
110
415
|
});
|
|
111
416
|
|
|
112
|
-
const effectiveMeta = { ...meta };
|
|
417
|
+
const effectiveMeta: IPropertyMetadata = { ...meta };
|
|
113
418
|
if (element.widget) {
|
|
114
|
-
effectiveMeta.ui = { ...meta.ui, widget: element.widget };
|
|
419
|
+
effectiveMeta.ui = { ...(meta.ui || {}), widget: element.widget };
|
|
115
420
|
}
|
|
116
421
|
|
|
117
|
-
const
|
|
422
|
+
const absolutePath = `/${bindingPath}`;
|
|
423
|
+
const control = engine.generateField(effectiveMeta, absolutePath, modelName);
|
|
424
|
+
|
|
425
|
+
// Track this path so _validateStep can target exactly the fields built for this step.
|
|
426
|
+
fieldPaths.push(absolutePath);
|
|
118
427
|
|
|
119
428
|
if (effectiveMeta.ui?.fullWidth) {
|
|
120
429
|
sap.ui.require(["sap/ui/layout/GridData"], (GridData: typeof import("sap/ui/layout/GridData").default) => {
|
|
@@ -125,11 +434,12 @@ export class WizardLayout implements ILayoutManager {
|
|
|
125
434
|
form.addContent(label);
|
|
126
435
|
form.addContent(control);
|
|
127
436
|
} catch (error) {
|
|
128
|
-
Logger.error(`[MetaUI] Failed to render
|
|
437
|
+
Logger.error(`[MetaUI WizardLayout] Failed to render field '${propKey}'`, (error as Error).message, "WizardLayout");
|
|
129
438
|
form.addContent(new Label({ text: propKey }));
|
|
130
439
|
}
|
|
131
|
-
|
|
132
|
-
Logger.warn(`[MetaUI] Nested groups within Wizard steps are not fully supported in SimpleForm yet.`, "", "WizardLayout");
|
|
440
|
+
return;
|
|
133
441
|
}
|
|
442
|
+
|
|
443
|
+
Logger.warn(`[MetaUI WizardLayout] Unsupported element type '${element.type}' inside a wizard step.`, "", "WizardLayout");
|
|
134
444
|
}
|
|
135
445
|
}
|