@syncfusion/ej2-react-form-renderer 0.1.1-beta → 34.2.2

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 (173) hide show
  1. package/README.md +86 -110
  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 -48
  149. package/src/ConditionalRuleEngine.d.ts +0 -253
  150. package/src/ConditionalRuleEngine.js +0 -489
  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 -144
  155. package/src/FormRenderer.d.ts +0 -90
  156. package/src/FormRenderer.js +0 -649
  157. package/src/FormValidator.d.ts +0 -479
  158. package/src/FormValidator.js +0 -579
  159. package/src/StaticHtmlRenderer.d.ts +0 -8
  160. package/src/StaticHtmlRenderer.js +0 -21
  161. package/src/json-converter.d.ts +0 -180
  162. package/src/json-converter.js +0 -1329
  163. package/src/parser.d.ts +0 -99
  164. package/src/parser.js +0 -750
  165. package/src/renderFormField.d.ts +0 -20
  166. package/src/renderFormField.js +0 -203
  167. package/src/types/FormSchema.d.ts +0 -205
  168. package/src/types/FormSchema.js +0 -1
  169. package/src/utils.js +0 -146
  170. package/styles/bootstrap5.3-dark.css +0 -26
  171. package/styles/fluent2-dark.css +0 -26
  172. package/styles/form-renderer.base.css +0 -489
  173. package/styles/tailwind3-dark.css +0 -10
@@ -1,1329 +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
- 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
- // Update component conditions (or remove if empty)
268
- component.conditions = hasValidConditions ? cleanedConditions : undefined;
269
- }
270
- // Get semantic type mapping (may be null for presentation-only widgets)
271
- const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[component.type];
272
- // Handle layout containers
273
- if (component.type === 'table') {
274
- const tableNode = {
275
- type: 'table',
276
- id: component.id,
277
- name: component.name,
278
- label: component.label,
279
- cssClass: component.cssClass,
280
- hideBorders: component.hideBorders,
281
- rows: component.tableCells?.length || 0,
282
- cols: component.tableCells?.[0]?.length || 0,
283
- cells: [],
284
- columnWidths: component.columnWidths,
285
- ...(component.conditions ? { conditions: component.conditions } : {})
286
- };
287
- // Process table cells
288
- component.tableCells?.forEach((row, rowIndex) => {
289
- const cellRow = [];
290
- row.forEach((cellComponents, colIndex) => {
291
- const cellChildren = [];
292
- // Reverse children and use unshift() to maintain order with depth-first processing
293
- [...cellComponents].reverse().forEach((comp) => {
294
- stack.unshift({ component: comp, layoutParent: cellChildren });
295
- });
296
- cellRow.push({ row: rowIndex, col: colIndex, children: cellChildren });
297
- });
298
- tableNode.cells.push(cellRow);
299
- });
300
- layoutParent.push(tableNode);
301
- continue;
302
- }
303
- if (component.type === 'panel') {
304
- const panelNode = {
305
- type: 'panel',
306
- id: component.id,
307
- name: component.name,
308
- label: component.label,
309
- cssClass: component.cssClass,
310
- hideBorders: component.hideBorders,
311
- hideLabel: component.hideLabel,
312
- children: [],
313
- ...(component.conditions ? { conditions: component.conditions } : {})
314
- };
315
- // Process panel children
316
- // Reverse children and use unshift() to maintain order with depth-first processing
317
- [...(component.children || [])].reverse().forEach((child) => {
318
- stack.unshift({ component: child, layoutParent: panelNode.children });
319
- });
320
- layoutParent.push(panelNode);
321
- continue;
322
- }
323
- if (component.type === 'tabs') {
324
- const tabsNode = {
325
- type: 'tabs',
326
- name: component.name,
327
- id: component.id,
328
- label: component.label,
329
- cssClass: component.cssClass,
330
- tabOptions: component.tabOptions,
331
- tabs: [],
332
- ...(component.conditions ? { conditions: component.conditions } : {})
333
- };
334
- // Process tabs
335
- component.tabItems?.forEach((tab) => {
336
- const tabChildren = [];
337
- // Reverse children and use unshift() to maintain order with depth-first processing
338
- [...(tab.content || [])].reverse().forEach((comp) => {
339
- stack.unshift({ component: comp, layoutParent: tabChildren });
340
- });
341
- tabsNode.tabs.push({ header: tab.header, children: tabChildren });
342
- });
343
- layoutParent.push(tabsNode);
344
- continue;
345
- }
346
- if (component.type === 'card') {
347
- const cardNode = {
348
- type: 'card',
349
- id: component.id,
350
- name: component.name,
351
- label: component.label,
352
- cssClass: component.cssClass,
353
- hideLabel: component.hideLabel,
354
- cardTitle: component.cardTitle,
355
- cardSubtitle: component.cardSubtitle,
356
- enableAction: component.enableAction,
357
- actionButtonLabel: component.actionButtonLabel,
358
- actionButtonType: component.actionButtonType,
359
- children: [],
360
- ...(component.conditions ? { conditions: component.conditions } : {})
361
- };
362
- // Process card children
363
- // Reverse children and use unshift() to maintain order with depth-first processing
364
- [...(component.children || [])].reverse().forEach((child) => {
365
- stack.unshift({ component: child, layoutParent: cardNode.children });
366
- });
367
- layoutParent.push(cardNode);
368
- continue;
369
- }
370
- // ✅ DEDUPLICATION: Skip if we've already processed this component ID
371
- if (seenComponentIds.has(component.id)) {
372
- //console.log(`⚠️ [splitSchema] Skipping duplicate component: ${component.id} (${component.label})`);
373
- continue;
374
- }
375
- seenComponentIds.add(component.id);
376
- // Generate semantic field key from label
377
- const baseFieldKey = labelToFieldKey(component.label || component.id, component.id);
378
- const fieldKey = makeUniqueFieldKey(baseFieldKey, usedFieldKeys, component.id);
379
- usedFieldKeys.add(fieldKey);
380
- // Extract data field properties (all components, no skipping)
381
- // If no semantic type mapping exists, use 'any' as the type
382
- const dataField = extractDataField(component, semanticType || { type: 'any' });
383
- dataFields[fieldKey] = dataField;
384
- // Extract UI field properties (all components)
385
- uiFields[fieldKey] = extractUIField(component);
386
- // Add property reference to layout (use fieldKey instead of component ID)
387
- layoutParent.push({ type: 'field', propertyId: fieldKey });
388
- }
389
- return {
390
- dataSchema: {
391
- version: heartSchema.version,
392
- properties: dataFields
393
- },
394
- uiSchema: {
395
- properties: stripConfiguredFlag(uiFields),
396
- layout
397
- }
398
- };
399
- }
400
- function extractDataField(component, semanticType) {
401
- const dataField = {
402
- id: component.id,
403
- name: component.name,
404
- type: semanticType.type,
405
- };
406
- // Add format if present
407
- if (semanticType.format) {
408
- dataField.format = semanticType.format;
409
- }
410
- // Extract allowed properties only
411
- if (component.label !== undefined)
412
- dataField.label = component.label;
413
- if (component.defaultValue !== undefined)
414
- dataField.defaultValue = component.defaultValue;
415
- if (component.content !== undefined)
416
- dataField.content = component.content;
417
- if (component.buttonType !== undefined)
418
- dataField.buttonType = component.buttonType;
419
- if (component.textboxType !== undefined)
420
- dataField.textboxType = component.textboxType;
421
- if (component.required !== undefined)
422
- dataField.required = component.required;
423
- // Extract options for selection fields (dropdown, radio, multiselect)
424
- if (component.options !== undefined)
425
- dataField.options = component.options;
426
- if (component.minLength !== undefined)
427
- dataField.minLength = component.minLength;
428
- if (component.maxLength !== undefined)
429
- dataField.maxLength = component.maxLength;
430
- // Handle numeric validation (use canonical names)
431
- if (component.minValue !== undefined)
432
- dataField.minValue = component.minValue;
433
- if (component.maxValue !== undefined)
434
- dataField.maxValue = component.maxValue;
435
- // Date/time validation
436
- if (component.minDate !== undefined)
437
- dataField.minDate = component.minDate;
438
- if (component.maxDate !== undefined)
439
- dataField.maxDate = component.maxDate;
440
- if (component.minTime !== undefined)
441
- dataField.minTime = component.minTime;
442
- if (component.maxTime !== undefined)
443
- dataField.maxTime = component.maxTime;
444
- // Pattern validation (rename from regex, convert RegExp to string)
445
- if (component.regex !== undefined) {
446
- const regex = component.regex;
447
- dataField.pattern = regex instanceof RegExp ? regex.source : regex;
448
- }
449
- if (component.pattern !== undefined) {
450
- const pattern = component.pattern;
451
- dataField.pattern = pattern instanceof RegExp ? pattern.source : pattern;
452
- }
453
- return dataField;
454
- }
455
- function extractUIField(component) {
456
- const uiField = {
457
- widget: component.type,
458
- };
459
- // Copy all properties EXCEPT those in data schema allowed list
460
- const dataSchemaProps = new Set([
461
- 'id', 'type', 'label', 'format', 'defaultValue', 'required', 'options', 'name',
462
- 'minLength', 'maxLength', 'minimum', 'maximum', 'minValue', 'maxValue',
463
- 'minDate', 'maxDate', 'minTime', 'maxTime', 'regex', 'pattern', 'content', 'buttonType', 'textboxType'
464
- ]);
465
- Object.keys(component).forEach(key => {
466
- if (!dataSchemaProps.has(key) && key !== 'id' && key !== 'type') {
467
- uiField[key] = component[key];
468
- }
469
- });
470
- // Special handling: Filter dataGrid columns to keep only required properties
471
- if (component.type === 'dataGrid' && uiField.columns && Array.isArray(uiField.columns)) {
472
- const requiredColumnProps = [
473
- 'field',
474
- 'headerText',
475
- 'type',
476
- 'width',
477
- 'format',
478
- 'editType',
479
- 'validationRules'
480
- ];
481
- uiField.columns = uiField.columns.map((col) => {
482
- const filteredCol = {};
483
- requiredColumnProps.forEach(prop => {
484
- if (col[prop] !== undefined) {
485
- filteredCol[prop] = col[prop];
486
- }
487
- });
488
- return filteredCol;
489
- });
490
- }
491
- return uiField;
492
- }
493
- // ============================================================================
494
- // MERGE ALGORITHM
495
- // ============================================================================
496
- /**
497
- * Merge split schemas back into heart schema
498
- * Supports both simple merge and fuzzy matching for re-import scenarios
499
- */
500
- export function mergeSchema(split, existingUISchema) {
501
- // Create lookup map for O(1) access to data fields
502
- const dataFieldMap = new Map();
503
- Object.entries(split.dataSchema.properties).forEach(([id, field]) => {
504
- dataFieldMap.set(id, field);
505
- });
506
- // Use existing UI schema if provided (for re-import), otherwise use split UI schema
507
- let uiSchema = existingUISchema || split.uiSchema;
508
- // ✅ Ensure all UI properties have _configured flag (add if missing from imported JSON)
509
- const enhancedUIProperties = {};
510
- Object.entries(uiSchema.properties).forEach(([key, uiField]) => {
511
- enhancedUIProperties[key] = {
512
- ...uiField,
513
- _configured: uiField._configured !== undefined ? uiField._configured : true
514
- };
515
- });
516
- // Use enhanced UI schema with _configured flags
517
- uiSchema = {
518
- ...uiSchema,
519
- properties: enhancedUIProperties
520
- };
521
- // Track processed field IDs to prevent duplicates
522
- const processedFieldIds = new Set();
523
- // Recursive function to process layout nodes
524
- const processLayoutNodes = (layoutNodes) => {
525
- const components = [];
526
- for (const node of layoutNodes) {
527
- if (node.type === 'field') {
528
- const fieldNode = node;
529
- // ✅ DEDUPLICATION: Skip if we've already processed this field
530
- if (processedFieldIds.has(fieldNode.propertyId)) {
531
- //console.log(`⚠️ [mergeSchema] Skipping duplicate field: ${fieldNode.propertyId}`);
532
- continue;
533
- }
534
- processedFieldIds.add(fieldNode.propertyId);
535
- const dataField = dataFieldMap.get(fieldNode.propertyId);
536
- const uiField = uiSchema.properties[fieldNode.propertyId];
537
- if (!dataField) {
538
- console.warn(`Field ${fieldNode.propertyId} in layout not found in data schema`);
539
- // Generate fallback data field
540
- const fallbackDataField = {
541
- id: fieldNode.propertyId,
542
- name: generateName(uiField?.widget || ''),
543
- type: 'any',
544
- label: uiField?.widget || 'Unknown Field'
545
- };
546
- components.push(mergeField(fallbackDataField, uiField));
547
- }
548
- else {
549
- components.push(mergeField(dataField, uiField));
550
- }
551
- }
552
- else if (node.type === 'table') {
553
- const tableNode = node;
554
- const tableComponent = {
555
- id: tableNode.id,
556
- name: tableNode.name,
557
- type: 'table',
558
- label: tableNode.label || 'Table',
559
- cssClass: tableNode.cssClass,
560
- hideBorders: tableNode.hideBorders,
561
- rows: tableNode.rows,
562
- columns: tableNode.cols,
563
- columnWidths: tableNode.columnWidths,
564
- tableCells: [],
565
- _configured: true,
566
- ...(tableNode.conditions ? { conditions: tableNode.conditions } : {})
567
- };
568
- // Process table cells recursively
569
- tableNode.cells.forEach((row) => {
570
- const cellRow = [];
571
- row.forEach((cell) => {
572
- // Recursively process children in each cell
573
- const cellComponents = processLayoutNodes(cell.children);
574
- cellRow.push(cellComponents);
575
- });
576
- tableComponent.tableCells.push(cellRow);
577
- });
578
- components.push(tableComponent);
579
- }
580
- else if (node.type === 'panel') {
581
- const panelNode = node;
582
- const panelComponent = {
583
- id: panelNode.id,
584
- type: 'panel',
585
- name: panelNode.name,
586
- cssClass: panelNode.cssClass,
587
- hideBorders: panelNode.hideBorders,
588
- hideLabel: panelNode.hideLabel,
589
- label: panelNode.label || 'Panel',
590
- children: processLayoutNodes(panelNode.children),
591
- _configured: true,
592
- ...(panelNode.conditions ? { conditions: panelNode.conditions } : {})
593
- };
594
- components.push(panelComponent);
595
- }
596
- else if (node.type === 'tabs') {
597
- const tabsNode = node;
598
- const tabsComponent = {
599
- id: tabsNode.id,
600
- type: 'tabs',
601
- name: tabsNode.name,
602
- label: tabsNode.label || 'Tabs',
603
- cssClass: tabsNode.cssClass,
604
- tabOptions: tabsNode.tabOptions,
605
- tabItems: [],
606
- _configured: true,
607
- ...(tabsNode.conditions ? { conditions: tabsNode.conditions } : {})
608
- };
609
- // Process tabs recursively
610
- tabsNode.tabs.forEach((tab) => {
611
- tabsComponent.tabItems.push({
612
- header: tab.header,
613
- content: processLayoutNodes(tab.children) // Recursively process tab content
614
- });
615
- });
616
- components.push(tabsComponent);
617
- }
618
- else if (node.type === 'card') {
619
- const cardNode = node;
620
- const cardComponent = {
621
- id: cardNode.id,
622
- type: 'card',
623
- name: cardNode.name,
624
- label: cardNode.label || 'Card',
625
- cssClass: cardNode.cssClass,
626
- hideLabel: cardNode.hideLabel,
627
- children: processLayoutNodes(cardNode.children),
628
- _configured: true,
629
- ...(cardNode.conditions ? { conditions: cardNode.conditions } : {})
630
- };
631
- // Add card-specific properties
632
- if (cardNode.cardTitle !== undefined)
633
- cardComponent.cardTitle = cardNode.cardTitle;
634
- if (cardNode.cardSubtitle !== undefined)
635
- cardComponent.cardSubtitle = cardNode.cardSubtitle;
636
- if (cardNode.enableAction !== undefined)
637
- cardComponent.enableAction = cardNode.enableAction;
638
- if (cardNode.actionButtonLabel !== undefined)
639
- cardComponent.actionButtonLabel = cardNode.actionButtonLabel;
640
- if (cardNode.actionButtonType !== undefined)
641
- cardComponent.actionButtonType = cardNode.actionButtonType;
642
- components.push(cardComponent);
643
- }
644
- }
645
- return components;
646
- };
647
- // Process root layout nodes
648
- let components;
649
- if (!uiSchema.layout || uiSchema.layout.length === 0) {
650
- // If layout is undefined or empty, generate a flat layout from data schema
651
- //console.log('⚠️ UI Schema layout is undefined or empty, generating from data schema');
652
- components = Object.entries(split.dataSchema.properties).map(([fieldKey, dataField]) => {
653
- const uiField = uiSchema.properties[fieldKey];
654
- return mergeField(dataField, uiField);
655
- });
656
- }
657
- else {
658
- components = processLayoutNodes(uiSchema.layout);
659
- }
660
- return {
661
- version: split.dataSchema.version,
662
- components
663
- };
664
- }
665
- /**
666
- * Merge a single field from data and UI schemas
667
- */
668
- function mergeField(dataField, uiField) {
669
- // Check if options are present in the data field
670
- const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
671
- const component = {
672
- id: dataField.id,
673
- name: dataField.name,
674
- type: (uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, hasOptions)),
675
- label: dataField.label || dataField.id,
676
- };
677
- // Copy data properties
678
- if (dataField.defaultValue !== undefined)
679
- component.defaultValue = dataField.defaultValue;
680
- if (dataField.required !== undefined)
681
- component.required = dataField.required;
682
- if (dataField.content !== undefined)
683
- component.content = dataField.content;
684
- if (dataField.buttonType !== undefined)
685
- component.buttonType = dataField.buttonType;
686
- if (dataField.textboxType !== undefined)
687
- component.textboxType = dataField.textboxType;
688
- if (dataField.options !== undefined)
689
- component.options = dataField.options;
690
- if (dataField.minLength !== undefined)
691
- component.minLength = dataField.minLength;
692
- if (dataField.maxLength !== undefined)
693
- component.maxLength = dataField.maxLength;
694
- if (dataField.minValue !== undefined)
695
- component.minValue = dataField.minValue;
696
- if (dataField.maxValue !== undefined)
697
- component.maxValue = dataField.maxValue;
698
- if (dataField.minDate !== undefined)
699
- component.minDate = dataField.minDate;
700
- if (dataField.maxDate !== undefined)
701
- component.maxDate = dataField.maxDate;
702
- if (dataField.minTime !== undefined)
703
- component.minTime = dataField.minTime;
704
- if (dataField.maxTime !== undefined)
705
- component.maxTime = dataField.maxTime;
706
- if (dataField.pattern !== undefined)
707
- component.regex = dataField.pattern;
708
- // Copy UI properties (including _configured flag)
709
- if (uiField) {
710
- Object.keys(uiField).forEach(key => {
711
- if (key !== 'widget' && component[key] === undefined) {
712
- component[key] = uiField[key];
713
- }
714
- });
715
- }
716
- return component;
717
- }
718
- /**
719
- * Infer widget type from semantic data type
720
- */
721
- export function inferWidgetFromType(type, format, hasOptions) {
722
- if (type === 'string') {
723
- if (format === 'date')
724
- return 'date';
725
- if (format === 'date-time')
726
- return 'dateTime';
727
- if (format === 'time')
728
- return 'time';
729
- if (format === 'email')
730
- return 'email';
731
- if (format === 'url')
732
- return 'url';
733
- // If options are present, default to dropdown for string types
734
- if (hasOptions)
735
- return 'dropdown';
736
- return 'textbox';
737
- }
738
- if (type === 'number')
739
- return 'number';
740
- if (type === 'boolean')
741
- return 'checkbox';
742
- if (type === 'array') {
743
- // For array type with options, use multiselect
744
- return 'multiselect';
745
- }
746
- if (type === 'button')
747
- return 'button';
748
- if (type === 'message')
749
- return 'message';
750
- if (type === 'sign')
751
- return 'signature';
752
- if (type === 'image')
753
- return 'imageEditor';
754
- if (type === 'file')
755
- return 'fileUpload';
756
- if (type === 'color')
757
- return 'colorPicker';
758
- if (type === 'grid')
759
- return 'dataGrid';
760
- if (type === 'any')
761
- return 'textbox';
762
- return 'textbox';
763
- }
764
- /**
765
- * Calculate Levenshtein distance for fuzzy string matching
766
- */
767
- function levenshteinDistance(str1, str2) {
768
- const len1 = str1.length;
769
- const len2 = str2.length;
770
- const matrix = [];
771
- // Initialize matrix
772
- for (let i = 0; i <= len1; i++) {
773
- matrix[i] = [i];
774
- }
775
- for (let j = 0; j <= len2; j++) {
776
- matrix[0][j] = j;
777
- }
778
- // Fill matrix
779
- for (let i = 1; i <= len1; i++) {
780
- for (let j = 1; j <= len2; j++) {
781
- const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
782
- matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
783
- matrix[i][j - 1] + 1, // insertion
784
- matrix[i - 1][j - 1] + cost // substitution
785
- );
786
- }
787
- }
788
- return matrix[len1][len2];
789
- }
790
- /**
791
- * Calculate similarity percentage between two strings
792
- */
793
- function calculateSimilarity(str1, str2) {
794
- const distance = levenshteinDistance(str1.toLowerCase(), str2.toLowerCase());
795
- const maxLen = Math.max(str1.length, str2.length);
796
- if (maxLen === 0)
797
- return 100;
798
- return Math.round(((maxLen - distance) / maxLen) * 100);
799
- }
800
- /**
801
- * Match a new data field with existing UI schema field
802
- * Preserves existing ID for stability
803
- */
804
- export function matchFieldWithIDPreservation(newDataField, existingUISchema) {
805
- // 1. Try exact ID match first
806
- if (existingUISchema.properties[newDataField.id]) {
807
- return {
808
- existingId: newDataField.id,
809
- newDataId: newDataField.id,
810
- confidence: 'exact',
811
- method: 'id',
812
- similarity: 100
813
- };
814
- }
815
- // 2. Try fuzzy match by ID (check if existing ID starts with new ID or vice versa)
816
- let bestIdMatch = null;
817
- for (const [existingId, existingUIField] of Object.entries(existingUISchema.properties)) {
818
- // Skip if this is a layout container
819
- if (['table', 'panel', 'tabs'].includes(existingUIField.widget)) {
820
- continue;
821
- }
822
- // Check if one ID is a prefix of the other (common case: radio_123 vs radio_123_456)
823
- const newId = newDataField.id || '';
824
- const isPrefix = existingId.startsWith(newId + '_') || newId.startsWith(existingId + '_');
825
- if (isPrefix) {
826
- const similarity = Math.round((Math.min(newId.length, existingId.length) / Math.max(newId.length, existingId.length)) * 100);
827
- if (!bestIdMatch || (bestIdMatch.similarity && similarity > bestIdMatch.similarity)) {
828
- bestIdMatch = {
829
- existingId: existingId, // PRESERVE EXISTING ID
830
- newDataId: newDataField.id,
831
- confidence: similarity >= 90 ? 'high' : similarity >= 70 ? 'medium' : 'low',
832
- method: 'id',
833
- similarity
834
- };
835
- }
836
- }
837
- else {
838
- // Try Levenshtein distance for ID similarity
839
- const idSimilarity = calculateSimilarity(existingId, newId);
840
- // Only consider as potential match if very similar (≥85%)
841
- if (idSimilarity >= 85) {
842
- if (!bestIdMatch || (bestIdMatch.similarity && idSimilarity > bestIdMatch.similarity)) {
843
- bestIdMatch = {
844
- existingId: existingId,
845
- newDataId: newDataField.id,
846
- confidence: idSimilarity >= 90 ? 'high' : 'medium',
847
- method: 'id',
848
- similarity: idSimilarity
849
- };
850
- }
851
- }
852
- }
853
- }
854
- // If we found a good ID match, use it
855
- if (bestIdMatch && bestIdMatch.similarity && bestIdMatch.similarity >= 70) {
856
- //console.log(`ID fuzzy match: "${newDataField.id}" → "${bestIdMatch.existingId}" (${bestIdMatch.similarity}% similar)`);
857
- return bestIdMatch;
858
- }
859
- // 3. Try fuzzy match by label
860
- let bestLabelMatch = null;
861
- for (const [existingId, existingUIField] of Object.entries(existingUISchema.properties)) {
862
- // Skip if this is a layout container
863
- if (['table', 'panel', 'tabs'].includes(existingUIField.widget)) {
864
- continue;
865
- }
866
- // Get label from UI field or try to extract from ID
867
- const existingLabel = existingUIField.label || existingId;
868
- const newLabel = newDataField.label || newDataField.id;
869
- if (!existingLabel || !newLabel)
870
- continue;
871
- const similarity = calculateSimilarity(existingLabel, newLabel);
872
- // Update best match if this is better
873
- if (!bestLabelMatch || (bestLabelMatch.similarity && similarity > bestLabelMatch.similarity)) {
874
- bestLabelMatch = {
875
- existingId: existingId, // PRESERVE EXISTING ID
876
- newDataId: newDataField.id,
877
- confidence: similarity >= 90 ? 'high' : similarity >= 70 ? 'medium' : 'low',
878
- method: similarity === 100 ? 'label-exact' : 'label-fuzzy',
879
- similarity
880
- };
881
- }
882
- }
883
- // 4. Return best label match if similarity is good enough (≥70%)
884
- if (bestLabelMatch && bestLabelMatch.similarity && bestLabelMatch.similarity >= 70) {
885
- //console.log(`Label fuzzy match: "${newDataField.label}" → "${bestLabelMatch.existingId}" (${bestLabelMatch.similarity}% similar)`);
886
- return bestLabelMatch;
887
- }
888
- // 5. No match found - use new ID
889
- return {
890
- existingId: newDataField.id,
891
- newDataId: newDataField.id,
892
- confidence: 'exact',
893
- method: 'new',
894
- similarity: 100
895
- };
896
- }
897
- /**
898
- * Merge with ID remapping for re-import scenarios
899
- * Preserves existing IDs to maintain layout stability
900
- */
901
- export function mergeWithIDRemapping(newDataSchema, existingUISchema) {
902
- const idMap = new Map(); // newId → canonicalId
903
- const matchResults = [];
904
- // Build ID mapping table
905
- Object.entries(newDataSchema.properties).forEach(([newId, dataField]) => {
906
- const match = matchFieldWithIDPreservation(dataField, existingUISchema);
907
- matchResults.push(match);
908
- idMap.set(newId, match.existingId || generateId(labelToFieldKey(dataField.label || '')));
909
- // Log remapping if IDs differ
910
- if (match.newDataId !== match.existingId) {
911
- //console.log(`ID remapped: ${match.newDataId} → ${match.existingId} (${match.method}, ${match.similarity}% similar)`);
912
- }
913
- });
914
- // Remap all data field IDs to canonical IDs
915
- const remappedDataFields = {};
916
- Object.entries(newDataSchema.properties).forEach(([newId, dataField]) => {
917
- const canonicalId = idMap.get(newId) || newId;
918
- remappedDataFields[canonicalId] = {
919
- ...dataField,
920
- id: canonicalId // Use canonical ID
921
- };
922
- });
923
- // Get existing field IDs from the layout
924
- const existingFieldIds = extractFieldIdsFromLayout(existingUISchema.layout);
925
- const newFieldIds = Object.keys(remappedDataFields).filter(id => !existingFieldIds.has(id));
926
- //console.log(`📋 Existing fields in layout: ${existingFieldIds.size}, New fields: ${newFieldIds.length}`);
927
- // ✅ CHECK IF SUBMIT BUTTON EXISTS: Prevent duplicate submit buttons
928
- const hasExistingSubmitButton = Object.values(existingUISchema.properties).some((uiField) => uiField.widget === 'button');
929
- // Filter out new submit buttons if one already exists
930
- const filteredNewFieldIds = newFieldIds.filter(id => {
931
- const dataField = remappedDataFields[id];
932
- if ((dataField.type === 'button' && dataField.buttonType === 'submit') && hasExistingSubmitButton) {
933
- //console.log(`⚠️ Skipping new button "${dataField.label}" - submit button already exists in layout`);
934
- return false;
935
- }
936
- return true;
937
- });
938
- //console.log(`📋 New fields after filtering: ${filteredNewFieldIds.length}`);
939
- // Update layout references to use canonical IDs
940
- let remappedLayout = remapLayoutIds(existingUISchema.layout, idMap);
941
- // Create UI properties for new fields that don't have them yet
942
- // ✅ Ensure all existing UI properties have _configured flag (add if missing from imported JSON)
943
- const enhancedUIProperties = {};
944
- Object.entries(existingUISchema.properties).forEach(([key, uiField]) => {
945
- enhancedUIProperties[key] = {
946
- ...uiField,
947
- _configured: uiField._configured !== undefined ? uiField._configured : true
948
- };
949
- });
950
- if (filteredNewFieldIds.length > 0) {
951
- //console.log(`➕ Adding ${filteredNewFieldIds.length} new fields to layout`);
952
- // Check if submit button exists and is at the last position (not moved by user)
953
- const lastNode = remappedLayout[remappedLayout.length - 1];
954
- const isSubmitAtEnd = lastNode?.type === 'field' &&
955
- enhancedUIProperties[lastNode.propertyId]?.widget === 'button';
956
- const newFieldNodes = [];
957
- // Generate default UI properties for new fields
958
- filteredNewFieldIds.forEach(id => {
959
- const dataField = remappedDataFields[id];
960
- const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
961
- const widget = inferWidgetFromType(dataField.type, dataField.format, hasOptions);
962
- // ✅ Add default button styling
963
- const uiProperties = {
964
- widget,
965
- label: dataField.label,
966
- _configured: true
967
- };
968
- // Add primary styling for buttons
969
- if (widget === 'button') {
970
- uiProperties.isPrimary = true;
971
- uiProperties.cssClass = 'e-primary';
972
- }
973
- enhancedUIProperties[id] = uiProperties;
974
- newFieldNodes.push({
975
- type: 'field',
976
- propertyId: id
977
- });
978
- });
979
- // Apply same logic as App.tsx: insert before submit if it's at the end
980
- if (isSubmitAtEnd) {
981
- // Insert new fields BEFORE the submit button
982
- remappedLayout = [...remappedLayout.slice(0, -1), ...newFieldNodes, remappedLayout[remappedLayout.length - 1]];
983
- //console.log(`📐 Layout rebuilt: new fields inserted before submit button at end`);
984
- }
985
- else {
986
- // Submit moved or doesn't exist - add new fields at end
987
- remappedLayout = [...remappedLayout, ...newFieldNodes];
988
- //console.log(`📐 Layout rebuilt: new fields added at end`);
989
- }
990
- }
991
- // Merge with remapped data
992
- const mergedSchema = mergeSchema({
993
- dataSchema: {
994
- version: newDataSchema.version,
995
- properties: remappedDataFields
996
- },
997
- uiSchema: {
998
- properties: enhancedUIProperties,
999
- layout: remappedLayout
1000
- }
1001
- });
1002
- // Log summary
1003
- //const remappedCount = matchResults.filter(m => m.newDataId !== m.existingId).length;
1004
- //const newFieldsCount = matchResults.filter(m => m.method === 'new').length;
1005
- //console.log(`Merge complete: ${matchResults.length} fields, ${remappedCount} remapped, ${newFieldsCount} new`);
1006
- return mergedSchema;
1007
- }
1008
- /**
1009
- * Recursively remap IDs in layout tree
1010
- */
1011
- function remapLayoutIds(layout, idMap) {
1012
- return layout.map(node => {
1013
- if (node.type === 'field') {
1014
- const fieldNode = node;
1015
- const canonicalId = idMap.get(fieldNode.propertyId) || fieldNode.propertyId;
1016
- return {
1017
- ...fieldNode,
1018
- propertyId: canonicalId
1019
- };
1020
- }
1021
- else if (node.type === 'table') {
1022
- const tableNode = node;
1023
- return {
1024
- ...tableNode,
1025
- cells: tableNode.cells.map(row => row.map(cell => ({
1026
- ...cell,
1027
- children: remapLayoutIds(cell.children, idMap)
1028
- })))
1029
- };
1030
- }
1031
- else if (node.type === 'panel') {
1032
- const panelNode = node;
1033
- return {
1034
- ...panelNode,
1035
- children: remapLayoutIds(panelNode.children, idMap)
1036
- };
1037
- }
1038
- else if (node.type === 'tabs') {
1039
- const tabsNode = node;
1040
- return {
1041
- ...tabsNode,
1042
- tabs: tabsNode.tabs.map(tab => ({
1043
- ...tab,
1044
- children: remapLayoutIds(tab.children, idMap)
1045
- }))
1046
- };
1047
- }
1048
- else if (node.type === 'card') {
1049
- const cardNode = node;
1050
- return {
1051
- ...cardNode,
1052
- children: remapLayoutIds(cardNode.children, idMap)
1053
- };
1054
- }
1055
- return node;
1056
- });
1057
- }
1058
- // ============================================================================
1059
- // UTILITY FUNCTIONS
1060
- // ============================================================================
1061
- /**
1062
- * Extract all field IDs from layout tree
1063
- */
1064
- export function extractFieldIdsFromLayout(layout) {
1065
- const fieldIds = new Set();
1066
- const stack = [...layout];
1067
- while (stack.length > 0) {
1068
- const node = stack.shift();
1069
- if (node.type === 'field') {
1070
- fieldIds.add(node.propertyId);
1071
- }
1072
- else if (node.type === 'table') {
1073
- node.cells.forEach(row => row.forEach(cell => stack.push(...cell.children)));
1074
- }
1075
- else if (node.type === 'panel') {
1076
- stack.push(...node.children);
1077
- }
1078
- else if (node.type === 'tabs') {
1079
- node.tabs.forEach(tab => stack.push(...tab.children));
1080
- }
1081
- else if (node.type === 'card') {
1082
- stack.push(...node.children);
1083
- }
1084
- }
1085
- return fieldIds;
1086
- }
1087
- /**
1088
- * Find unmatched fields (in data schema but not in layout)
1089
- */
1090
- export function findUnmatchedFields(dataSchema, layout) {
1091
- const layoutFieldIds = extractFieldIdsFromLayout(layout);
1092
- const dataFieldIds = Object.keys(dataSchema.properties);
1093
- return dataFieldIds.filter(id => !layoutFieldIds.has(id));
1094
- }
1095
- // ============================================================================
1096
- // PARSER OUTPUT CONVERSION
1097
- // ============================================================================
1098
- /**
1099
- * Convert parser output (FormComponent[]) to DataSchema format
1100
- * This allows parser results to be merged with existing UI schemas
1101
- */
1102
- export function convertComponentsToDataSchema(components) {
1103
- const properties = {};
1104
- const seenComponentIds = new Set(); // Track component IDs to prevent duplicates
1105
- // Flatten all components (including nested ones)
1106
- const stack = [...components];
1107
- while (stack.length > 0) {
1108
- const comp = stack.shift();
1109
- // Infer semantic type from component
1110
- const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[comp.type];
1111
- // Skip presentation-only widgets (button, message, etc.) - they have null semantic type
1112
- if (semanticType === null) {
1113
- // Still process nested children for layout containers
1114
- if (comp.children?.length) {
1115
- stack.unshift(...comp.children);
1116
- }
1117
- if (comp.tabItems?.length) {
1118
- for (const tab of comp.tabItems) {
1119
- if (tab.content?.length) {
1120
- stack.unshift(...tab.content);
1121
- }
1122
- }
1123
- }
1124
- if (comp.tableCells?.length) {
1125
- for (const row of comp.tableCells) {
1126
- for (const cell of row) {
1127
- if (cell.length) {
1128
- stack.unshift(...cell);
1129
- }
1130
- }
1131
- }
1132
- }
1133
- continue; // Skip adding to data schema
1134
- }
1135
- // ✅ DEDUPLICATION: Skip if we've already processed this component ID
1136
- if (seenComponentIds.has(comp.id)) {
1137
- //console.log(`⚠️ Skipping duplicate component: ${comp.id} (${comp.label})`);
1138
- // Still process nested children
1139
- if (comp.children?.length) {
1140
- stack.unshift(...comp.children);
1141
- }
1142
- if (comp.tabItems?.length) {
1143
- for (const tab of comp.tabItems) {
1144
- if (tab.content?.length) {
1145
- stack.unshift(...tab.content);
1146
- }
1147
- }
1148
- }
1149
- if (comp.tableCells?.length) {
1150
- for (const row of comp.tableCells) {
1151
- for (const cell of row) {
1152
- if (cell.length) {
1153
- stack.unshift(...cell);
1154
- }
1155
- }
1156
- }
1157
- }
1158
- continue;
1159
- }
1160
- seenComponentIds.add(comp.id);
1161
- // Generate fieldKey from label or ID
1162
- const baseFieldKey = labelToFieldKey(comp.label || comp.id, comp.id);
1163
- const fieldKey = makeUniqueFieldKey(baseFieldKey, new Set(Object.keys(properties)), comp.id);
1164
- const dataField = extractDataField(comp, semanticType || { type: 'any' });
1165
- properties[fieldKey] = dataField;
1166
- // Process nested children
1167
- if (comp.children?.length) {
1168
- stack.unshift(...comp.children);
1169
- }
1170
- // Process tab items
1171
- if (comp.tabItems?.length) {
1172
- for (const tab of comp.tabItems) {
1173
- if (tab.content?.length) {
1174
- stack.unshift(...tab.content);
1175
- }
1176
- }
1177
- }
1178
- // Process table cells
1179
- if (comp.tableCells?.length) {
1180
- for (const row of comp.tableCells) {
1181
- for (const cell of row) {
1182
- // cell is FormComponent[], not an object with .components
1183
- if (cell.length) {
1184
- stack.unshift(...cell);
1185
- }
1186
- }
1187
- }
1188
- }
1189
- }
1190
- return {
1191
- version: '1.0',
1192
- properties
1193
- };
1194
- }
1195
- /**
1196
- * Convert parser output to a simple linear layout
1197
- * Used when no existing UI schema is present
1198
- */
1199
- export function convertComponentsToUISchema(components) {
1200
- const properties = {};
1201
- const layout = [];
1202
- // Helper to recursively process components and build layout
1203
- function processComponent(comp) {
1204
- // Extract UI field
1205
- const uiField = extractUIField(comp);
1206
- properties[comp.id] = uiField;
1207
- // Handle different component types
1208
- if (comp.type === 'table' && comp.tableCells) {
1209
- const tableCells = comp.tableCells.map((row, rowIndex) => row.map((cellComponents, colIndex) => ({
1210
- row: rowIndex,
1211
- col: colIndex,
1212
- // cellComponents is FormComponent[], so map over it directly
1213
- children: cellComponents.map(processComponent)
1214
- })));
1215
- return {
1216
- type: 'table',
1217
- id: comp.id,
1218
- name: comp.name,
1219
- label: comp.label,
1220
- cssClass: comp.cssClass,
1221
- hideBorders: comp.hideBorders,
1222
- rows: comp.tableCells.length,
1223
- cols: comp.tableCells[0]?.length || 0,
1224
- cells: tableCells,
1225
- columnWidths: comp.columnWidths
1226
- };
1227
- }
1228
- if (comp.type === 'panel' && comp.children) {
1229
- return {
1230
- type: 'panel',
1231
- id: comp.id,
1232
- name: comp.name,
1233
- label: comp.label,
1234
- cssClass: comp.cssClass,
1235
- hideBorders: comp.hideBorders,
1236
- hideLabel: comp.hideLabel,
1237
- children: comp.children.map(processComponent)
1238
- };
1239
- }
1240
- if (comp.type === 'tabs' && comp.tabItems) {
1241
- // tabs is an array of { header, children }
1242
- const tabs = comp.tabItems.map((tab) => ({
1243
- header: tab.header,
1244
- children: tab.content?.map(processComponent) || []
1245
- }));
1246
- return {
1247
- type: 'tabs',
1248
- id: comp.id,
1249
- name: comp.name,
1250
- cssClass: comp.cssClass,
1251
- tabs
1252
- };
1253
- }
1254
- if (comp.type === 'card' && comp.children) {
1255
- return {
1256
- type: 'card',
1257
- id: comp.id,
1258
- name: comp.name,
1259
- label: comp.label,
1260
- cssClass: comp.cssClass,
1261
- hideLabel: comp.hideLabel,
1262
- cardTitle: comp.cardTitle,
1263
- cardSubtitle: comp.cardSubtitle,
1264
- enableAction: comp.enableAction,
1265
- actionButtonLabel: comp.actionButtonLabel,
1266
- actionButtonType: comp.actionButtonType,
1267
- children: comp.children.map(processComponent)
1268
- };
1269
- }
1270
- // Regular field
1271
- return {
1272
- type: 'field',
1273
- propertyId: comp.id
1274
- };
1275
- }
1276
- // Process all root components
1277
- for (const comp of components) {
1278
- layout.push(processComponent(comp));
1279
- }
1280
- return {
1281
- properties,
1282
- layout
1283
- };
1284
- }
1285
- // ============================================================================
1286
- // RENDERER HELPER FUNCTIONS
1287
- // ============================================================================
1288
- /**
1289
- * Generate default UI properties for each data field
1290
- * Used by renderers when only data schema is provided
1291
- */
1292
- export function generateUIPropertiesFromData(dataSchema) {
1293
- const properties = {};
1294
- Object.entries(dataSchema.properties).forEach(([fieldId, dataField]) => {
1295
- const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
1296
- const widget = inferWidgetFromType(dataField.type, dataField.format, hasOptions);
1297
- properties[fieldId] = { widget };
1298
- });
1299
- return properties;
1300
- }
1301
- /**
1302
- * Generate sequential layout from data schema
1303
- * Used by renderers when only data schema is provided
1304
- */
1305
- export function generateLayoutFromData(dataSchema) {
1306
- return Object.keys(dataSchema.properties).map(fieldId => ({
1307
- type: 'field',
1308
- propertyId: fieldId
1309
- }));
1310
- }
1311
- /**
1312
- * Generate minimal data schema from UI schema
1313
- * Used by renderers when only UI schema is provided
1314
- */
1315
- export function generateDataSchemaFromUI(uiSchema) {
1316
- const dataProperties = {};
1317
- Object.entries(uiSchema.properties).forEach(([fieldId, uiField]) => {
1318
- dataProperties[fieldId] = {
1319
- id: fieldId,
1320
- type: 'string',
1321
- name: generateName(uiField.label),
1322
- label: uiField.label || fieldId
1323
- };
1324
- });
1325
- return {
1326
- version: '0.1.0',
1327
- properties: dataProperties
1328
- };
1329
- }