@syncfusion/ej2-react-form-renderer 0.1.2-beta → 34.2.3

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