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