@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.
- package/README.md +86 -110
- package/diConfig.json +85 -0
- package/dist/ej2-react-form-renderer.min.js +10 -0
- package/dist/ej2-react-form-renderer.umd.min.js +11 -0
- package/dist/ej2-react-form-renderer.umd.min.js.map +1 -0
- package/dist/es6/ej2-react-form-renderer.es2015.js +37 -0
- package/dist/es6/ej2-react-form-renderer.es2015.js.map +1 -0
- package/form-renderer.d.ts +4 -0
- package/form-renderer.js +4 -0
- package/index.d.ts +2 -4
- package/index.js +2 -4
- package/license +6 -13
- package/package.json +19 -38
- package/src/form-renderer/formrenderer.component.d.ts +34 -0
- package/src/form-renderer/formrenderer.component.js +49 -0
- package/src/form-renderer/index.d.ts +1 -0
- package/src/form-renderer/index.js +1 -0
- package/src/global.js +1 -0
- package/src/index.d.ts +2 -6
- package/src/index.js +2 -6
- package/styles/bds-lite.css +1 -0
- package/styles/bds-lite.scss +1 -0
- package/styles/bds.css +1 -0
- package/styles/bds.scss +1 -0
- package/styles/bootstrap-dark-lite.css +1 -0
- package/styles/bootstrap-dark-lite.scss +1 -0
- package/styles/bootstrap-dark.css +1 -26
- package/styles/bootstrap-dark.scss +1 -0
- package/styles/bootstrap-lite.css +1 -0
- package/styles/bootstrap-lite.scss +1 -0
- package/styles/bootstrap.css +1 -26
- package/styles/bootstrap.scss +1 -0
- package/styles/bootstrap4-lite.css +1 -0
- package/styles/bootstrap4-lite.scss +1 -0
- package/styles/bootstrap4.css +1 -26
- package/styles/bootstrap4.scss +1 -0
- package/styles/bootstrap5-dark-lite.css +1 -0
- package/styles/bootstrap5-dark-lite.scss +1 -0
- package/styles/bootstrap5-dark.css +1 -26
- package/styles/bootstrap5-dark.scss +1 -0
- package/styles/bootstrap5-lite.css +1 -0
- package/styles/bootstrap5-lite.scss +1 -0
- package/styles/bootstrap5.3-lite.css +1 -0
- package/styles/bootstrap5.3-lite.scss +1 -0
- package/styles/bootstrap5.3.css +1 -26
- package/styles/bootstrap5.3.scss +1 -0
- package/styles/bootstrap5.css +1 -26
- package/styles/bootstrap5.scss +1 -0
- package/styles/fabric-dark-lite.css +1 -0
- package/styles/fabric-dark-lite.scss +1 -0
- package/styles/fabric-dark.css +1 -0
- package/styles/fabric-dark.scss +1 -0
- package/styles/fabric-lite.css +1 -0
- package/styles/fabric-lite.scss +1 -0
- package/styles/fabric.css +1 -0
- package/styles/fabric.scss +1 -0
- package/styles/fluent-dark-lite.css +1 -0
- package/styles/fluent-dark-lite.scss +1 -0
- package/styles/fluent-dark.css +1 -26
- package/styles/fluent-dark.scss +1 -0
- package/styles/fluent-lite.css +1 -0
- package/styles/fluent-lite.scss +1 -0
- package/styles/fluent.css +1 -26
- package/styles/fluent.scss +1 -0
- package/styles/fluent2-lite.css +1 -0
- package/styles/fluent2-lite.scss +1 -0
- package/styles/fluent2.css +1 -26
- package/styles/fluent2.scss +1 -0
- package/styles/form-renderer/bds.css +1 -0
- package/styles/form-renderer/bds.scss +1 -0
- package/styles/form-renderer/bootstrap-dark.css +1 -0
- package/styles/form-renderer/bootstrap-dark.scss +1 -0
- package/styles/form-renderer/bootstrap.css +1 -0
- package/styles/form-renderer/bootstrap.scss +1 -0
- package/styles/form-renderer/bootstrap4.css +1 -0
- package/styles/form-renderer/bootstrap4.scss +1 -0
- package/styles/form-renderer/bootstrap5-dark.css +1 -0
- package/styles/form-renderer/bootstrap5-dark.scss +1 -0
- package/styles/form-renderer/bootstrap5.3.css +1 -0
- package/styles/form-renderer/bootstrap5.3.scss +1 -0
- package/styles/form-renderer/bootstrap5.css +1 -0
- package/styles/form-renderer/bootstrap5.scss +1 -0
- package/styles/form-renderer/fabric-dark.css +1 -0
- package/styles/form-renderer/fabric-dark.scss +1 -0
- package/styles/form-renderer/fabric.css +1 -0
- package/styles/form-renderer/fabric.scss +1 -0
- package/styles/form-renderer/fluent-dark.css +1 -0
- package/styles/form-renderer/fluent-dark.scss +1 -0
- package/styles/form-renderer/fluent.css +1 -0
- package/styles/form-renderer/fluent.scss +1 -0
- package/styles/form-renderer/fluent2.css +1 -0
- package/styles/form-renderer/fluent2.scss +1 -0
- package/styles/form-renderer/highcontrast-light.css +1 -0
- package/styles/form-renderer/highcontrast-light.scss +1 -0
- package/styles/form-renderer/highcontrast.css +1 -0
- package/styles/form-renderer/highcontrast.scss +1 -0
- package/styles/form-renderer/material-dark.css +1 -0
- package/styles/form-renderer/material-dark.scss +1 -0
- package/styles/form-renderer/material.css +1 -0
- package/styles/form-renderer/material.scss +1 -0
- package/styles/form-renderer/material3-dark.css +1 -0
- package/styles/form-renderer/material3-dark.scss +1 -0
- package/styles/form-renderer/material3.css +1 -0
- package/styles/form-renderer/material3.scss +1 -0
- package/styles/form-renderer/tailwind-dark.css +1 -0
- package/styles/form-renderer/tailwind-dark.scss +1 -0
- package/styles/form-renderer/tailwind.css +1 -0
- package/styles/form-renderer/tailwind.scss +1 -0
- package/styles/form-renderer/tailwind3.css +1 -0
- package/styles/form-renderer/tailwind3.scss +1 -0
- package/styles/highcontrast-light-lite.css +1 -0
- package/styles/highcontrast-light-lite.scss +1 -0
- package/styles/highcontrast-light.css +1 -0
- package/styles/highcontrast-light.scss +1 -0
- package/styles/highcontrast-lite.css +1 -0
- package/styles/highcontrast-lite.scss +1 -0
- package/styles/highcontrast.css +1 -0
- package/styles/highcontrast.scss +1 -0
- package/styles/material-dark-lite.css +1 -0
- package/styles/material-dark-lite.scss +1 -0
- package/styles/material-dark.css +1 -26
- package/styles/material-dark.scss +1 -0
- package/styles/material-lite.css +1 -0
- package/styles/material-lite.scss +1 -0
- package/styles/material.css +1 -26
- package/styles/material.scss +1 -0
- package/styles/material3-dark-lite.css +1 -0
- package/styles/material3-dark-lite.scss +1 -0
- package/styles/material3-dark.css +1 -26
- package/styles/material3-dark.scss +1 -0
- package/styles/material3-lite.css +1 -0
- package/styles/material3-lite.scss +1 -0
- package/styles/material3.css +1 -26
- package/styles/material3.scss +1 -0
- package/styles/tailwind-dark-lite.css +1 -0
- package/styles/tailwind-dark-lite.scss +1 -0
- package/styles/tailwind-dark.css +1 -26
- package/styles/tailwind-dark.scss +1 -0
- package/styles/tailwind-lite.css +1 -0
- package/styles/tailwind-lite.scss +1 -0
- package/styles/tailwind.css +1 -26
- package/styles/tailwind.scss +1 -0
- package/styles/tailwind3-lite.css +1 -0
- package/styles/tailwind3-lite.scss +1 -0
- package/styles/tailwind3.css +1 -26
- package/styles/tailwind3.scss +1 -0
- package/src/CheckboxGroup.d.ts +0 -12
- package/src/CheckboxGroup.js +0 -48
- package/src/ConditionalRuleEngine.d.ts +0 -253
- package/src/ConditionalRuleEngine.js +0 -489
- package/src/CustomTabComponent.d.ts +0 -13
- package/src/CustomTabComponent.js +0 -35
- package/src/DataGridComponent.d.ts +0 -28
- package/src/DataGridComponent.js +0 -144
- package/src/FormRenderer.d.ts +0 -90
- package/src/FormRenderer.js +0 -649
- package/src/FormValidator.d.ts +0 -479
- package/src/FormValidator.js +0 -579
- package/src/StaticHtmlRenderer.d.ts +0 -8
- package/src/StaticHtmlRenderer.js +0 -21
- package/src/json-converter.d.ts +0 -180
- package/src/json-converter.js +0 -1329
- package/src/parser.d.ts +0 -99
- package/src/parser.js +0 -750
- package/src/renderFormField.d.ts +0 -20
- package/src/renderFormField.js +0 -203
- package/src/types/FormSchema.d.ts +0 -205
- package/src/types/FormSchema.js +0 -1
- package/src/utils.js +0 -146
- package/styles/bootstrap5.3-dark.css +0 -26
- package/styles/fluent2-dark.css +0 -26
- package/styles/form-renderer.base.css +0 -489
- package/styles/tailwind3-dark.css +0 -10
package/src/json-converter.js
DELETED
|
@@ -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
|
-
}
|