@zolomedia/bifrost-client 1.7.74

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 (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,583 @@
1
+ /**
2
+ * FormRenderer - Async Form Rendering for zDialog in Bifrost Mode
3
+ *
4
+ * This renderer handles the display and submission of zDialog forms in the browser.
5
+ * Unlike Terminal mode (which collects input field-by-field), Bifrost displays the
6
+ * entire form at once with all fields visible.
7
+ *
8
+ * Key Differences from Terminal:
9
+ * - Terminal: Blocking, field-by-field, synchronous
10
+ * - Bifrost: Non-blocking, all-at-once, asynchronous
11
+ *
12
+ * Flow:
13
+ * 1. Backend sends zDialog event with form context
14
+ * 2. FormRenderer displays full HTML form
15
+ * 3. User fills and clicks Submit
16
+ * 4. FormRenderer sends form_submit WebSocket message
17
+ * 5. Backend validates and executes onSubmit
18
+ * 6. Backend sends result back to frontend
19
+ *
20
+ * Architecture:
21
+ * - Uses form_primitives.js for raw HTML element creation
22
+ * - Uses dom_utils.js for DOM manipulation
23
+ * - Applies zTheme classes for styling
24
+ * - Handles WebSocket communication for submission
25
+ *
26
+ * @module FormRenderer
27
+ */
28
+
29
+ // ─────────────────────────────────────────────────────────────────
30
+ // Imports
31
+ // ─────────────────────────────────────────────────────────────────
32
+
33
+ // Layer 2: Utilities
34
+ import { createElement, clearElement } from '../../../zSys/dom/dom_utils.js';
35
+ import { withErrorBoundary } from '../../../zSys/validation/error_boundary.js';
36
+ import { convertZPathToURL } from '../primitives/link_primitives.js';
37
+
38
+ // Layer 0: Primitives
39
+ import {
40
+ createForm,
41
+ createInput,
42
+ createTextarea,
43
+ createSelect,
44
+ createOption,
45
+ createLabel
46
+ } from '../primitives/form_primitives.js';
47
+
48
+ export class FormRenderer {
49
+ constructor(logger, client = null) {
50
+ if (!logger) {
51
+ throw new Error('[FormRenderer] logger is required');
52
+ }
53
+ this.logger = logger;
54
+ this.client = client;
55
+ // Use Map to store multiple form contexts, keyed by _dialogId
56
+ this.formContexts = new Map();
57
+
58
+ // Wrap renderForm with the error boundary (public API is renderForm).
59
+ const originalRender = this.renderForm.bind(this);
60
+ this.renderForm = withErrorBoundary(originalRender, {
61
+ component: 'FormRenderer',
62
+ logger: this.logger
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Render a zDialog form
68
+ * @param {Object} eventData - Form context from backend
69
+ * @param {string} eventData.title - Form title
70
+ * @param {string} eventData.model - Schema model path (optional)
71
+ * @param {Array} eventData.fields - Field definitions
72
+ * @param {Object} eventData.onSubmit - Submit action to execute
73
+ * @param {string} eventData._dialogId - Unique form identifier
74
+ * @returns {HTMLElement} Form container element
75
+ */
76
+ renderForm(eventData) {
77
+ this.logger.log('[FormRenderer] Rendering form:', eventData.title);
78
+
79
+ const {
80
+ title = 'Form',
81
+ model,
82
+ table,
83
+ fields = [],
84
+ dialog_mode,
85
+ onSubmit,
86
+ _dialogId
87
+ } = eventData;
88
+
89
+ // Store form context in Map, keyed by unique _dialogId
90
+ this.formContexts.set(_dialogId, {
91
+ model,
92
+ table,
93
+ onSubmit,
94
+ fields
95
+ });
96
+
97
+ // dialog_mode: "confirm" → confirm button (backend-declared, SSOT signal)
98
+ // Backend sets this when fields: [] — we read the server signal, not fields.length
99
+ if (dialog_mode === 'confirm') {
100
+ return this._renderConfirmButton(title, onSubmit, _dialogId);
101
+ }
102
+
103
+ // Create form container using primitives
104
+ const formContainer = createElement('div', ['zDialog-container', 'zCard', 'zp-4'], {
105
+ 'data-dialog-id': _dialogId
106
+ });
107
+
108
+ // Form title
109
+ if (title) {
110
+ const titleElement = createElement('h2', ['zDialog-title', 'zCard-title', 'zmb-3']);
111
+ titleElement.textContent = title;
112
+ formContainer.appendChild(titleElement);
113
+ }
114
+
115
+ // Create HTML form element using primitive
116
+ const form = createForm({
117
+ class: 'zDialog-form',
118
+ 'data-dialog-id': _dialogId
119
+ });
120
+
121
+ // Render fields
122
+ fields.forEach(fieldDef => {
123
+ const fieldGroup = this._createFieldGroup(fieldDef);
124
+ form.appendChild(fieldGroup);
125
+ });
126
+
127
+ // Error display area (hidden by default)
128
+ const errorContainer = createElement('div', ['zDialog-errors', 'zAlert', 'zAlert-danger', 'zmt-3']);
129
+ errorContainer.style.display = 'none';
130
+ form.appendChild(errorContainer);
131
+
132
+ // Submit button using primitive
133
+ const submitButton = createElement('button', ['zBtn', 'zBtn-primary', 'zmt-3'], {
134
+ type: 'submit'
135
+ });
136
+ submitButton.textContent = 'Submit';
137
+ form.appendChild(submitButton);
138
+
139
+ // Handle form submission
140
+ form.addEventListener('submit', (e) => {
141
+ e.preventDefault();
142
+ this._handleSubmit(form, _dialogId);
143
+ });
144
+
145
+ formContainer.appendChild(form);
146
+ return formContainer;
147
+ }
148
+
149
+ /**
150
+ * Render a confirm button for zDialog with fields: []
151
+ * No input collection — action is pre-baked into onSubmit.
152
+ * Color is derived from the action type: delete → danger, others → primary.
153
+ * @private
154
+ * @param {string} title - Dialog title (shown above the button)
155
+ * @param {Object} onSubmit - onSubmit action from the dialog definition
156
+ * @param {string} dialogId - Unique dialog identifier
157
+ * @returns {HTMLElement} Confirm button container
158
+ */
159
+ _renderConfirmButton(title, onSubmit, dialogId) {
160
+ const action = onSubmit?.zData?.action || 'submit';
161
+ const isDangerous = action === 'delete';
162
+ const btnClass = isDangerous ? 'zBtn-danger' : 'zBtn-primary';
163
+ const btnLabel = isDangerous ? 'Confirm Delete' : 'Confirm';
164
+
165
+ this.logger.log(`[FormRenderer] fields:[] → confirm button mode | action: ${action} | dialogId: ${dialogId}`);
166
+
167
+ const container = createElement('div', ['zDialog-container', 'zCard', 'zp-4'], {
168
+ 'data-dialog-id': dialogId
169
+ });
170
+
171
+ if (title) {
172
+ const titleElement = createElement('p', ['zmb-2', 'zText-muted']);
173
+ titleElement.textContent = title;
174
+ container.appendChild(titleElement);
175
+ }
176
+
177
+ // Error display area (hidden by default)
178
+ const errorContainer = createElement('div', ['zDialog-errors', 'zAlert', 'zAlert-danger', 'zmt-3']);
179
+ errorContainer.style.display = 'none';
180
+ container.appendChild(errorContainer);
181
+
182
+ const btn = createElement('button', ['zBtn', btnClass]);
183
+ btn.textContent = btnLabel;
184
+ btn.addEventListener('click', async () => {
185
+ btn.disabled = true;
186
+ btn.textContent = 'Processing...';
187
+ errorContainer.style.display = 'none';
188
+ clearElement(errorContainer);
189
+
190
+ try {
191
+ const formContext = this.formContexts.get(dialogId);
192
+ if (!formContext) {
193
+ this.logger.error('[FormRenderer] No form context for confirm button:', dialogId);
194
+ return;
195
+ }
196
+
197
+ const response = await this.client.send({
198
+ event: 'form_submit',
199
+ dialogId: dialogId,
200
+ data: {},
201
+ model: formContext.model,
202
+ table: formContext.table
203
+ });
204
+
205
+ if (response.success) {
206
+ clearElement(container);
207
+ const successMsg = createElement('div', ['zAlert', 'zAlert-success']);
208
+ successMsg.innerHTML = `<strong>Done!</strong> ${response.message || 'Action completed.'}`;
209
+ container.appendChild(successMsg);
210
+ this.formContexts.delete(dialogId);
211
+ } else {
212
+ errorContainer.style.display = 'block';
213
+ errorContainer.textContent = response.message || 'Action failed.';
214
+ btn.disabled = false;
215
+ btn.textContent = btnLabel;
216
+ }
217
+ } catch (error) {
218
+ this.logger.error('[FormRenderer] Confirm button error:', error);
219
+ errorContainer.style.display = 'block';
220
+ errorContainer.textContent = 'Failed to execute action. Please try again.';
221
+ btn.disabled = false;
222
+ btn.textContent = btnLabel;
223
+ }
224
+ });
225
+
226
+ container.appendChild(btn);
227
+ return container;
228
+ }
229
+
230
+ /**
231
+ * Create a form field group (label + input)
232
+ * @private
233
+ * @param {string|Object} fieldDef - Field definition (string or object)
234
+ * @returns {HTMLElement} Field group element
235
+ */
236
+ _createFieldGroup(fieldDef) {
237
+ // Handle both string and object field definitions
238
+ const fieldName = typeof fieldDef === 'string' ? fieldDef : fieldDef.name;
239
+
240
+ // Auto-detect field type from field name if not explicitly provided
241
+ let fieldType = 'text';
242
+ if (typeof fieldDef === 'object' && fieldDef.type) {
243
+ // Explicit type provided
244
+ fieldType = fieldDef.type;
245
+ } else {
246
+ // Auto-detect based on field name
247
+ const lowerName = fieldName.toLowerCase();
248
+ if (lowerName === 'password' || lowerName.includes('password')) {
249
+ fieldType = 'password';
250
+ } else if (lowerName === 'email' || lowerName.includes('email')) {
251
+ fieldType = 'email';
252
+ } else if (lowerName === 'phone' || lowerName === 'tel' || lowerName.includes('phone')) {
253
+ fieldType = 'tel';
254
+ }
255
+ }
256
+
257
+ const fieldLabel = typeof fieldDef === 'object' ? (fieldDef.label || fieldName) : fieldName;
258
+ const required = typeof fieldDef === 'object' ? (fieldDef.required === true) : false;
259
+
260
+ // Field group container
261
+ const fieldGroup = createElement('div', ['zmb-3']);
262
+
263
+ // Label using primitive
264
+ const label = createLabel(fieldName, { class: 'zLabel' });
265
+ label.textContent = this._formatLabel(fieldLabel);
266
+
267
+ if (required) {
268
+ const requiredMark = createElement('span', ['zText-danger']);
269
+ requiredMark.textContent = ' *';
270
+ label.appendChild(requiredMark);
271
+ }
272
+ fieldGroup.appendChild(label);
273
+
274
+ // Input field using primitive
275
+ const input = this._createInput(fieldName, fieldType, required, fieldDef);
276
+ fieldGroup.appendChild(input);
277
+
278
+ return fieldGroup;
279
+ }
280
+
281
+ /**
282
+ * Create an input element based on field type using primitives
283
+ * @private
284
+ * @param {string} fieldName - Field name
285
+ * @param {string} fieldType - Field type (text, password, email, select, textarea, etc.)
286
+ * @param {boolean} required - Whether field is required
287
+ * @param {string|Object} fieldDef - Original field definition (for options/default)
288
+ * @returns {HTMLElement} Input element
289
+ */
290
+ _createInput(fieldName, fieldType, required, fieldDef = null) {
291
+ let input;
292
+
293
+ if (fieldType === 'select') {
294
+ // Render <select> for enum fields
295
+ const options = (fieldDef && typeof fieldDef === 'object' && fieldDef.options) ? fieldDef.options : [];
296
+ const defaultVal = (fieldDef && typeof fieldDef === 'object') ? fieldDef.default : null;
297
+
298
+ input = createSelect({
299
+ name: fieldName,
300
+ class: 'zInput',
301
+ required: required
302
+ });
303
+
304
+ // Add blank placeholder option when no default
305
+ if (!defaultVal) {
306
+ const placeholder = createOption('', `— select ${this._formatLabel(fieldName).toLowerCase()} —`);
307
+ placeholder.disabled = true;
308
+ placeholder.selected = true;
309
+ input.appendChild(placeholder);
310
+ }
311
+
312
+ options.forEach(opt => {
313
+ const optEl = createOption(opt, opt);
314
+ if (String(opt) === String(defaultVal)) {
315
+ optEl.selected = true;
316
+ }
317
+ input.appendChild(optEl);
318
+ });
319
+
320
+ } else if (fieldType === 'radio') {
321
+ // Render radio button group for bool fields (True / False [/ null if not required])
322
+ const options = (fieldDef && typeof fieldDef === 'object' && fieldDef.options) ? fieldDef.options : ['true', 'false'];
323
+ const defaultVal = (fieldDef && typeof fieldDef === 'object' && fieldDef.default != null) ? String(fieldDef.default) : null;
324
+
325
+ const wrapper = createElement('div', ['zRadio-group', 'zd-flex', 'zgap-3']);
326
+ options.forEach(opt => {
327
+ const optLabel = createElement('label', ['zRadio-label', 'zd-flex', 'zalign-items-center', 'zgap-1']);
328
+ const radio = createInput('radio', { name: fieldName, value: opt });
329
+ if (String(opt) === defaultVal) radio.checked = true;
330
+ const caption = createElement('span');
331
+ caption.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
332
+ optLabel.appendChild(radio);
333
+ optLabel.appendChild(caption);
334
+ wrapper.appendChild(optLabel);
335
+ });
336
+ input = wrapper;
337
+
338
+ } else if (fieldType === 'textarea') {
339
+ const defaultVal = (fieldDef && typeof fieldDef === 'object') ? fieldDef.default : null;
340
+ input = createTextarea({
341
+ name: fieldName,
342
+ class: 'zInput',
343
+ rows: 4,
344
+ required: required,
345
+ placeholder: `Enter ${this._formatLabel(fieldName).toLowerCase()}`,
346
+ ...(defaultVal != null && { value: String(defaultVal) })
347
+ });
348
+ } else {
349
+ // Use input primitive with appropriate type
350
+ let inputType = 'text';
351
+
352
+ // Map field types to HTML5 input types
353
+ if (fieldType === 'password') {
354
+ inputType = 'password';
355
+ } else if (fieldType === 'email') {
356
+ inputType = 'email';
357
+ } else if (fieldType === 'number') {
358
+ inputType = 'number';
359
+ } else if (fieldType === 'tel' || fieldType === 'phone') {
360
+ inputType = 'tel';
361
+ } else if (fieldType === 'date') {
362
+ inputType = 'date';
363
+ } else if (fieldType === 'time') {
364
+ inputType = 'time';
365
+ } else if (fieldType === 'datetime') {
366
+ inputType = 'datetime-local';
367
+ }
368
+
369
+ const defaultVal = (fieldDef && typeof fieldDef === 'object') ? fieldDef.default : null;
370
+ input = createInput(inputType, {
371
+ name: fieldName,
372
+ class: 'zInput',
373
+ required: required,
374
+ placeholder: `Enter ${this._formatLabel(fieldName).toLowerCase()}`,
375
+ ...(defaultVal != null && { value: String(defaultVal) })
376
+ });
377
+ }
378
+
379
+ return input;
380
+ }
381
+
382
+ /**
383
+ * Format field name to human-readable label
384
+ * @private
385
+ * @param {string} fieldName - Field name (snake_case or camelCase)
386
+ * @returns {string} Formatted label (Title Case)
387
+ */
388
+ _formatLabel(fieldName) {
389
+ // Convert snake_case or camelCase to Title Case
390
+ return fieldName
391
+ .replace(/_/g, ' ')
392
+ .replace(/([A-Z])/g, ' $1')
393
+ .replace(/^./, str => str.toUpperCase())
394
+ .trim();
395
+ }
396
+
397
+ /**
398
+ * Handle form submission
399
+ * @private
400
+ * @param {HTMLFormElement} formElement - Form element
401
+ * @param {string} dialogId - Dialog identifier
402
+ */
403
+ async _handleSubmit(formElement, dialogId) {
404
+ this.logger.log('[FormRenderer] Form submit triggered:', dialogId);
405
+
406
+ // Retrieve form context from Map using dialogId
407
+ const formContext = this.formContexts.get(dialogId);
408
+ if (!formContext) {
409
+ this.logger.error('[FormRenderer] No form context found for dialogId:', dialogId);
410
+ return;
411
+ }
412
+
413
+ // Clear previous errors
414
+ const errorContainer = formElement.querySelector('.zDialog-errors');
415
+ if (errorContainer) {
416
+ errorContainer.style.display = 'none';
417
+ clearElement(errorContainer);
418
+ }
419
+
420
+ // Collect form data
421
+ const formData = new FormData(formElement);
422
+ const data = {};
423
+ for (const [key, value] of formData.entries()) {
424
+ data[key] = value;
425
+ }
426
+
427
+ this.logger.log('[FormRenderer] Collected form data:', Object.keys(data));
428
+
429
+ // Disable submit button during submission
430
+ const submitButton = formElement.querySelector('button[type="submit"]');
431
+ const originalText = submitButton.textContent;
432
+ submitButton.disabled = true;
433
+ submitButton.textContent = 'Submitting...';
434
+
435
+ try {
436
+ // Send form submission to backend via WebSocket
437
+ const response = await this.client.send({
438
+ event: 'form_submit',
439
+ dialogId: dialogId,
440
+ data: data,
441
+ model: formContext.model,
442
+ table: formContext.table
443
+ });
444
+
445
+ this.logger.log('[FormRenderer] Submission response:', response);
446
+
447
+ // Handle response
448
+ if (response.success) {
449
+ this._handleSuccess(formElement, response);
450
+ } else {
451
+ this._handleError(formElement, response);
452
+ }
453
+
454
+ } catch (error) {
455
+ this.logger.error('[FormRenderer] Submission error:', error);
456
+ this._handleError(formElement, {
457
+ success: false,
458
+ message: 'Failed to submit form. Please try again.',
459
+ errors: [error.message]
460
+ });
461
+ } finally {
462
+ // Re-enable submit button
463
+ submitButton.disabled = false;
464
+ submitButton.textContent = originalText;
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Handle successful form submission
470
+ * @private
471
+ * @param {HTMLFormElement} formElement - Form element
472
+ * @param {Object} response - Server response
473
+ */
474
+ _handleSuccess(formElement, response) {
475
+ this.logger.log('[FormRenderer] Form submission successful');
476
+
477
+ // Display success message
478
+ const successContainer = createElement('div', ['zAlert', 'zAlert-success', 'zmt-3']);
479
+ successContainer.innerHTML = `
480
+ <strong>Success!</strong> ${response.message || 'Form submitted successfully.'}
481
+ `;
482
+
483
+ // Replace form with success message
484
+ const formContainer = formElement.closest('.zDialog-container');
485
+ if (formContainer) {
486
+ clearElement(formContainer);
487
+ formContainer.appendChild(successContainer);
488
+
489
+ // Clean up form context from Map using dialogId from form container
490
+ const dialogId = formContainer.getAttribute('data-dialog-id');
491
+ if (dialogId) {
492
+ this.formContexts.delete(dialogId);
493
+ this.logger.log('[FormRenderer] Cleaned up form context for:', dialogId);
494
+ }
495
+ }
496
+
497
+ // Server-requested navigation (e.g. login → home page).
498
+ // Converts @. zPaths to URL paths before client-side SPA navigation.
499
+ if (response.navigate) {
500
+ const routePath = convertZPathToURL(response.navigate);
501
+ this.logger.log('[FormRenderer] Server requested navigation to:', routePath);
502
+ setTimeout(() => {
503
+ if (this.client && typeof this.client._navigateToRoute === 'function') {
504
+ // Navigate, THEN refresh the navbar — login changed the session, so the
505
+ // RBAC-filtered nav (zAccount/logout) must rebuild for the new role.
506
+ // Mirrors the bounce-back path; without it the navbar is stale until reload.
507
+ this.client._navigateToRoute(routePath).then(() => {
508
+ if (typeof this.client._fetchAndPopulateNavBar === 'function') {
509
+ this.logger.log('[FormRenderer] Refreshing navbar after post-login navigation');
510
+ this.client._fetchAndPopulateNavBar().catch(err => {
511
+ this.logger.error('[FormRenderer] Failed to refresh navbar:', err);
512
+ });
513
+ }
514
+ }).catch(err => {
515
+ this.logger.error('[FormRenderer] Navigation failed:', err);
516
+ });
517
+ } else {
518
+ window.location.href = routePath;
519
+ }
520
+ }, 800);
521
+ return;
522
+ }
523
+
524
+ // If the server requests a full reload (e.g., after login, to rebuild the
525
+ // RBAC-filtered zDash sidebar with the new session), honour it with a short
526
+ // delay so the success message is briefly visible before navigation.
527
+ if (response.reload === true) {
528
+ this.logger.log('[FormRenderer] Server requested page reload for RBAC sidebar refresh');
529
+ setTimeout(() => { window.location.reload(); }, 800);
530
+ return;
531
+ }
532
+
533
+ // Refresh navbar after successful submission (e.g., after login)
534
+ // This ensures RBAC-filtered navbar items are updated
535
+ if (this.client && typeof this.client._fetchAndPopulateNavBar === 'function') {
536
+ this.logger.log('[FormRenderer] Refreshing navbar for RBAC update');
537
+ this.client._fetchAndPopulateNavBar().catch(err => {
538
+ this.logger.error('[FormRenderer] Failed to refresh navbar:', err);
539
+ });
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Handle form submission error
545
+ * @private
546
+ * @param {HTMLFormElement} formElement - Form element
547
+ * @param {Object} response - Server error response
548
+ */
549
+ _handleError(formElement, response) {
550
+ this.logger.error('[FormRenderer] Form submission failed:', response);
551
+
552
+ const errorContainer = formElement.querySelector('.zDialog-errors');
553
+ if (errorContainer) {
554
+ errorContainer.style.display = 'block';
555
+
556
+ const errorList = createElement('ul', ['zmb-0']);
557
+
558
+ // Display validation errors
559
+ if (response.errors && Array.isArray(response.errors)) {
560
+ response.errors.forEach(error => {
561
+ const errorItem = createElement('li');
562
+ errorItem.textContent = error;
563
+ errorList.appendChild(errorItem);
564
+ });
565
+ } else {
566
+ const errorItem = createElement('li');
567
+ errorItem.textContent = response.message || 'Validation failed. Please check your input.';
568
+ errorList.appendChild(errorItem);
569
+ }
570
+
571
+ clearElement(errorContainer);
572
+ const errorHeader = createElement('strong');
573
+ errorHeader.textContent = 'Error:';
574
+ errorContainer.appendChild(errorHeader);
575
+ errorContainer.appendChild(errorList);
576
+ }
577
+
578
+ // Scroll to errors
579
+ if (errorContainer) {
580
+ errorContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
581
+ }
582
+ }
583
+ }