@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.
- package/L1_Foundation/L1_Foundation.js +13 -0
- package/L1_Foundation/bootstrap/bootstrap.js +11 -0
- package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
- package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
- package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
- package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
- package/L1_Foundation/bootstrap/module_registry.js +102 -0
- package/L1_Foundation/bootstrap/prism_loader.js +164 -0
- package/L1_Foundation/config/client_config.js +110 -0
- package/L1_Foundation/config/config.js +7 -0
- package/L1_Foundation/connection/connection.js +8 -0
- package/L1_Foundation/connection/websocket_connection.js +122 -0
- package/L1_Foundation/constants/bifrost_constants.js +284 -0
- package/L1_Foundation/constants/constants.js +7 -0
- package/L1_Foundation/logger/logger.js +10 -0
- package/L2_Handling/L2_Handling.js +15 -0
- package/L2_Handling/cache/cache.js +22 -0
- package/L2_Handling/cache/cache_constants.js +69 -0
- package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
- package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
- package/L2_Handling/cache/orchestration/orchestration.js +12 -0
- package/L2_Handling/cache/storage/session_manager.js +289 -0
- package/L2_Handling/cache/storage/storage.js +10 -0
- package/L2_Handling/cache/storage/storage_manager.js +590 -0
- package/L2_Handling/display/composite/composite.js +13 -0
- package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
- package/L2_Handling/display/composite/swiper_renderer.js +564 -0
- package/L2_Handling/display/composite/terminal_renderer.js +922 -0
- package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
- package/L2_Handling/display/display.js +30 -0
- package/L2_Handling/display/feedback/feedback.js +11 -0
- package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
- package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
- package/L2_Handling/display/inputs/button_renderer.js +634 -0
- package/L2_Handling/display/inputs/form_renderer.js +583 -0
- package/L2_Handling/display/inputs/input_renderer.js +658 -0
- package/L2_Handling/display/inputs/inputs.js +12 -0
- package/L2_Handling/display/navigation/menu_renderer.js +206 -0
- package/L2_Handling/display/navigation/navigation.js +11 -0
- package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
- package/L2_Handling/display/orchestration/orchestration.js +11 -0
- package/L2_Handling/display/orchestration/renderer.js +430 -0
- package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
- package/L2_Handling/display/outputs/alert_renderer.js +161 -0
- package/L2_Handling/display/outputs/audio_renderer.js +94 -0
- package/L2_Handling/display/outputs/card_renderer.js +229 -0
- package/L2_Handling/display/outputs/code_renderer.js +66 -0
- package/L2_Handling/display/outputs/dl_renderer.js +131 -0
- package/L2_Handling/display/outputs/header_renderer.js +162 -0
- package/L2_Handling/display/outputs/icon_renderer.js +107 -0
- package/L2_Handling/display/outputs/image_renderer.js +145 -0
- package/L2_Handling/display/outputs/list_renderer.js +190 -0
- package/L2_Handling/display/outputs/outputs.js +19 -0
- package/L2_Handling/display/outputs/table_renderer.js +765 -0
- package/L2_Handling/display/outputs/text_renderer.js +818 -0
- package/L2_Handling/display/outputs/typography_renderer.js +293 -0
- package/L2_Handling/display/outputs/video_renderer.js +116 -0
- package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
- package/L2_Handling/display/primitives/form_primitives.js +526 -0
- package/L2_Handling/display/primitives/generic_containers.js +109 -0
- package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
- package/L2_Handling/display/primitives/link_primitives.js +552 -0
- package/L2_Handling/display/primitives/lists_primitives.js +262 -0
- package/L2_Handling/display/primitives/media_primitives.js +383 -0
- package/L2_Handling/display/primitives/primitives.js +19 -0
- package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
- package/L2_Handling/display/primitives/table_primitives.js +528 -0
- package/L2_Handling/display/primitives/typography_primitives.js +175 -0
- package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
- package/L2_Handling/display/specialized/specialized.js +10 -0
- package/L2_Handling/hooks/hooks.js +9 -0
- package/L2_Handling/hooks/menu_integration.js +57 -0
- package/L2_Handling/hooks/widget_hook_manager.js +292 -0
- package/L2_Handling/message/message.js +8 -0
- package/L2_Handling/message/message_handler.js +701 -0
- package/L2_Handling/navigation/navigation.js +8 -0
- package/L2_Handling/navigation/navigation_manager.js +403 -0
- package/L2_Handling/zhooks/features/cache_live.js +287 -0
- package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
- package/L2_Handling/zhooks/zhooks_manager.js +65 -0
- package/L2_Handling/zvaf/zvaf.js +8 -0
- package/L2_Handling/zvaf/zvaf_manager.js +334 -0
- package/L3_Abstraction/L3_Abstraction.js +12 -0
- package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
- package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
- package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
- package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
- package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
- package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
- package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
- package/L3_Abstraction/renderer/renderer.js +1 -0
- package/L3_Abstraction/session/session.js +1 -0
- package/L4_Orchestration/L4_Orchestration.js +11 -0
- package/L4_Orchestration/client/client.js +1 -0
- package/L4_Orchestration/facade/facade.js +9 -0
- package/L4_Orchestration/facade/manager_registry.js +118 -0
- package/L4_Orchestration/facade/renderer_registry.js +274 -0
- package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
- package/L4_Orchestration/lifecycle/initializer.js +135 -0
- package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
- package/L4_Orchestration/rendering/facade.js +94 -0
- package/L4_Orchestration/rendering/rendering.js +7 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/bifrost_client.js +204 -0
- package/bifrost_core.js +1686 -0
- package/docs/ARCHITECTURE.md +111 -0
- package/docs/PROTOCOL.md +106 -0
- package/docs/RENDERERS.md +101 -0
- package/docs/SECURITY.md +92 -0
- package/package.json +24 -0
- package/syntax/prism-zconfig.js +41 -0
- package/syntax/prism-zenv.js +69 -0
- package/syntax/prism-zolo-theme.css +288 -0
- package/syntax/prism-zolo.js +380 -0
- package/syntax/prism-zschema.js +38 -0
- package/syntax/prism-zspark.js +25 -0
- package/syntax/prism-zui.js +68 -0
- package/zSys/accessibility/accessibility.js +10 -0
- package/zSys/accessibility/emoji_accessibility.js +173 -0
- package/zSys/dom/block_utils.js +122 -0
- package/zSys/dom/container_utils.js +370 -0
- package/zSys/dom/dom.js +13 -0
- package/zSys/dom/dom_utils.js +328 -0
- package/zSys/dom/encoding_utils.js +117 -0
- package/zSys/dom/style_utils.js +71 -0
- package/zSys/errors/error_display.js +299 -0
- package/zSys/errors/errors.js +10 -0
- package/zSys/theme/color_utils.js +274 -0
- package/zSys/theme/dark_mode_utils.js +272 -0
- package/zSys/theme/size_utils.js +256 -0
- package/zSys/theme/spacing_utils.js +405 -0
- package/zSys/theme/theme.js +14 -0
- package/zSys/theme/zbase.css +1735 -0
- package/zSys/theme/zbase_inject.js +161 -0
- package/zSys/theme/ztheme_utils.js +305 -0
- package/zSys/validation/error_boundary.js +201 -0
- package/zSys/validation/validation.js +11 -0
- package/zSys/validation/validation_utils.js +238 -0
- 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
|
+
}
|