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