@syncfusion/ej2-react-form-renderer 0.1.0-beta → 0.1.2-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.
@@ -6,6 +6,10 @@ export const generateId = (type) => {
6
6
  const random = Math.floor(Math.random() * 1000);
7
7
  return `${type}_${timestamp}_${random}`;
8
8
  };
9
+ export const generateName = (type) => {
10
+ const random = Math.floor(Math.random() * 1000);
11
+ return `${type}_${random}`;
12
+ };
9
13
  // ============================================================================
10
14
  // LABEL TO CAMELCASE CONVERTER
11
15
  // ============================================================================
@@ -68,7 +72,7 @@ export function makeUniqueFieldKey(baseKey, existingKeys, originalId) {
68
72
  // ============================================================================
69
73
  // UI TYPE TO SEMANTIC TYPE MAPPING
70
74
  // ============================================================================
71
- const UI_TYPE_TO_SEMANTIC_TYPE = {
75
+ export const UI_TYPE_TO_SEMANTIC_TYPE = {
72
76
  // String types
73
77
  'textbox': { type: 'string' },
74
78
  'textarea': { type: 'string' },
@@ -76,36 +80,36 @@ const UI_TYPE_TO_SEMANTIC_TYPE = {
76
80
  'url': { type: 'string', format: 'url' },
77
81
  'phone': { type: 'string' },
78
82
  'password': { type: 'string' },
79
- 'inputmask': { type: 'string' },
83
+ 'inputMask': { type: 'string' },
80
84
  // Numeric types
81
85
  'number': { type: 'number' },
82
- 'rangeslider': { type: 'number' },
86
+ 'rangeSlider': { type: 'number' },
83
87
  'rating': { type: 'number' },
84
88
  // Boolean types
85
89
  'checkbox': { type: 'boolean' },
86
90
  'switch': { type: 'boolean' },
87
91
  // 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
+ 'date': { type: 'string', format: 'date' },
93
+ 'dateTime': { type: 'string', format: 'date-time' },
94
+ 'time': { type: 'string', format: 'time' },
95
+ 'dateRange': { type: 'string', format: 'date' },
92
96
  // Array types
93
97
  'multiselect': { type: 'array' },
94
- 'checkboxgroup': { type: 'array' },
98
+ 'checkboxGroup': { type: 'array' },
95
99
  // Single-value selection (stored as string)
96
100
  'dropdown': { type: 'string' },
97
101
  'radio': { type: 'string' },
98
- 'splitbutton': { type: 'string' },
102
+ 'splitButton': { type: 'string' },
99
103
  // Special types (excluded from data schema)
100
104
  'button': { type: 'button' },
101
105
  'message': { type: 'message' },
102
106
  'signature': { type: 'sign' },
103
- 'imageeditor': { type: 'image' },
104
- 'fileupload': { type: 'file' },
105
- 'colorpicker': { type: 'color' },
106
- 'datagrid': { type: 'grid' },
107
+ 'imageEditor': { type: 'image' },
108
+ 'fileUpload': { type: 'file' },
109
+ 'colorPicker': { type: 'color' },
110
+ 'dataGrid': { type: 'grid' },
107
111
  // Layout types (excluded from data schema)
108
- 'statichtml': { type: 'string' },
112
+ 'staticHtml': { type: 'string' },
109
113
  'table': null,
110
114
  'panel': null,
111
115
  'tabs': null,
@@ -126,6 +130,73 @@ function stripConfiguredFlag(properties) {
126
130
  return out;
127
131
  }
128
132
  // ============================================================================
133
+ // CONDITION VALIDATION HELPERS
134
+ // ============================================================================
135
+ /**
136
+ * Clean condition rules that reference non-existent field IDs
137
+ *
138
+ * Validates condition rules during split to remove references to deleted fields.
139
+ * Returns undefined if condition becomes empty after cleanup.
140
+ *
141
+ * @param condition - Condition object (visibleWhen/readOnlyWhen/disabledWhen)
142
+ * @param validFieldIds - Set of valid field IDs in current schema
143
+ * @returns Cleaned condition or undefined if empty
144
+ */
145
+ function cleanConditionRules(condition, validFieldIds) {
146
+ if (!condition || !condition.rules || !Array.isArray(condition.rules)) {
147
+ return undefined;
148
+ }
149
+ const cleanedRules = condition.rules
150
+ .map((rule) => {
151
+ // If rule has nested rules (group), recursively clean
152
+ if (rule.rules && Array.isArray(rule.rules)) {
153
+ return cleanConditionRules(rule, validFieldIds);
154
+ }
155
+ // If rule references non-existent field, exclude it
156
+ if (rule.field && !validFieldIds.has(rule.field)) {
157
+ return null;
158
+ }
159
+ return rule;
160
+ })
161
+ .filter((rule) => rule !== null && rule !== undefined);
162
+ // If no rules remain, return undefined
163
+ if (cleanedRules.length === 0) {
164
+ return undefined;
165
+ }
166
+ return {
167
+ ...condition,
168
+ rules: cleanedRules
169
+ };
170
+ }
171
+ /**
172
+ * Get all valid field IDs from components
173
+ *
174
+ * @param components - Form components array
175
+ * @returns Set of field IDs
176
+ */
177
+ function getAllComponentIds(components) {
178
+ const ids = new Set();
179
+ const stack = [...components];
180
+ while (stack.length) {
181
+ const comp = stack.pop();
182
+ ids.add(comp.id);
183
+ if (comp.children)
184
+ stack.push(...comp.children);
185
+ if (comp.tabItems) {
186
+ comp.tabItems.forEach(tab => {
187
+ if (tab.content)
188
+ stack.push(...tab.content);
189
+ });
190
+ }
191
+ if (comp.tableCells) {
192
+ comp.tableCells.forEach(row => {
193
+ row.forEach(cell => stack.push(...cell));
194
+ });
195
+ }
196
+ }
197
+ return ids;
198
+ }
199
+ // ============================================================================
129
200
  // SPLIT ALGORITHM
130
201
  // ============================================================================
131
202
  export function splitSchema(heartSchema) {
@@ -134,6 +205,8 @@ export function splitSchema(heartSchema) {
134
205
  const layout = [];
135
206
  const usedFieldKeys = new Set(); // Track unique field keys
136
207
  const seenComponentIds = new Set(); // Track component IDs to prevent duplicates
208
+ // ✅ Get all valid field IDs for condition validation
209
+ const validFieldIds = getAllComponentIds(heartSchema.components);
137
210
  // Stack for iterative traversal (avoid recursion)
138
211
  const stack = heartSchema.components.map(c => ({
139
212
  component: c,
@@ -142,6 +215,86 @@ export function splitSchema(heartSchema) {
142
215
  while (stack.length > 0) {
143
216
  // Use shift() instead of pop() to maintain order (FIFO instead of LIFO)
144
217
  const { component, layoutParent } = stack.shift();
218
+ // ✅ Clean conditions before processing
219
+ if (component.conditions) {
220
+ const cleanedConditions = {};
221
+ let hasValidConditions = false;
222
+ if (component.conditions.visibleWhen) {
223
+ const cleaned = cleanConditionRules(component.conditions.visibleWhen, validFieldIds);
224
+ if (cleaned) {
225
+ cleanedConditions.visibleWhen = cleaned;
226
+ hasValidConditions = true;
227
+ }
228
+ }
229
+ if (component.conditions.hideWhen) {
230
+ const cleaned = cleanConditionRules(component.conditions.hideWhen, validFieldIds);
231
+ if (cleaned) {
232
+ cleanedConditions.hideWhen = cleaned;
233
+ hasValidConditions = true;
234
+ }
235
+ }
236
+ if (component.conditions.readOnlyWhen) {
237
+ const cleaned = cleanConditionRules(component.conditions.readOnlyWhen, validFieldIds);
238
+ if (cleaned) {
239
+ cleanedConditions.readOnlyWhen = cleaned;
240
+ hasValidConditions = true;
241
+ }
242
+ }
243
+ if (component.conditions.disabledWhen) {
244
+ const cleaned = cleanConditionRules(component.conditions.disabledWhen, validFieldIds);
245
+ if (cleaned) {
246
+ cleanedConditions.disabledWhen = cleaned;
247
+ hasValidConditions = true;
248
+ }
249
+ }
250
+ if (component.conditions.requiredWhen) {
251
+ const cleaned = cleanConditionRules(component.conditions.requiredWhen, validFieldIds);
252
+ if (cleaned) {
253
+ cleanedConditions.requiredWhen = cleaned;
254
+ hasValidConditions = true;
255
+ }
256
+ }
257
+ if (component.conditions.setValueWhen) {
258
+ const cleaned = cleanConditionRules(component.conditions.setValueWhen.condition, validFieldIds);
259
+ if (cleaned && component.conditions.setValueWhen.value !== undefined && component.conditions.setValueWhen.value !== '') {
260
+ cleanedConditions.setValueWhen = {
261
+ condition: cleaned,
262
+ value: component.conditions.setValueWhen.value
263
+ };
264
+ hasValidConditions = true;
265
+ }
266
+ }
267
+ if (component.conditions?.choiceBasedField) {
268
+ const rule = component.conditions.choiceBasedField;
269
+ // Validate that primary field exists
270
+ if (rule.primaryFieldId && validFieldIds.has(rule.primaryFieldId)) {
271
+ cleanedConditions.choiceBasedField = rule;
272
+ hasValidConditions = true;
273
+ }
274
+ }
275
+ if (component.conditions.conditionalData && Array.isArray(component.conditions.conditionalData)) {
276
+ const cleanedDataRules = component.conditions.conditionalData
277
+ .map((rule) => {
278
+ const cleaned = cleanConditionRules(rule.condition, validFieldIds);
279
+ if (cleaned && rule.dataSource) {
280
+ return {
281
+ condition: cleaned,
282
+ dataSource: rule.dataSource,
283
+ columns: rule.columns,
284
+ includeColumns: rule.includeColumns
285
+ };
286
+ }
287
+ return null;
288
+ })
289
+ .filter((rule) => rule !== null && rule !== undefined);
290
+ if (cleanedDataRules.length > 0) {
291
+ cleanedConditions.conditionalData = cleanedDataRules;
292
+ hasValidConditions = true;
293
+ }
294
+ }
295
+ // Update component conditions (or remove if empty)
296
+ component.conditions = hasValidConditions ? cleanedConditions : undefined;
297
+ }
145
298
  // Get semantic type mapping (may be null for presentation-only widgets)
146
299
  const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[component.type];
147
300
  // Handle layout containers
@@ -149,13 +302,15 @@ export function splitSchema(heartSchema) {
149
302
  const tableNode = {
150
303
  type: 'table',
151
304
  id: component.id,
305
+ name: component.name,
152
306
  label: component.label,
153
307
  cssClass: component.cssClass,
154
308
  hideBorders: component.hideBorders,
155
309
  rows: component.tableCells?.length || 0,
156
310
  cols: component.tableCells?.[0]?.length || 0,
157
311
  cells: [],
158
- columnWidths: component.columnWidths
312
+ columnWidths: component.columnWidths,
313
+ ...(component.conditions ? { conditions: component.conditions } : {})
159
314
  };
160
315
  // Process table cells
161
316
  component.tableCells?.forEach((row, rowIndex) => {
@@ -177,11 +332,13 @@ export function splitSchema(heartSchema) {
177
332
  const panelNode = {
178
333
  type: 'panel',
179
334
  id: component.id,
335
+ name: component.name,
180
336
  label: component.label,
181
337
  cssClass: component.cssClass,
182
338
  hideBorders: component.hideBorders,
183
339
  hideLabel: component.hideLabel,
184
- children: []
340
+ children: [],
341
+ ...(component.conditions ? { conditions: component.conditions } : {})
185
342
  };
186
343
  // Process panel children
187
344
  // Reverse children and use unshift() to maintain order with depth-first processing
@@ -194,11 +351,13 @@ export function splitSchema(heartSchema) {
194
351
  if (component.type === 'tabs') {
195
352
  const tabsNode = {
196
353
  type: 'tabs',
354
+ name: component.name,
197
355
  id: component.id,
198
356
  label: component.label,
199
357
  cssClass: component.cssClass,
200
358
  tabOptions: component.tabOptions,
201
- tabs: []
359
+ tabs: [],
360
+ ...(component.conditions ? { conditions: component.conditions } : {})
202
361
  };
203
362
  // Process tabs
204
363
  component.tabItems?.forEach((tab) => {
@@ -216,6 +375,7 @@ export function splitSchema(heartSchema) {
216
375
  const cardNode = {
217
376
  type: 'card',
218
377
  id: component.id,
378
+ name: component.name,
219
379
  label: component.label,
220
380
  cssClass: component.cssClass,
221
381
  hideLabel: component.hideLabel,
@@ -224,7 +384,8 @@ export function splitSchema(heartSchema) {
224
384
  enableAction: component.enableAction,
225
385
  actionButtonLabel: component.actionButtonLabel,
226
386
  actionButtonType: component.actionButtonType,
227
- children: []
387
+ children: [],
388
+ ...(component.conditions ? { conditions: component.conditions } : {})
228
389
  };
229
390
  // Process card children
230
391
  // Reverse children and use unshift() to maintain order with depth-first processing
@@ -253,20 +414,29 @@ export function splitSchema(heartSchema) {
253
414
  // Add property reference to layout (use fieldKey instead of component ID)
254
415
  layoutParent.push({ type: 'field', propertyId: fieldKey });
255
416
  }
417
+ // ✅ Merge data and UI fields into unified properties
418
+ const unifiedProperties = {};
419
+ Object.keys(dataFields).forEach(fieldKey => {
420
+ const dataField = dataFields[fieldKey];
421
+ const uiField = uiFields[fieldKey];
422
+ unifiedProperties[fieldKey] = {
423
+ ...dataField,
424
+ ...uiField,
425
+ // Ensure widget is always set
426
+ widget: uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, dataField.options !== undefined)
427
+ };
428
+ });
256
429
  return {
257
- dataSchema: {
258
- version: heartSchema.version,
259
- properties: dataFields
260
- },
261
- uiSchema: {
262
- properties: stripConfiguredFlag(uiFields),
263
- layout
264
- }
430
+ version: heartSchema.version,
431
+ properties: stripConfiguredFlag(unifiedProperties),
432
+ layout,
433
+ ...(heartSchema.settings ? { settings: heartSchema.settings } : {})
265
434
  };
266
435
  }
267
436
  function extractDataField(component, semanticType) {
268
437
  const dataField = {
269
438
  id: component.id,
439
+ name: component.name,
270
440
  type: semanticType.type,
271
441
  };
272
442
  // Add format if present
@@ -278,6 +448,8 @@ function extractDataField(component, semanticType) {
278
448
  dataField.label = component.label;
279
449
  if (component.defaultValue !== undefined)
280
450
  dataField.defaultValue = component.defaultValue;
451
+ if (component.expressionValue !== undefined)
452
+ dataField.expressionValue = component.expressionValue;
281
453
  if (component.content !== undefined)
282
454
  dataField.content = component.content;
283
455
  if (component.buttonType !== undefined)
@@ -324,7 +496,7 @@ function extractUIField(component) {
324
496
  };
325
497
  // Copy all properties EXCEPT those in data schema allowed list
326
498
  const dataSchemaProps = new Set([
327
- 'id', 'type', 'label', 'format', 'defaultValue', 'required', 'options',
499
+ 'id', 'type', 'label', 'format', 'defaultValue', 'required', 'options', 'name', 'expressionValue',
328
500
  'minLength', 'maxLength', 'minimum', 'maximum', 'minValue', 'maxValue',
329
501
  'minDate', 'maxDate', 'minTime', 'maxTime', 'regex', 'pattern', 'content', 'buttonType', 'textboxType'
330
502
  ]);
@@ -333,8 +505,8 @@ function extractUIField(component) {
333
505
  uiField[key] = component[key];
334
506
  }
335
507
  });
336
- // Special handling: Filter DataGrid columns to keep only required properties
337
- if (component.type === 'datagrid' && uiField.columns && Array.isArray(uiField.columns)) {
508
+ // Special handling: Filter dataGrid columns to keep only required properties
509
+ if (component.type === 'dataGrid' && uiField.columns && Array.isArray(uiField.columns)) {
338
510
  const requiredColumnProps = [
339
511
  'field',
340
512
  'headerText',
@@ -360,30 +532,15 @@ function extractUIField(component) {
360
532
  // MERGE ALGORITHM
361
533
  // ============================================================================
362
534
  /**
363
- * Merge split schemas back into heart schema
364
- * Supports both simple merge and fuzzy matching for re-import scenarios
535
+ * Merge unified schema back into heart schema
536
+ * Reconstructs FormComponent array from unified properties and layout
365
537
  */
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
- };
538
+ export function mergeSchema(unified) {
539
+ // Create lookup map for O(1) access to unified properties
540
+ const propertyMap = new Map();
541
+ Object.entries(unified.properties).forEach(([id, field]) => {
542
+ propertyMap.set(id, field);
381
543
  });
382
- // Use enhanced UI schema with _configured flags
383
- uiSchema = {
384
- ...uiSchema,
385
- properties: enhancedUIProperties
386
- };
387
544
  // Track processed field IDs to prevent duplicates
388
545
  const processedFieldIds = new Set();
389
546
  // Recursive function to process layout nodes
@@ -394,30 +551,31 @@ export function mergeSchema(split, existingUISchema) {
394
551
  const fieldNode = node;
395
552
  // ✅ DEDUPLICATION: Skip if we've already processed this field
396
553
  if (processedFieldIds.has(fieldNode.propertyId)) {
397
- //console.log(`⚠️ [mergeSchema] Skipping duplicate field: ${fieldNode.propertyId}`);
398
554
  continue;
399
555
  }
400
556
  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 = {
557
+ const unifiedField = propertyMap.get(fieldNode.propertyId);
558
+ if (!unifiedField) {
559
+ console.warn(`Field ${fieldNode.propertyId} in layout not found in unified schema`);
560
+ // Generate fallback component
561
+ const fallbackComponent = {
407
562
  id: fieldNode.propertyId,
408
- type: 'any',
409
- label: uiField?.widget || 'Unknown Field'
563
+ name: generateName(''),
564
+ type: 'textbox',
565
+ label: 'Unknown Field',
566
+ _configured: true
410
567
  };
411
- components.push(mergeField(fallbackDataField, uiField));
568
+ components.push(fallbackComponent);
412
569
  }
413
570
  else {
414
- components.push(mergeField(dataField, uiField));
571
+ components.push(mergeUnifiedField(unifiedField));
415
572
  }
416
573
  }
417
574
  else if (node.type === 'table') {
418
575
  const tableNode = node;
419
576
  const tableComponent = {
420
577
  id: tableNode.id,
578
+ name: tableNode.name,
421
579
  type: 'table',
422
580
  label: tableNode.label || 'Table',
423
581
  cssClass: tableNode.cssClass,
@@ -426,13 +584,13 @@ export function mergeSchema(split, existingUISchema) {
426
584
  columns: tableNode.cols,
427
585
  columnWidths: tableNode.columnWidths,
428
586
  tableCells: [],
429
- _configured: true
587
+ _configured: true,
588
+ ...(tableNode.conditions ? { conditions: tableNode.conditions } : {})
430
589
  };
431
590
  // Process table cells recursively
432
591
  tableNode.cells.forEach((row) => {
433
592
  const cellRow = [];
434
593
  row.forEach((cell) => {
435
- // Recursively process children in each cell
436
594
  const cellComponents = processLayoutNodes(cell.children);
437
595
  cellRow.push(cellComponents);
438
596
  });
@@ -445,12 +603,14 @@ export function mergeSchema(split, existingUISchema) {
445
603
  const panelComponent = {
446
604
  id: panelNode.id,
447
605
  type: 'panel',
606
+ name: panelNode.name,
448
607
  cssClass: panelNode.cssClass,
449
608
  hideBorders: panelNode.hideBorders,
450
609
  hideLabel: panelNode.hideLabel,
451
610
  label: panelNode.label || 'Panel',
452
611
  children: processLayoutNodes(panelNode.children),
453
- _configured: true
612
+ _configured: true,
613
+ ...(panelNode.conditions ? { conditions: panelNode.conditions } : {})
454
614
  };
455
615
  components.push(panelComponent);
456
616
  }
@@ -459,17 +619,19 @@ export function mergeSchema(split, existingUISchema) {
459
619
  const tabsComponent = {
460
620
  id: tabsNode.id,
461
621
  type: 'tabs',
622
+ name: tabsNode.name,
462
623
  label: tabsNode.label || 'Tabs',
463
624
  cssClass: tabsNode.cssClass,
464
625
  tabOptions: tabsNode.tabOptions,
465
626
  tabItems: [],
466
- _configured: true
627
+ _configured: true,
628
+ ...(tabsNode.conditions ? { conditions: tabsNode.conditions } : {})
467
629
  };
468
630
  // Process tabs recursively
469
631
  tabsNode.tabs.forEach((tab) => {
470
632
  tabsComponent.tabItems.push({
471
633
  header: tab.header,
472
- content: processLayoutNodes(tab.children) // Recursively process tab content
634
+ content: processLayoutNodes(tab.children)
473
635
  });
474
636
  });
475
637
  components.push(tabsComponent);
@@ -479,11 +641,13 @@ export function mergeSchema(split, existingUISchema) {
479
641
  const cardComponent = {
480
642
  id: cardNode.id,
481
643
  type: 'card',
644
+ name: cardNode.name,
482
645
  label: cardNode.label || 'Card',
483
646
  cssClass: cardNode.cssClass,
484
647
  hideLabel: cardNode.hideLabel,
485
648
  children: processLayoutNodes(cardNode.children),
486
- _configured: true
649
+ _configured: true,
650
+ ...(cardNode.conditions ? { conditions: cardNode.conditions } : {})
487
651
  };
488
652
  // Add card-specific properties
489
653
  if (cardNode.cardTitle !== undefined)
@@ -503,72 +667,76 @@ export function mergeSchema(split, existingUISchema) {
503
667
  };
504
668
  // Process root layout nodes
505
669
  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);
670
+ if (!unified.layout || unified.layout.length === 0) {
671
+ // If layout is undefined or empty, generate a flat layout from properties
672
+ components = Object.entries(unified.properties).map(([, unifiedField]) => {
673
+ return mergeUnifiedField(unifiedField);
512
674
  });
513
675
  }
514
676
  else {
515
- components = processLayoutNodes(uiSchema.layout);
677
+ components = processLayoutNodes(unified.layout);
516
678
  }
517
679
  return {
518
- version: split.dataSchema.version,
519
- components
680
+ version: unified.version,
681
+ components,
682
+ ...(unified.settings ? { settings: unified.settings } : {})
520
683
  };
521
684
  }
522
685
  /**
523
- * Merge a single field from data and UI schemas
686
+ * Merge a single unified field into FormComponent
524
687
  */
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;
688
+ function mergeUnifiedField(unifiedField) {
689
+ const hasOptions = unifiedField.options !== undefined && unifiedField.options.length > 0;
528
690
  const component = {
529
- id: dataField.id,
530
- type: (uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, hasOptions)),
531
- label: dataField.label || dataField.id,
691
+ id: unifiedField.id,
692
+ name: unifiedField.name,
693
+ type: (unifiedField.widget || inferWidgetFromType(unifiedField.type, unifiedField.format, hasOptions)),
694
+ label: unifiedField.label || unifiedField.id,
532
695
  };
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
- }
696
+ // Copy all data and UI properties from unified field
697
+ const dataSchemaProps = new Set([
698
+ 'id', 'type', 'name', 'label', 'format', 'widget', 'defaultValue', 'required', 'options', 'expressionValue',
699
+ 'minLength', 'maxLength', 'minimum', 'maximum', 'minValue', 'maxValue',
700
+ 'minDate', 'maxDate', 'minTime', 'maxTime', 'regex', 'pattern', 'content', 'buttonType', 'textboxType'
701
+ ]);
702
+ Object.keys(unifiedField).forEach(key => {
703
+ if (!dataSchemaProps.has(key) && key !== 'id' && key !== 'name' && key !== 'type' && key !== 'label') {
704
+ component[key] = unifiedField[key];
705
+ }
706
+ });
707
+ // Copy specific data properties
708
+ if (unifiedField.defaultValue !== undefined)
709
+ component.defaultValue = unifiedField.defaultValue;
710
+ if (unifiedField.expressionValue !== undefined)
711
+ component.expressionValue = unifiedField.expressionValue;
712
+ if (unifiedField.required !== undefined)
713
+ component.required = unifiedField.required;
714
+ if (unifiedField.content !== undefined)
715
+ component.content = unifiedField.content;
716
+ if (unifiedField.buttonType !== undefined)
717
+ component.buttonType = unifiedField.buttonType;
718
+ if (unifiedField.textboxType !== undefined)
719
+ component.textboxType = unifiedField.textboxType;
720
+ if (unifiedField.options !== undefined)
721
+ component.options = unifiedField.options;
722
+ if (unifiedField.minLength !== undefined)
723
+ component.minLength = unifiedField.minLength;
724
+ if (unifiedField.maxLength !== undefined)
725
+ component.maxLength = unifiedField.maxLength;
726
+ if (unifiedField.minValue !== undefined)
727
+ component.minValue = unifiedField.minValue;
728
+ if (unifiedField.maxValue !== undefined)
729
+ component.maxValue = unifiedField.maxValue;
730
+ if (unifiedField.minDate !== undefined)
731
+ component.minDate = unifiedField.minDate;
732
+ if (unifiedField.maxDate !== undefined)
733
+ component.maxDate = unifiedField.maxDate;
734
+ if (unifiedField.minTime !== undefined)
735
+ component.minTime = unifiedField.minTime;
736
+ if (unifiedField.maxTime !== undefined)
737
+ component.maxTime = unifiedField.maxTime;
738
+ if (unifiedField.pattern !== undefined)
739
+ component.regex = unifiedField.pattern;
572
740
  return component;
573
741
  }
574
742
  /**
@@ -577,11 +745,11 @@ function mergeField(dataField, uiField) {
577
745
  export function inferWidgetFromType(type, format, hasOptions) {
578
746
  if (type === 'string') {
579
747
  if (format === 'date')
580
- return 'datepicker';
748
+ return 'date';
581
749
  if (format === 'date-time')
582
- return 'datetimepicker';
750
+ return 'dateTime';
583
751
  if (format === 'time')
584
- return 'timepicker';
752
+ return 'time';
585
753
  if (format === 'email')
586
754
  return 'email';
587
755
  if (format === 'url')
@@ -606,13 +774,13 @@ export function inferWidgetFromType(type, format, hasOptions) {
606
774
  if (type === 'sign')
607
775
  return 'signature';
608
776
  if (type === 'image')
609
- return 'imageeditor';
777
+ return 'imageEditor';
610
778
  if (type === 'file')
611
- return 'fileupload';
779
+ return 'fileUpload';
612
780
  if (type === 'color')
613
- return 'colorpicker';
781
+ return 'colorPicker';
614
782
  if (type === 'grid')
615
- return 'datagrid';
783
+ return 'dataGrid';
616
784
  if (type === 'any')
617
785
  return 'textbox';
618
786
  return 'textbox';
@@ -844,21 +1012,23 @@ export function mergeWithIDRemapping(newDataSchema, existingUISchema) {
844
1012
  //console.log(`📐 Layout rebuilt: new fields added at end`);
845
1013
  }
846
1014
  }
847
- // Merge with remapped data
1015
+ // Merge to FormSchema using unified schema
1016
+ const unifiedProperties = {};
1017
+ Object.keys(remappedDataFields).forEach(fieldKey => {
1018
+ const dataField = remappedDataFields[fieldKey];
1019
+ const uiField = enhancedUIProperties[fieldKey];
1020
+ unifiedProperties[fieldKey] = {
1021
+ ...dataField,
1022
+ ...uiField,
1023
+ widget: uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, dataField.options !== undefined)
1024
+ };
1025
+ });
848
1026
  const mergedSchema = mergeSchema({
849
- dataSchema: {
850
- version: newDataSchema.version,
851
- properties: remappedDataFields
852
- },
853
- uiSchema: {
854
- properties: enhancedUIProperties,
855
- layout: remappedLayout
856
- }
1027
+ version: newDataSchema.version,
1028
+ properties: unifiedProperties,
1029
+ layout: remappedLayout,
1030
+ ...(newDataSchema.settings ? { settings: newDataSchema.settings } : {})
857
1031
  });
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
1032
  return mergedSchema;
863
1033
  }
864
1034
  /**
@@ -911,9 +1081,6 @@ function remapLayoutIds(layout, idMap) {
911
1081
  return node;
912
1082
  });
913
1083
  }
914
- // ============================================================================
915
- // UTILITY FUNCTIONS
916
- // ============================================================================
917
1084
  /**
918
1085
  * Extract all field IDs from layout tree
919
1086
  */
@@ -940,14 +1107,6 @@ export function extractFieldIdsFromLayout(layout) {
940
1107
  }
941
1108
  return fieldIds;
942
1109
  }
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
1110
  // ============================================================================
952
1111
  // PARSER OUTPUT CONVERSION
953
1112
  // ============================================================================
@@ -1048,133 +1207,3 @@ export function convertComponentsToDataSchema(components) {
1048
1207
  properties
1049
1208
  };
1050
1209
  }
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
- }