@syncfusion/ej2-react-form-renderer 0.1.0-beta

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 (48) hide show
  1. package/README.md +148 -0
  2. package/index.d.ts +6 -0
  3. package/index.js +6 -0
  4. package/license +13 -0
  5. package/package.json +49 -0
  6. package/src/CheckboxGroup.d.ts +12 -0
  7. package/src/CheckboxGroup.js +48 -0
  8. package/src/CustomTabComponent.d.ts +13 -0
  9. package/src/CustomTabComponent.js +35 -0
  10. package/src/DataGridComponent.d.ts +29 -0
  11. package/src/DataGridComponent.js +160 -0
  12. package/src/FormRenderer.d.ts +63 -0
  13. package/src/FormRenderer.js +567 -0
  14. package/src/FormValidator.d.ts +479 -0
  15. package/src/FormValidator.js +579 -0
  16. package/src/StaticHtmlRenderer.d.ts +8 -0
  17. package/src/StaticHtmlRenderer.js +21 -0
  18. package/src/index.d.ts +6 -0
  19. package/src/index.js +6 -0
  20. package/src/json-converter.d.ts +170 -0
  21. package/src/json-converter.js +1180 -0
  22. package/src/parser.d.ts +99 -0
  23. package/src/parser.js +749 -0
  24. package/src/renderFormField.d.ts +19 -0
  25. package/src/renderFormField.js +202 -0
  26. package/src/types/FormSchema.d.ts +177 -0
  27. package/src/types/FormSchema.js +1 -0
  28. package/src/utils.js +60 -0
  29. package/styles/bootstrap-dark.css +26 -0
  30. package/styles/bootstrap.css +26 -0
  31. package/styles/bootstrap4.css +26 -0
  32. package/styles/bootstrap5-dark.css +26 -0
  33. package/styles/bootstrap5.3-dark.css +26 -0
  34. package/styles/bootstrap5.3.css +26 -0
  35. package/styles/bootstrap5.css +26 -0
  36. package/styles/fluent-dark.css +26 -0
  37. package/styles/fluent.css +26 -0
  38. package/styles/fluent2-dark.css +26 -0
  39. package/styles/fluent2.css +26 -0
  40. package/styles/form-renderer.base.css +473 -0
  41. package/styles/material-dark.css +26 -0
  42. package/styles/material.css +26 -0
  43. package/styles/material3-dark.css +26 -0
  44. package/styles/material3.css +26 -0
  45. package/styles/tailwind-dark.css +26 -0
  46. package/styles/tailwind.css +26 -0
  47. package/styles/tailwind3-dark.css +10 -0
  48. package/styles/tailwind3.css +26 -0
@@ -0,0 +1,1180 @@
1
+ // ============================================================================
2
+ // TYPE DEFINITIONS (from spec)
3
+ // ============================================================================
4
+ export const generateId = (type) => {
5
+ const timestamp = Date.now();
6
+ const random = Math.floor(Math.random() * 1000);
7
+ return `${type}_${timestamp}_${random}`;
8
+ };
9
+ // ============================================================================
10
+ // LABEL TO CAMELCASE CONVERTER
11
+ // ============================================================================
12
+ /**
13
+ * Convert label to camelCase identifier
14
+ * Examples:
15
+ * "First Name" → "firstName"
16
+ * "Email Address" → "emailAddress"
17
+ * "Date of Birth" → "dateOfBirth"
18
+ * "Phone Number (Mobile)" → "phoneNumberMobile"
19
+ */
20
+ export function labelToFieldKey(label, fallbackId) {
21
+ if (!label || label.trim() === '') {
22
+ return fallbackId || 'field';
23
+ }
24
+ // Remove special characters, keep letters, numbers, and spaces
25
+ const cleaned = label
26
+ .replace(/[^\w\s]/g, '') // Remove special chars
27
+ .trim();
28
+ if (cleaned === '') {
29
+ return fallbackId || 'field';
30
+ }
31
+ // Split by spaces/underscores and convert to camelCase
32
+ const words = cleaned.split(/[\s_]+/);
33
+ if (words.length === 0) {
34
+ return fallbackId || 'field';
35
+ }
36
+ // First word lowercase, rest title case
37
+ const camelCase = words
38
+ .map((word, index) => {
39
+ if (index === 0) {
40
+ return word.toLowerCase();
41
+ }
42
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
43
+ })
44
+ .join('');
45
+ return camelCase || fallbackId || 'field';
46
+ }
47
+ /**
48
+ * Ensure field key is unique by appending a number if needed
49
+ */
50
+ export function makeUniqueFieldKey(baseKey, existingKeys, originalId) {
51
+ if (!existingKeys.has(baseKey)) {
52
+ return baseKey;
53
+ }
54
+ // Try with original ID suffix first
55
+ if (originalId) {
56
+ const withId = `${baseKey}_${originalId.split('_').pop()}`;
57
+ if (!existingKeys.has(withId)) {
58
+ return withId;
59
+ }
60
+ }
61
+ // Fall back to numeric suffix
62
+ let counter = 2;
63
+ while (existingKeys.has(`${baseKey}${counter}`)) {
64
+ counter++;
65
+ }
66
+ return `${baseKey}${counter}`;
67
+ }
68
+ // ============================================================================
69
+ // UI TYPE TO SEMANTIC TYPE MAPPING
70
+ // ============================================================================
71
+ const UI_TYPE_TO_SEMANTIC_TYPE = {
72
+ // String types
73
+ 'textbox': { type: 'string' },
74
+ 'textarea': { type: 'string' },
75
+ 'email': { type: 'string', format: 'email' },
76
+ 'url': { type: 'string', format: 'url' },
77
+ 'phone': { type: 'string' },
78
+ 'password': { type: 'string' },
79
+ 'inputmask': { type: 'string' },
80
+ // Numeric types
81
+ 'number': { type: 'number' },
82
+ 'rangeslider': { type: 'number' },
83
+ 'rating': { type: 'number' },
84
+ // Boolean types
85
+ 'checkbox': { type: 'boolean' },
86
+ 'switch': { type: 'boolean' },
87
+ // Date/Time types
88
+ 'datepicker': { type: 'string', format: 'date' },
89
+ 'datetimepicker': { type: 'string', format: 'date-time' },
90
+ 'timepicker': { type: 'string', format: 'time' },
91
+ 'daterange': { type: 'string', format: 'date' },
92
+ // Array types
93
+ 'multiselect': { type: 'array' },
94
+ 'checkboxgroup': { type: 'array' },
95
+ // Single-value selection (stored as string)
96
+ 'dropdown': { type: 'string' },
97
+ 'radio': { type: 'string' },
98
+ 'splitbutton': { type: 'string' },
99
+ // Special types (excluded from data schema)
100
+ 'button': { type: 'button' },
101
+ 'message': { type: 'message' },
102
+ 'signature': { type: 'sign' },
103
+ 'imageeditor': { type: 'image' },
104
+ 'fileupload': { type: 'file' },
105
+ 'colorpicker': { type: 'color' },
106
+ 'datagrid': { type: 'grid' },
107
+ // Layout types (excluded from data schema)
108
+ 'statichtml': { type: 'string' },
109
+ 'table': null,
110
+ 'panel': null,
111
+ 'tabs': null,
112
+ 'card': null
113
+ };
114
+ /**
115
+ * Remove internal-only UI properties before emitting UI schema
116
+ */
117
+ function stripConfiguredFlag(properties) {
118
+ const out = {};
119
+ Object.entries(properties || {}).forEach(([key, val]) => {
120
+ const copy = { ...val };
121
+ if (copy && copy._configured !== undefined) {
122
+ delete copy._configured;
123
+ }
124
+ out[key] = copy;
125
+ });
126
+ return out;
127
+ }
128
+ // ============================================================================
129
+ // SPLIT ALGORITHM
130
+ // ============================================================================
131
+ export function splitSchema(heartSchema) {
132
+ const dataFields = {};
133
+ const uiFields = {};
134
+ const layout = [];
135
+ const usedFieldKeys = new Set(); // Track unique field keys
136
+ const seenComponentIds = new Set(); // Track component IDs to prevent duplicates
137
+ // Stack for iterative traversal (avoid recursion)
138
+ const stack = heartSchema.components.map(c => ({
139
+ component: c,
140
+ layoutParent: layout
141
+ }));
142
+ while (stack.length > 0) {
143
+ // Use shift() instead of pop() to maintain order (FIFO instead of LIFO)
144
+ const { component, layoutParent } = stack.shift();
145
+ // Get semantic type mapping (may be null for presentation-only widgets)
146
+ const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[component.type];
147
+ // Handle layout containers
148
+ if (component.type === 'table') {
149
+ const tableNode = {
150
+ type: 'table',
151
+ id: component.id,
152
+ label: component.label,
153
+ cssClass: component.cssClass,
154
+ hideBorders: component.hideBorders,
155
+ rows: component.tableCells?.length || 0,
156
+ cols: component.tableCells?.[0]?.length || 0,
157
+ cells: [],
158
+ columnWidths: component.columnWidths
159
+ };
160
+ // Process table cells
161
+ component.tableCells?.forEach((row, rowIndex) => {
162
+ const cellRow = [];
163
+ row.forEach((cellComponents, colIndex) => {
164
+ const cellChildren = [];
165
+ // Reverse children and use unshift() to maintain order with depth-first processing
166
+ [...cellComponents].reverse().forEach((comp) => {
167
+ stack.unshift({ component: comp, layoutParent: cellChildren });
168
+ });
169
+ cellRow.push({ row: rowIndex, col: colIndex, children: cellChildren });
170
+ });
171
+ tableNode.cells.push(cellRow);
172
+ });
173
+ layoutParent.push(tableNode);
174
+ continue;
175
+ }
176
+ if (component.type === 'panel') {
177
+ const panelNode = {
178
+ type: 'panel',
179
+ id: component.id,
180
+ label: component.label,
181
+ cssClass: component.cssClass,
182
+ hideBorders: component.hideBorders,
183
+ hideLabel: component.hideLabel,
184
+ children: []
185
+ };
186
+ // Process panel children
187
+ // Reverse children and use unshift() to maintain order with depth-first processing
188
+ [...(component.children || [])].reverse().forEach((child) => {
189
+ stack.unshift({ component: child, layoutParent: panelNode.children });
190
+ });
191
+ layoutParent.push(panelNode);
192
+ continue;
193
+ }
194
+ if (component.type === 'tabs') {
195
+ const tabsNode = {
196
+ type: 'tabs',
197
+ id: component.id,
198
+ label: component.label,
199
+ cssClass: component.cssClass,
200
+ tabOptions: component.tabOptions,
201
+ tabs: []
202
+ };
203
+ // Process tabs
204
+ component.tabItems?.forEach((tab) => {
205
+ const tabChildren = [];
206
+ // Reverse children and use unshift() to maintain order with depth-first processing
207
+ [...(tab.content || [])].reverse().forEach((comp) => {
208
+ stack.unshift({ component: comp, layoutParent: tabChildren });
209
+ });
210
+ tabsNode.tabs.push({ header: tab.header, children: tabChildren });
211
+ });
212
+ layoutParent.push(tabsNode);
213
+ continue;
214
+ }
215
+ if (component.type === 'card') {
216
+ const cardNode = {
217
+ type: 'card',
218
+ id: component.id,
219
+ label: component.label,
220
+ cssClass: component.cssClass,
221
+ hideLabel: component.hideLabel,
222
+ cardTitle: component.cardTitle,
223
+ cardSubtitle: component.cardSubtitle,
224
+ enableAction: component.enableAction,
225
+ actionButtonLabel: component.actionButtonLabel,
226
+ actionButtonType: component.actionButtonType,
227
+ children: []
228
+ };
229
+ // Process card children
230
+ // Reverse children and use unshift() to maintain order with depth-first processing
231
+ [...(component.children || [])].reverse().forEach((child) => {
232
+ stack.unshift({ component: child, layoutParent: cardNode.children });
233
+ });
234
+ layoutParent.push(cardNode);
235
+ continue;
236
+ }
237
+ // ✅ DEDUPLICATION: Skip if we've already processed this component ID
238
+ if (seenComponentIds.has(component.id)) {
239
+ //console.log(`⚠️ [splitSchema] Skipping duplicate component: ${component.id} (${component.label})`);
240
+ continue;
241
+ }
242
+ seenComponentIds.add(component.id);
243
+ // Generate semantic field key from label
244
+ const baseFieldKey = labelToFieldKey(component.label || component.id, component.id);
245
+ const fieldKey = makeUniqueFieldKey(baseFieldKey, usedFieldKeys, component.id);
246
+ usedFieldKeys.add(fieldKey);
247
+ // Extract data field properties (all components, no skipping)
248
+ // If no semantic type mapping exists, use 'any' as the type
249
+ const dataField = extractDataField(component, semanticType || { type: 'any' });
250
+ dataFields[fieldKey] = dataField;
251
+ // Extract UI field properties (all components)
252
+ uiFields[fieldKey] = extractUIField(component);
253
+ // Add property reference to layout (use fieldKey instead of component ID)
254
+ layoutParent.push({ type: 'field', propertyId: fieldKey });
255
+ }
256
+ return {
257
+ dataSchema: {
258
+ version: heartSchema.version,
259
+ properties: dataFields
260
+ },
261
+ uiSchema: {
262
+ properties: stripConfiguredFlag(uiFields),
263
+ layout
264
+ }
265
+ };
266
+ }
267
+ function extractDataField(component, semanticType) {
268
+ const dataField = {
269
+ id: component.id,
270
+ type: semanticType.type,
271
+ };
272
+ // Add format if present
273
+ if (semanticType.format) {
274
+ dataField.format = semanticType.format;
275
+ }
276
+ // Extract allowed properties only
277
+ if (component.label !== undefined)
278
+ dataField.label = component.label;
279
+ if (component.defaultValue !== undefined)
280
+ dataField.defaultValue = component.defaultValue;
281
+ if (component.content !== undefined)
282
+ dataField.content = component.content;
283
+ if (component.buttonType !== undefined)
284
+ dataField.buttonType = component.buttonType;
285
+ if (component.textboxType !== undefined)
286
+ dataField.textboxType = component.textboxType;
287
+ if (component.required !== undefined)
288
+ dataField.required = component.required;
289
+ // Extract options for selection fields (dropdown, radio, multiselect)
290
+ if (component.options !== undefined)
291
+ dataField.options = component.options;
292
+ if (component.minLength !== undefined)
293
+ dataField.minLength = component.minLength;
294
+ if (component.maxLength !== undefined)
295
+ dataField.maxLength = component.maxLength;
296
+ // Handle numeric validation (use canonical names)
297
+ if (component.minValue !== undefined)
298
+ dataField.minValue = component.minValue;
299
+ if (component.maxValue !== undefined)
300
+ dataField.maxValue = component.maxValue;
301
+ // Date/time validation
302
+ if (component.minDate !== undefined)
303
+ dataField.minDate = component.minDate;
304
+ if (component.maxDate !== undefined)
305
+ dataField.maxDate = component.maxDate;
306
+ if (component.minTime !== undefined)
307
+ dataField.minTime = component.minTime;
308
+ if (component.maxTime !== undefined)
309
+ dataField.maxTime = component.maxTime;
310
+ // Pattern validation (rename from regex, convert RegExp to string)
311
+ if (component.regex !== undefined) {
312
+ const regex = component.regex;
313
+ dataField.pattern = regex instanceof RegExp ? regex.source : regex;
314
+ }
315
+ if (component.pattern !== undefined) {
316
+ const pattern = component.pattern;
317
+ dataField.pattern = pattern instanceof RegExp ? pattern.source : pattern;
318
+ }
319
+ return dataField;
320
+ }
321
+ function extractUIField(component) {
322
+ const uiField = {
323
+ widget: component.type,
324
+ };
325
+ // Copy all properties EXCEPT those in data schema allowed list
326
+ const dataSchemaProps = new Set([
327
+ 'id', 'type', 'label', 'format', 'defaultValue', 'required', 'options',
328
+ 'minLength', 'maxLength', 'minimum', 'maximum', 'minValue', 'maxValue',
329
+ 'minDate', 'maxDate', 'minTime', 'maxTime', 'regex', 'pattern', 'content', 'buttonType', 'textboxType'
330
+ ]);
331
+ Object.keys(component).forEach(key => {
332
+ if (!dataSchemaProps.has(key) && key !== 'id' && key !== 'type') {
333
+ uiField[key] = component[key];
334
+ }
335
+ });
336
+ // Special handling: Filter DataGrid columns to keep only required properties
337
+ if (component.type === 'datagrid' && uiField.columns && Array.isArray(uiField.columns)) {
338
+ const requiredColumnProps = [
339
+ 'field',
340
+ 'headerText',
341
+ 'type',
342
+ 'width',
343
+ 'format',
344
+ 'editType',
345
+ 'validationRules'
346
+ ];
347
+ uiField.columns = uiField.columns.map((col) => {
348
+ const filteredCol = {};
349
+ requiredColumnProps.forEach(prop => {
350
+ if (col[prop] !== undefined) {
351
+ filteredCol[prop] = col[prop];
352
+ }
353
+ });
354
+ return filteredCol;
355
+ });
356
+ }
357
+ return uiField;
358
+ }
359
+ // ============================================================================
360
+ // MERGE ALGORITHM
361
+ // ============================================================================
362
+ /**
363
+ * Merge split schemas back into heart schema
364
+ * Supports both simple merge and fuzzy matching for re-import scenarios
365
+ */
366
+ export function mergeSchema(split, existingUISchema) {
367
+ // Create lookup map for O(1) access to data fields
368
+ const dataFieldMap = new Map();
369
+ Object.entries(split.dataSchema.properties).forEach(([id, field]) => {
370
+ dataFieldMap.set(id, field);
371
+ });
372
+ // Use existing UI schema if provided (for re-import), otherwise use split UI schema
373
+ let uiSchema = existingUISchema || split.uiSchema;
374
+ // ✅ Ensure all UI properties have _configured flag (add if missing from imported JSON)
375
+ const enhancedUIProperties = {};
376
+ Object.entries(uiSchema.properties).forEach(([key, uiField]) => {
377
+ enhancedUIProperties[key] = {
378
+ ...uiField,
379
+ _configured: uiField._configured !== undefined ? uiField._configured : true
380
+ };
381
+ });
382
+ // Use enhanced UI schema with _configured flags
383
+ uiSchema = {
384
+ ...uiSchema,
385
+ properties: enhancedUIProperties
386
+ };
387
+ // Track processed field IDs to prevent duplicates
388
+ const processedFieldIds = new Set();
389
+ // Recursive function to process layout nodes
390
+ const processLayoutNodes = (layoutNodes) => {
391
+ const components = [];
392
+ for (const node of layoutNodes) {
393
+ if (node.type === 'field') {
394
+ const fieldNode = node;
395
+ // ✅ DEDUPLICATION: Skip if we've already processed this field
396
+ if (processedFieldIds.has(fieldNode.propertyId)) {
397
+ //console.log(`⚠️ [mergeSchema] Skipping duplicate field: ${fieldNode.propertyId}`);
398
+ continue;
399
+ }
400
+ processedFieldIds.add(fieldNode.propertyId);
401
+ const dataField = dataFieldMap.get(fieldNode.propertyId);
402
+ const uiField = uiSchema.properties[fieldNode.propertyId];
403
+ if (!dataField) {
404
+ console.warn(`Field ${fieldNode.propertyId} in layout not found in data schema`);
405
+ // Generate fallback data field
406
+ const fallbackDataField = {
407
+ id: fieldNode.propertyId,
408
+ type: 'any',
409
+ label: uiField?.widget || 'Unknown Field'
410
+ };
411
+ components.push(mergeField(fallbackDataField, uiField));
412
+ }
413
+ else {
414
+ components.push(mergeField(dataField, uiField));
415
+ }
416
+ }
417
+ else if (node.type === 'table') {
418
+ const tableNode = node;
419
+ const tableComponent = {
420
+ id: tableNode.id,
421
+ type: 'table',
422
+ label: tableNode.label || 'Table',
423
+ cssClass: tableNode.cssClass,
424
+ hideBorders: tableNode.hideBorders,
425
+ rows: tableNode.rows,
426
+ columns: tableNode.cols,
427
+ columnWidths: tableNode.columnWidths,
428
+ tableCells: [],
429
+ _configured: true
430
+ };
431
+ // Process table cells recursively
432
+ tableNode.cells.forEach((row) => {
433
+ const cellRow = [];
434
+ row.forEach((cell) => {
435
+ // Recursively process children in each cell
436
+ const cellComponents = processLayoutNodes(cell.children);
437
+ cellRow.push(cellComponents);
438
+ });
439
+ tableComponent.tableCells.push(cellRow);
440
+ });
441
+ components.push(tableComponent);
442
+ }
443
+ else if (node.type === 'panel') {
444
+ const panelNode = node;
445
+ const panelComponent = {
446
+ id: panelNode.id,
447
+ type: 'panel',
448
+ cssClass: panelNode.cssClass,
449
+ hideBorders: panelNode.hideBorders,
450
+ hideLabel: panelNode.hideLabel,
451
+ label: panelNode.label || 'Panel',
452
+ children: processLayoutNodes(panelNode.children),
453
+ _configured: true
454
+ };
455
+ components.push(panelComponent);
456
+ }
457
+ else if (node.type === 'tabs') {
458
+ const tabsNode = node;
459
+ const tabsComponent = {
460
+ id: tabsNode.id,
461
+ type: 'tabs',
462
+ label: tabsNode.label || 'Tabs',
463
+ cssClass: tabsNode.cssClass,
464
+ tabOptions: tabsNode.tabOptions,
465
+ tabItems: [],
466
+ _configured: true
467
+ };
468
+ // Process tabs recursively
469
+ tabsNode.tabs.forEach((tab) => {
470
+ tabsComponent.tabItems.push({
471
+ header: tab.header,
472
+ content: processLayoutNodes(tab.children) // Recursively process tab content
473
+ });
474
+ });
475
+ components.push(tabsComponent);
476
+ }
477
+ else if (node.type === 'card') {
478
+ const cardNode = node;
479
+ const cardComponent = {
480
+ id: cardNode.id,
481
+ type: 'card',
482
+ label: cardNode.label || 'Card',
483
+ cssClass: cardNode.cssClass,
484
+ hideLabel: cardNode.hideLabel,
485
+ children: processLayoutNodes(cardNode.children),
486
+ _configured: true
487
+ };
488
+ // Add card-specific properties
489
+ if (cardNode.cardTitle !== undefined)
490
+ cardComponent.cardTitle = cardNode.cardTitle;
491
+ if (cardNode.cardSubtitle !== undefined)
492
+ cardComponent.cardSubtitle = cardNode.cardSubtitle;
493
+ if (cardNode.enableAction !== undefined)
494
+ cardComponent.enableAction = cardNode.enableAction;
495
+ if (cardNode.actionButtonLabel !== undefined)
496
+ cardComponent.actionButtonLabel = cardNode.actionButtonLabel;
497
+ if (cardNode.actionButtonType !== undefined)
498
+ cardComponent.actionButtonType = cardNode.actionButtonType;
499
+ components.push(cardComponent);
500
+ }
501
+ }
502
+ return components;
503
+ };
504
+ // Process root layout nodes
505
+ let components;
506
+ if (!uiSchema.layout || uiSchema.layout.length === 0) {
507
+ // If layout is undefined or empty, generate a flat layout from data schema
508
+ //console.log('⚠️ UI Schema layout is undefined or empty, generating from data schema');
509
+ components = Object.entries(split.dataSchema.properties).map(([fieldKey, dataField]) => {
510
+ const uiField = uiSchema.properties[fieldKey];
511
+ return mergeField(dataField, uiField);
512
+ });
513
+ }
514
+ else {
515
+ components = processLayoutNodes(uiSchema.layout);
516
+ }
517
+ return {
518
+ version: split.dataSchema.version,
519
+ components
520
+ };
521
+ }
522
+ /**
523
+ * Merge a single field from data and UI schemas
524
+ */
525
+ function mergeField(dataField, uiField) {
526
+ // Check if options are present in the data field
527
+ const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
528
+ const component = {
529
+ id: dataField.id,
530
+ type: (uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, hasOptions)),
531
+ label: dataField.label || dataField.id,
532
+ };
533
+ // Copy data properties
534
+ if (dataField.defaultValue !== undefined)
535
+ component.defaultValue = dataField.defaultValue;
536
+ if (dataField.required !== undefined)
537
+ component.required = dataField.required;
538
+ if (dataField.content !== undefined)
539
+ component.content = dataField.content;
540
+ if (dataField.buttonType !== undefined)
541
+ component.buttonType = dataField.buttonType;
542
+ if (dataField.textboxType !== undefined)
543
+ component.textboxType = dataField.textboxType;
544
+ if (dataField.options !== undefined)
545
+ component.options = dataField.options;
546
+ if (dataField.minLength !== undefined)
547
+ component.minLength = dataField.minLength;
548
+ if (dataField.maxLength !== undefined)
549
+ component.maxLength = dataField.maxLength;
550
+ if (dataField.minValue !== undefined)
551
+ component.minValue = dataField.minValue;
552
+ if (dataField.maxValue !== undefined)
553
+ component.maxValue = dataField.maxValue;
554
+ if (dataField.minDate !== undefined)
555
+ component.minDate = dataField.minDate;
556
+ if (dataField.maxDate !== undefined)
557
+ component.maxDate = dataField.maxDate;
558
+ if (dataField.minTime !== undefined)
559
+ component.minTime = dataField.minTime;
560
+ if (dataField.maxTime !== undefined)
561
+ component.maxTime = dataField.maxTime;
562
+ if (dataField.pattern !== undefined)
563
+ component.regex = dataField.pattern;
564
+ // Copy UI properties (including _configured flag)
565
+ if (uiField) {
566
+ Object.keys(uiField).forEach(key => {
567
+ if (key !== 'widget' && component[key] === undefined) {
568
+ component[key] = uiField[key];
569
+ }
570
+ });
571
+ }
572
+ return component;
573
+ }
574
+ /**
575
+ * Infer widget type from semantic data type
576
+ */
577
+ export function inferWidgetFromType(type, format, hasOptions) {
578
+ if (type === 'string') {
579
+ if (format === 'date')
580
+ return 'datepicker';
581
+ if (format === 'date-time')
582
+ return 'datetimepicker';
583
+ if (format === 'time')
584
+ return 'timepicker';
585
+ if (format === 'email')
586
+ return 'email';
587
+ if (format === 'url')
588
+ return 'url';
589
+ // If options are present, default to dropdown for string types
590
+ if (hasOptions)
591
+ return 'dropdown';
592
+ return 'textbox';
593
+ }
594
+ if (type === 'number')
595
+ return 'number';
596
+ if (type === 'boolean')
597
+ return 'checkbox';
598
+ if (type === 'array') {
599
+ // For array type with options, use multiselect
600
+ return 'multiselect';
601
+ }
602
+ if (type === 'button')
603
+ return 'button';
604
+ if (type === 'message')
605
+ return 'message';
606
+ if (type === 'sign')
607
+ return 'signature';
608
+ if (type === 'image')
609
+ return 'imageeditor';
610
+ if (type === 'file')
611
+ return 'fileupload';
612
+ if (type === 'color')
613
+ return 'colorpicker';
614
+ if (type === 'grid')
615
+ return 'datagrid';
616
+ if (type === 'any')
617
+ return 'textbox';
618
+ return 'textbox';
619
+ }
620
+ /**
621
+ * Calculate Levenshtein distance for fuzzy string matching
622
+ */
623
+ function levenshteinDistance(str1, str2) {
624
+ const len1 = str1.length;
625
+ const len2 = str2.length;
626
+ const matrix = [];
627
+ // Initialize matrix
628
+ for (let i = 0; i <= len1; i++) {
629
+ matrix[i] = [i];
630
+ }
631
+ for (let j = 0; j <= len2; j++) {
632
+ matrix[0][j] = j;
633
+ }
634
+ // Fill matrix
635
+ for (let i = 1; i <= len1; i++) {
636
+ for (let j = 1; j <= len2; j++) {
637
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
638
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
639
+ matrix[i][j - 1] + 1, // insertion
640
+ matrix[i - 1][j - 1] + cost // substitution
641
+ );
642
+ }
643
+ }
644
+ return matrix[len1][len2];
645
+ }
646
+ /**
647
+ * Calculate similarity percentage between two strings
648
+ */
649
+ function calculateSimilarity(str1, str2) {
650
+ const distance = levenshteinDistance(str1.toLowerCase(), str2.toLowerCase());
651
+ const maxLen = Math.max(str1.length, str2.length);
652
+ if (maxLen === 0)
653
+ return 100;
654
+ return Math.round(((maxLen - distance) / maxLen) * 100);
655
+ }
656
+ /**
657
+ * Match a new data field with existing UI schema field
658
+ * Preserves existing ID for stability
659
+ */
660
+ export function matchFieldWithIDPreservation(newDataField, existingUISchema) {
661
+ // 1. Try exact ID match first
662
+ if (existingUISchema.properties[newDataField.id]) {
663
+ return {
664
+ existingId: newDataField.id,
665
+ newDataId: newDataField.id,
666
+ confidence: 'exact',
667
+ method: 'id',
668
+ similarity: 100
669
+ };
670
+ }
671
+ // 2. Try fuzzy match by ID (check if existing ID starts with new ID or vice versa)
672
+ let bestIdMatch = null;
673
+ for (const [existingId, existingUIField] of Object.entries(existingUISchema.properties)) {
674
+ // Skip if this is a layout container
675
+ if (['table', 'panel', 'tabs'].includes(existingUIField.widget)) {
676
+ continue;
677
+ }
678
+ // Check if one ID is a prefix of the other (common case: radio_123 vs radio_123_456)
679
+ const newId = newDataField.id || '';
680
+ const isPrefix = existingId.startsWith(newId + '_') || newId.startsWith(existingId + '_');
681
+ if (isPrefix) {
682
+ const similarity = Math.round((Math.min(newId.length, existingId.length) / Math.max(newId.length, existingId.length)) * 100);
683
+ if (!bestIdMatch || (bestIdMatch.similarity && similarity > bestIdMatch.similarity)) {
684
+ bestIdMatch = {
685
+ existingId: existingId, // PRESERVE EXISTING ID
686
+ newDataId: newDataField.id,
687
+ confidence: similarity >= 90 ? 'high' : similarity >= 70 ? 'medium' : 'low',
688
+ method: 'id',
689
+ similarity
690
+ };
691
+ }
692
+ }
693
+ else {
694
+ // Try Levenshtein distance for ID similarity
695
+ const idSimilarity = calculateSimilarity(existingId, newId);
696
+ // Only consider as potential match if very similar (≥85%)
697
+ if (idSimilarity >= 85) {
698
+ if (!bestIdMatch || (bestIdMatch.similarity && idSimilarity > bestIdMatch.similarity)) {
699
+ bestIdMatch = {
700
+ existingId: existingId,
701
+ newDataId: newDataField.id,
702
+ confidence: idSimilarity >= 90 ? 'high' : 'medium',
703
+ method: 'id',
704
+ similarity: idSimilarity
705
+ };
706
+ }
707
+ }
708
+ }
709
+ }
710
+ // If we found a good ID match, use it
711
+ if (bestIdMatch && bestIdMatch.similarity && bestIdMatch.similarity >= 70) {
712
+ //console.log(`ID fuzzy match: "${newDataField.id}" → "${bestIdMatch.existingId}" (${bestIdMatch.similarity}% similar)`);
713
+ return bestIdMatch;
714
+ }
715
+ // 3. Try fuzzy match by label
716
+ let bestLabelMatch = null;
717
+ for (const [existingId, existingUIField] of Object.entries(existingUISchema.properties)) {
718
+ // Skip if this is a layout container
719
+ if (['table', 'panel', 'tabs'].includes(existingUIField.widget)) {
720
+ continue;
721
+ }
722
+ // Get label from UI field or try to extract from ID
723
+ const existingLabel = existingUIField.label || existingId;
724
+ const newLabel = newDataField.label || newDataField.id;
725
+ if (!existingLabel || !newLabel)
726
+ continue;
727
+ const similarity = calculateSimilarity(existingLabel, newLabel);
728
+ // Update best match if this is better
729
+ if (!bestLabelMatch || (bestLabelMatch.similarity && similarity > bestLabelMatch.similarity)) {
730
+ bestLabelMatch = {
731
+ existingId: existingId, // PRESERVE EXISTING ID
732
+ newDataId: newDataField.id,
733
+ confidence: similarity >= 90 ? 'high' : similarity >= 70 ? 'medium' : 'low',
734
+ method: similarity === 100 ? 'label-exact' : 'label-fuzzy',
735
+ similarity
736
+ };
737
+ }
738
+ }
739
+ // 4. Return best label match if similarity is good enough (≥70%)
740
+ if (bestLabelMatch && bestLabelMatch.similarity && bestLabelMatch.similarity >= 70) {
741
+ //console.log(`Label fuzzy match: "${newDataField.label}" → "${bestLabelMatch.existingId}" (${bestLabelMatch.similarity}% similar)`);
742
+ return bestLabelMatch;
743
+ }
744
+ // 5. No match found - use new ID
745
+ return {
746
+ existingId: newDataField.id,
747
+ newDataId: newDataField.id,
748
+ confidence: 'exact',
749
+ method: 'new',
750
+ similarity: 100
751
+ };
752
+ }
753
+ /**
754
+ * Merge with ID remapping for re-import scenarios
755
+ * Preserves existing IDs to maintain layout stability
756
+ */
757
+ export function mergeWithIDRemapping(newDataSchema, existingUISchema) {
758
+ const idMap = new Map(); // newId → canonicalId
759
+ const matchResults = [];
760
+ // Build ID mapping table
761
+ Object.entries(newDataSchema.properties).forEach(([newId, dataField]) => {
762
+ const match = matchFieldWithIDPreservation(dataField, existingUISchema);
763
+ matchResults.push(match);
764
+ idMap.set(newId, match.existingId || generateId(labelToFieldKey(dataField.label || '')));
765
+ // Log remapping if IDs differ
766
+ if (match.newDataId !== match.existingId) {
767
+ //console.log(`ID remapped: ${match.newDataId} → ${match.existingId} (${match.method}, ${match.similarity}% similar)`);
768
+ }
769
+ });
770
+ // Remap all data field IDs to canonical IDs
771
+ const remappedDataFields = {};
772
+ Object.entries(newDataSchema.properties).forEach(([newId, dataField]) => {
773
+ const canonicalId = idMap.get(newId) || newId;
774
+ remappedDataFields[canonicalId] = {
775
+ ...dataField,
776
+ id: canonicalId // Use canonical ID
777
+ };
778
+ });
779
+ // Get existing field IDs from the layout
780
+ const existingFieldIds = extractFieldIdsFromLayout(existingUISchema.layout);
781
+ const newFieldIds = Object.keys(remappedDataFields).filter(id => !existingFieldIds.has(id));
782
+ //console.log(`📋 Existing fields in layout: ${existingFieldIds.size}, New fields: ${newFieldIds.length}`);
783
+ // ✅ CHECK IF SUBMIT BUTTON EXISTS: Prevent duplicate submit buttons
784
+ const hasExistingSubmitButton = Object.values(existingUISchema.properties).some((uiField) => uiField.widget === 'button');
785
+ // Filter out new submit buttons if one already exists
786
+ const filteredNewFieldIds = newFieldIds.filter(id => {
787
+ const dataField = remappedDataFields[id];
788
+ if ((dataField.type === 'button' && dataField.buttonType === 'submit') && hasExistingSubmitButton) {
789
+ //console.log(`⚠️ Skipping new button "${dataField.label}" - submit button already exists in layout`);
790
+ return false;
791
+ }
792
+ return true;
793
+ });
794
+ //console.log(`📋 New fields after filtering: ${filteredNewFieldIds.length}`);
795
+ // Update layout references to use canonical IDs
796
+ let remappedLayout = remapLayoutIds(existingUISchema.layout, idMap);
797
+ // Create UI properties for new fields that don't have them yet
798
+ // ✅ Ensure all existing UI properties have _configured flag (add if missing from imported JSON)
799
+ const enhancedUIProperties = {};
800
+ Object.entries(existingUISchema.properties).forEach(([key, uiField]) => {
801
+ enhancedUIProperties[key] = {
802
+ ...uiField,
803
+ _configured: uiField._configured !== undefined ? uiField._configured : true
804
+ };
805
+ });
806
+ if (filteredNewFieldIds.length > 0) {
807
+ //console.log(`➕ Adding ${filteredNewFieldIds.length} new fields to layout`);
808
+ // Check if submit button exists and is at the last position (not moved by user)
809
+ const lastNode = remappedLayout[remappedLayout.length - 1];
810
+ const isSubmitAtEnd = lastNode?.type === 'field' &&
811
+ enhancedUIProperties[lastNode.propertyId]?.widget === 'button';
812
+ const newFieldNodes = [];
813
+ // Generate default UI properties for new fields
814
+ filteredNewFieldIds.forEach(id => {
815
+ const dataField = remappedDataFields[id];
816
+ const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
817
+ const widget = inferWidgetFromType(dataField.type, dataField.format, hasOptions);
818
+ // ✅ Add default button styling
819
+ const uiProperties = {
820
+ widget,
821
+ label: dataField.label,
822
+ _configured: true
823
+ };
824
+ // Add primary styling for buttons
825
+ if (widget === 'button') {
826
+ uiProperties.isPrimary = true;
827
+ uiProperties.cssClass = 'e-primary';
828
+ }
829
+ enhancedUIProperties[id] = uiProperties;
830
+ newFieldNodes.push({
831
+ type: 'field',
832
+ propertyId: id
833
+ });
834
+ });
835
+ // Apply same logic as App.tsx: insert before submit if it's at the end
836
+ if (isSubmitAtEnd) {
837
+ // Insert new fields BEFORE the submit button
838
+ remappedLayout = [...remappedLayout.slice(0, -1), ...newFieldNodes, remappedLayout[remappedLayout.length - 1]];
839
+ //console.log(`📐 Layout rebuilt: new fields inserted before submit button at end`);
840
+ }
841
+ else {
842
+ // Submit moved or doesn't exist - add new fields at end
843
+ remappedLayout = [...remappedLayout, ...newFieldNodes];
844
+ //console.log(`📐 Layout rebuilt: new fields added at end`);
845
+ }
846
+ }
847
+ // Merge with remapped data
848
+ const mergedSchema = mergeSchema({
849
+ dataSchema: {
850
+ version: newDataSchema.version,
851
+ properties: remappedDataFields
852
+ },
853
+ uiSchema: {
854
+ properties: enhancedUIProperties,
855
+ layout: remappedLayout
856
+ }
857
+ });
858
+ // Log summary
859
+ //const remappedCount = matchResults.filter(m => m.newDataId !== m.existingId).length;
860
+ //const newFieldsCount = matchResults.filter(m => m.method === 'new').length;
861
+ //console.log(`Merge complete: ${matchResults.length} fields, ${remappedCount} remapped, ${newFieldsCount} new`);
862
+ return mergedSchema;
863
+ }
864
+ /**
865
+ * Recursively remap IDs in layout tree
866
+ */
867
+ function remapLayoutIds(layout, idMap) {
868
+ return layout.map(node => {
869
+ if (node.type === 'field') {
870
+ const fieldNode = node;
871
+ const canonicalId = idMap.get(fieldNode.propertyId) || fieldNode.propertyId;
872
+ return {
873
+ ...fieldNode,
874
+ propertyId: canonicalId
875
+ };
876
+ }
877
+ else if (node.type === 'table') {
878
+ const tableNode = node;
879
+ return {
880
+ ...tableNode,
881
+ cells: tableNode.cells.map(row => row.map(cell => ({
882
+ ...cell,
883
+ children: remapLayoutIds(cell.children, idMap)
884
+ })))
885
+ };
886
+ }
887
+ else if (node.type === 'panel') {
888
+ const panelNode = node;
889
+ return {
890
+ ...panelNode,
891
+ children: remapLayoutIds(panelNode.children, idMap)
892
+ };
893
+ }
894
+ else if (node.type === 'tabs') {
895
+ const tabsNode = node;
896
+ return {
897
+ ...tabsNode,
898
+ tabs: tabsNode.tabs.map(tab => ({
899
+ ...tab,
900
+ children: remapLayoutIds(tab.children, idMap)
901
+ }))
902
+ };
903
+ }
904
+ else if (node.type === 'card') {
905
+ const cardNode = node;
906
+ return {
907
+ ...cardNode,
908
+ children: remapLayoutIds(cardNode.children, idMap)
909
+ };
910
+ }
911
+ return node;
912
+ });
913
+ }
914
+ // ============================================================================
915
+ // UTILITY FUNCTIONS
916
+ // ============================================================================
917
+ /**
918
+ * Extract all field IDs from layout tree
919
+ */
920
+ export function extractFieldIdsFromLayout(layout) {
921
+ const fieldIds = new Set();
922
+ const stack = [...layout];
923
+ while (stack.length > 0) {
924
+ const node = stack.shift();
925
+ if (node.type === 'field') {
926
+ fieldIds.add(node.propertyId);
927
+ }
928
+ else if (node.type === 'table') {
929
+ node.cells.forEach(row => row.forEach(cell => stack.push(...cell.children)));
930
+ }
931
+ else if (node.type === 'panel') {
932
+ stack.push(...node.children);
933
+ }
934
+ else if (node.type === 'tabs') {
935
+ node.tabs.forEach(tab => stack.push(...tab.children));
936
+ }
937
+ else if (node.type === 'card') {
938
+ stack.push(...node.children);
939
+ }
940
+ }
941
+ return fieldIds;
942
+ }
943
+ /**
944
+ * Find unmatched fields (in data schema but not in layout)
945
+ */
946
+ export function findUnmatchedFields(dataSchema, layout) {
947
+ const layoutFieldIds = extractFieldIdsFromLayout(layout);
948
+ const dataFieldIds = Object.keys(dataSchema.properties);
949
+ return dataFieldIds.filter(id => !layoutFieldIds.has(id));
950
+ }
951
+ // ============================================================================
952
+ // PARSER OUTPUT CONVERSION
953
+ // ============================================================================
954
+ /**
955
+ * Convert parser output (FormComponent[]) to DataSchema format
956
+ * This allows parser results to be merged with existing UI schemas
957
+ */
958
+ export function convertComponentsToDataSchema(components) {
959
+ const properties = {};
960
+ const seenComponentIds = new Set(); // Track component IDs to prevent duplicates
961
+ // Flatten all components (including nested ones)
962
+ const stack = [...components];
963
+ while (stack.length > 0) {
964
+ const comp = stack.shift();
965
+ // Infer semantic type from component
966
+ const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[comp.type];
967
+ // Skip presentation-only widgets (button, message, etc.) - they have null semantic type
968
+ if (semanticType === null) {
969
+ // Still process nested children for layout containers
970
+ if (comp.children?.length) {
971
+ stack.unshift(...comp.children);
972
+ }
973
+ if (comp.tabItems?.length) {
974
+ for (const tab of comp.tabItems) {
975
+ if (tab.content?.length) {
976
+ stack.unshift(...tab.content);
977
+ }
978
+ }
979
+ }
980
+ if (comp.tableCells?.length) {
981
+ for (const row of comp.tableCells) {
982
+ for (const cell of row) {
983
+ if (cell.length) {
984
+ stack.unshift(...cell);
985
+ }
986
+ }
987
+ }
988
+ }
989
+ continue; // Skip adding to data schema
990
+ }
991
+ // ✅ DEDUPLICATION: Skip if we've already processed this component ID
992
+ if (seenComponentIds.has(comp.id)) {
993
+ //console.log(`⚠️ Skipping duplicate component: ${comp.id} (${comp.label})`);
994
+ // Still process nested children
995
+ if (comp.children?.length) {
996
+ stack.unshift(...comp.children);
997
+ }
998
+ if (comp.tabItems?.length) {
999
+ for (const tab of comp.tabItems) {
1000
+ if (tab.content?.length) {
1001
+ stack.unshift(...tab.content);
1002
+ }
1003
+ }
1004
+ }
1005
+ if (comp.tableCells?.length) {
1006
+ for (const row of comp.tableCells) {
1007
+ for (const cell of row) {
1008
+ if (cell.length) {
1009
+ stack.unshift(...cell);
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ continue;
1015
+ }
1016
+ seenComponentIds.add(comp.id);
1017
+ // Generate fieldKey from label or ID
1018
+ const baseFieldKey = labelToFieldKey(comp.label || comp.id, comp.id);
1019
+ const fieldKey = makeUniqueFieldKey(baseFieldKey, new Set(Object.keys(properties)), comp.id);
1020
+ const dataField = extractDataField(comp, semanticType || { type: 'any' });
1021
+ properties[fieldKey] = dataField;
1022
+ // Process nested children
1023
+ if (comp.children?.length) {
1024
+ stack.unshift(...comp.children);
1025
+ }
1026
+ // Process tab items
1027
+ if (comp.tabItems?.length) {
1028
+ for (const tab of comp.tabItems) {
1029
+ if (tab.content?.length) {
1030
+ stack.unshift(...tab.content);
1031
+ }
1032
+ }
1033
+ }
1034
+ // Process table cells
1035
+ if (comp.tableCells?.length) {
1036
+ for (const row of comp.tableCells) {
1037
+ for (const cell of row) {
1038
+ // cell is FormComponent[], not an object with .components
1039
+ if (cell.length) {
1040
+ stack.unshift(...cell);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ return {
1047
+ version: '1.0',
1048
+ properties
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Convert parser output to a simple linear layout
1053
+ * Used when no existing UI schema is present
1054
+ */
1055
+ export function convertComponentsToUISchema(components) {
1056
+ const properties = {};
1057
+ const layout = [];
1058
+ // Helper to recursively process components and build layout
1059
+ function processComponent(comp) {
1060
+ // Extract UI field
1061
+ const uiField = extractUIField(comp);
1062
+ properties[comp.id] = uiField;
1063
+ // Handle different component types
1064
+ if (comp.type === 'table' && comp.tableCells) {
1065
+ const tableCells = comp.tableCells.map((row, rowIndex) => row.map((cellComponents, colIndex) => ({
1066
+ row: rowIndex,
1067
+ col: colIndex,
1068
+ // cellComponents is FormComponent[], so map over it directly
1069
+ children: cellComponents.map(processComponent)
1070
+ })));
1071
+ return {
1072
+ type: 'table',
1073
+ id: comp.id,
1074
+ label: comp.label,
1075
+ cssClass: comp.cssClass,
1076
+ hideBorders: comp.hideBorders,
1077
+ rows: comp.tableCells.length,
1078
+ cols: comp.tableCells[0]?.length || 0,
1079
+ cells: tableCells,
1080
+ columnWidths: comp.columnWidths
1081
+ };
1082
+ }
1083
+ if (comp.type === 'panel' && comp.children) {
1084
+ return {
1085
+ type: 'panel',
1086
+ id: comp.id,
1087
+ label: comp.label,
1088
+ cssClass: comp.cssClass,
1089
+ hideBorders: comp.hideBorders,
1090
+ hideLabel: comp.hideLabel,
1091
+ children: comp.children.map(processComponent)
1092
+ };
1093
+ }
1094
+ if (comp.type === 'tabs' && comp.tabItems) {
1095
+ // tabs is an array of { header, children }
1096
+ const tabs = comp.tabItems.map((tab) => ({
1097
+ header: tab.header,
1098
+ children: tab.content?.map(processComponent) || []
1099
+ }));
1100
+ return {
1101
+ type: 'tabs',
1102
+ id: comp.id,
1103
+ cssClass: comp.cssClass,
1104
+ tabs
1105
+ };
1106
+ }
1107
+ if (comp.type === 'card' && comp.children) {
1108
+ return {
1109
+ type: 'card',
1110
+ id: comp.id,
1111
+ label: comp.label,
1112
+ cssClass: comp.cssClass,
1113
+ hideLabel: comp.hideLabel,
1114
+ cardTitle: comp.cardTitle,
1115
+ cardSubtitle: comp.cardSubtitle,
1116
+ enableAction: comp.enableAction,
1117
+ actionButtonLabel: comp.actionButtonLabel,
1118
+ actionButtonType: comp.actionButtonType,
1119
+ children: comp.children.map(processComponent)
1120
+ };
1121
+ }
1122
+ // Regular field
1123
+ return {
1124
+ type: 'field',
1125
+ propertyId: comp.id
1126
+ };
1127
+ }
1128
+ // Process all root components
1129
+ for (const comp of components) {
1130
+ layout.push(processComponent(comp));
1131
+ }
1132
+ return {
1133
+ properties,
1134
+ layout
1135
+ };
1136
+ }
1137
+ // ============================================================================
1138
+ // RENDERER HELPER FUNCTIONS
1139
+ // ============================================================================
1140
+ /**
1141
+ * Generate default UI properties for each data field
1142
+ * Used by renderers when only data schema is provided
1143
+ */
1144
+ export function generateUIPropertiesFromData(dataSchema) {
1145
+ const properties = {};
1146
+ Object.entries(dataSchema.properties).forEach(([fieldId, dataField]) => {
1147
+ const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
1148
+ const widget = inferWidgetFromType(dataField.type, dataField.format, hasOptions);
1149
+ properties[fieldId] = { widget };
1150
+ });
1151
+ return properties;
1152
+ }
1153
+ /**
1154
+ * Generate sequential layout from data schema
1155
+ * Used by renderers when only data schema is provided
1156
+ */
1157
+ export function generateLayoutFromData(dataSchema) {
1158
+ return Object.keys(dataSchema.properties).map(fieldId => ({
1159
+ type: 'field',
1160
+ propertyId: fieldId
1161
+ }));
1162
+ }
1163
+ /**
1164
+ * Generate minimal data schema from UI schema
1165
+ * Used by renderers when only UI schema is provided
1166
+ */
1167
+ export function generateDataSchemaFromUI(uiSchema) {
1168
+ const dataProperties = {};
1169
+ Object.entries(uiSchema.properties).forEach(([fieldId, uiField]) => {
1170
+ dataProperties[fieldId] = {
1171
+ id: fieldId,
1172
+ type: 'string',
1173
+ label: uiField.label || fieldId
1174
+ };
1175
+ });
1176
+ return {
1177
+ version: '1.0.0-beta',
1178
+ properties: dataProperties
1179
+ };
1180
+ }