@siliconst/metaui 1.4.37 → 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.
Files changed (62) hide show
  1. package/package.json +1 -1
  2. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost-dbg.js +24 -0
  3. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost-dbg.js.map +1 -1
  4. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.d.ts +24 -0
  5. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.d.ts.map +1 -1
  6. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.js +1 -1
  7. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.js.map +1 -1
  8. package/src/nz/co/siliconst/ui5/metaui/controls/DynamicHost.ts +15 -1
  9. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost-dbg.js +34 -0
  10. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost-dbg.js.map +1 -1
  11. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.d.ts +34 -0
  12. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.d.ts.map +1 -1
  13. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.js +1 -1
  14. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.js.map +1 -1
  15. package/src/nz/co/siliconst/ui5/metaui/controls/host/GeneratorHost.ts +25 -1
  16. package/src/nz/co/siliconst/ui5/metaui/core/Engine-dbg.js +31 -1
  17. package/src/nz/co/siliconst/ui5/metaui/core/Engine-dbg.js.map +1 -1
  18. package/src/nz/co/siliconst/ui5/metaui/core/Engine.d.ts +2 -0
  19. package/src/nz/co/siliconst/ui5/metaui/core/Engine.d.ts.map +1 -1
  20. package/src/nz/co/siliconst/ui5/metaui/core/Engine.js +1 -1
  21. package/src/nz/co/siliconst/ui5/metaui/core/Engine.js.map +1 -1
  22. package/src/nz/co/siliconst/ui5/metaui/core/Engine.ts +38 -0
  23. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine-dbg.js +15 -96
  24. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine-dbg.js.map +1 -1
  25. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine.d.ts +1 -6
  26. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine.d.ts.map +1 -1
  27. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine.js +1 -1
  28. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine.js.map +1 -1
  29. package/src/nz/co/siliconst/ui5/metaui/core/PolicyEngine.ts +20 -100
  30. package/src/nz/co/siliconst/ui5/metaui/core/StateManager-dbg.js.map +1 -1
  31. package/src/nz/co/siliconst/ui5/metaui/core/StateManager.js.map +1 -1
  32. package/src/nz/co/siliconst/ui5/metaui/core/StateManager.ts +1 -1
  33. package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin-dbg.js.map +1 -1
  34. package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.d.ts +1 -0
  35. package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.d.ts.map +1 -1
  36. package/src/nz/co/siliconst/ui5/metaui/interfaces/IPlugin.ts +1 -0
  37. package/src/nz/co/siliconst/ui5/metaui/interfaces/ISchema-dbg.js.map +1 -1
  38. package/src/nz/co/siliconst/ui5/metaui/interfaces/ISchema.d.ts +2 -1
  39. package/src/nz/co/siliconst/ui5/metaui/interfaces/ISchema.d.ts.map +1 -1
  40. package/src/nz/co/siliconst/ui5/metaui/interfaces/ISchema.ts +2 -1
  41. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout-dbg.js +327 -67
  42. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout-dbg.js.map +1 -1
  43. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.d.ts +57 -15
  44. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.d.ts.map +1 -1
  45. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.js +1 -1
  46. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.js.map +1 -1
  47. package/src/nz/co/siliconst/ui5/metaui/layouts/WizardLayout.ts +375 -65
  48. package/src/nz/co/siliconst/ui5/metaui/library-preload.js +7 -7
  49. package/src/nz/co/siliconst/ui5/metaui/library-preload.js.map +1 -1
  50. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin-dbg.js +10 -15
  51. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin-dbg.js.map +1 -1
  52. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.d.ts +4 -0
  53. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.d.ts.map +1 -1
  54. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.js +1 -1
  55. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.js.map +1 -1
  56. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/BasePlugin.ts +10 -15
  57. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/StepInputPlugin-dbg.js +11 -1
  58. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/StepInputPlugin-dbg.js.map +1 -1
  59. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/StepInputPlugin.d.ts.map +1 -1
  60. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/StepInputPlugin.js +1 -1
  61. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/StepInputPlugin.js.map +1 -1
  62. package/src/nz/co/siliconst/ui5/metaui/plugins/controls/StepInputPlugin.ts +10 -1
@@ -1,14 +1,29 @@
1
1
  /**
2
2
  * @file WizardLayout.ts
3
- * @description Generates a fully responsive SAP Fiori Wizard layout.
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
- * Layout Manager responsible for generating a responsive SAP Fiori Wizard.
20
- * It segments the schema fields into discrete Wizard Steps based on `uiLayout` elements
21
- * of type `Group` or `WizardStep`.
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 JSON Schema properties into a paginated UI5 Wizard using uiLayout.
29
- *
30
- * @param schema The active JSON schema object.
31
- * @param modelName The bound UI5 model name.
32
- * @param engine The central orchestrator for delegating plugin generation.
33
- * @param onSubmit Hook executed when the Wizard's native 'complete' event is fired.
34
- * @returns {Control} The generated sap.m.Wizard 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
- const wizard = new Wizard({
38
- renderMode: "Page",
39
- showNextButton: true,
40
- complete: function() {
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
- if (!schema.uiLayout || !Array.isArray(schema.uiLayout)) {
46
- Logger.warn("[MetaUI] Missing 'uiLayout' array in wizard schema. Wizard will not render any steps.");
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
- schema.uiLayout.forEach((element: ILayoutElement) => {
51
- if (element.type === "Group" || element.type === "WizardStep") {
52
- const step = new WizardStep({
53
- title: element.label || "Step",
54
- validated: true // The PipelineManager handles global validation, wizard step validation can be local later
55
- });
185
+ if (!host) {
186
+ // No upstream host to notify — advance directly.
187
+ navigateTo(currentStepIndex + 1);
188
+ return;
189
+ }
56
190
 
57
- const form = new SimpleForm({
58
- editable: true,
59
- layout: "ResponsiveGridLayout",
60
- labelSpanXL: 4, labelSpanL: 4, labelSpanM: 4, labelSpanS: 12,
61
- adjustLabelSpan: false,
62
- emptySpanXL: 0, emptySpanL: 0, emptySpanM: 0, emptySpanS: 0,
63
- columnsXL: 1, columnsL: 1, columnsM: 1,
64
- singleContainerFullSize: false
65
- });
66
-
67
- if (element.elements && Array.isArray(element.elements)) {
68
- element.elements.forEach(childElement => {
69
- this._renderElementInForm(form, childElement, schema, modelName, engine);
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
- step.addContent(form);
74
- wizard.addStep(step);
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
- Logger.warn(`[MetaUI] Top-level element '${element.type}' inside WizardLayout must be 'Group' or 'WizardStep'.`, "", "WizardLayout");
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
- return wizard;
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
- * Synthesizes a field into the wizard step form.
85
- * @param form The SimpleForm belonging to the step.
86
- * @param element The layout element definition.
87
- * @param schema The schema reference.
88
- * @param modelName The UI5 data model name.
89
- * @param engine The core Engine instance.
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(form: SimpleForm, element: ILayoutElement, schema: ISchema, modelName: string, engine: Engine): void {
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}' in uiLayout Control.`, "", "WizardLayout");
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 definitions.`, "", "WizardLayout");
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
- required: meta.required
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 control = engine.generateField(effectiveMeta, `/${bindingPath}`, modelName);
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 Wizard field ${propKey}`, (error as Error).message, "WizardLayout");
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
- } else {
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
  }