@syncfusion/ej2-react-form-renderer 0.1.0-beta → 0.1.2-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -10
- package/package.json +1 -1
- package/src/CheckboxGroup.js +3 -2
- package/src/ConditionalRuleEngine.d.ts +295 -0
- package/src/ConditionalRuleEngine.js +566 -0
- package/src/DataGridComponent.d.ts +0 -1
- package/src/DataGridComponent.js +9 -22
- package/src/FormRenderer.d.ts +28 -7
- package/src/FormRenderer.js +342 -202
- package/src/MinMaxRuleEngine.d.ts +95 -0
- package/src/MinMaxRuleEngine.js +361 -0
- package/src/expressions/DependencyTracker.d.ts +30 -0
- package/src/expressions/DependencyTracker.js +128 -0
- package/src/expressions/ExpressionEngine.d.ts +81 -0
- package/src/expressions/ExpressionEngine.js +503 -0
- package/src/expressions/ExpressionParser.d.ts +39 -0
- package/src/expressions/ExpressionParser.js +270 -0
- package/src/json-converter.d.ts +32 -31
- package/src/json-converter.js +313 -284
- package/src/parser.js +22 -18
- package/src/renderFormField.d.ts +3 -1
- package/src/renderFormField.js +70 -46
- package/src/types/FormSchema.d.ts +36 -2
- package/src/utils.js +348 -0
- package/styles/form-renderer.base.css +49 -23
package/src/json-converter.js
CHANGED
|
@@ -6,6 +6,10 @@ export const generateId = (type) => {
|
|
|
6
6
|
const random = Math.floor(Math.random() * 1000);
|
|
7
7
|
return `${type}_${timestamp}_${random}`;
|
|
8
8
|
};
|
|
9
|
+
export const generateName = (type) => {
|
|
10
|
+
const random = Math.floor(Math.random() * 1000);
|
|
11
|
+
return `${type}_${random}`;
|
|
12
|
+
};
|
|
9
13
|
// ============================================================================
|
|
10
14
|
// LABEL TO CAMELCASE CONVERTER
|
|
11
15
|
// ============================================================================
|
|
@@ -68,7 +72,7 @@ export function makeUniqueFieldKey(baseKey, existingKeys, originalId) {
|
|
|
68
72
|
// ============================================================================
|
|
69
73
|
// UI TYPE TO SEMANTIC TYPE MAPPING
|
|
70
74
|
// ============================================================================
|
|
71
|
-
const UI_TYPE_TO_SEMANTIC_TYPE = {
|
|
75
|
+
export const UI_TYPE_TO_SEMANTIC_TYPE = {
|
|
72
76
|
// String types
|
|
73
77
|
'textbox': { type: 'string' },
|
|
74
78
|
'textarea': { type: 'string' },
|
|
@@ -76,36 +80,36 @@ const UI_TYPE_TO_SEMANTIC_TYPE = {
|
|
|
76
80
|
'url': { type: 'string', format: 'url' },
|
|
77
81
|
'phone': { type: 'string' },
|
|
78
82
|
'password': { type: 'string' },
|
|
79
|
-
'
|
|
83
|
+
'inputMask': { type: 'string' },
|
|
80
84
|
// Numeric types
|
|
81
85
|
'number': { type: 'number' },
|
|
82
|
-
'
|
|
86
|
+
'rangeSlider': { type: 'number' },
|
|
83
87
|
'rating': { type: 'number' },
|
|
84
88
|
// Boolean types
|
|
85
89
|
'checkbox': { type: 'boolean' },
|
|
86
90
|
'switch': { type: 'boolean' },
|
|
87
91
|
// Date/Time types
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
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' },
|
|
92
96
|
// Array types
|
|
93
97
|
'multiselect': { type: 'array' },
|
|
94
|
-
'
|
|
98
|
+
'checkboxGroup': { type: 'array' },
|
|
95
99
|
// Single-value selection (stored as string)
|
|
96
100
|
'dropdown': { type: 'string' },
|
|
97
101
|
'radio': { type: 'string' },
|
|
98
|
-
'
|
|
102
|
+
'splitButton': { type: 'string' },
|
|
99
103
|
// Special types (excluded from data schema)
|
|
100
104
|
'button': { type: 'button' },
|
|
101
105
|
'message': { type: 'message' },
|
|
102
106
|
'signature': { type: 'sign' },
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
+
'imageEditor': { type: 'image' },
|
|
108
|
+
'fileUpload': { type: 'file' },
|
|
109
|
+
'colorPicker': { type: 'color' },
|
|
110
|
+
'dataGrid': { type: 'grid' },
|
|
107
111
|
// Layout types (excluded from data schema)
|
|
108
|
-
'
|
|
112
|
+
'staticHtml': { type: 'string' },
|
|
109
113
|
'table': null,
|
|
110
114
|
'panel': null,
|
|
111
115
|
'tabs': null,
|
|
@@ -126,6 +130,73 @@ function stripConfiguredFlag(properties) {
|
|
|
126
130
|
return out;
|
|
127
131
|
}
|
|
128
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
|
+
// ============================================================================
|
|
129
200
|
// SPLIT ALGORITHM
|
|
130
201
|
// ============================================================================
|
|
131
202
|
export function splitSchema(heartSchema) {
|
|
@@ -134,6 +205,8 @@ export function splitSchema(heartSchema) {
|
|
|
134
205
|
const layout = [];
|
|
135
206
|
const usedFieldKeys = new Set(); // Track unique field keys
|
|
136
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);
|
|
137
210
|
// Stack for iterative traversal (avoid recursion)
|
|
138
211
|
const stack = heartSchema.components.map(c => ({
|
|
139
212
|
component: c,
|
|
@@ -142,6 +215,86 @@ export function splitSchema(heartSchema) {
|
|
|
142
215
|
while (stack.length > 0) {
|
|
143
216
|
// Use shift() instead of pop() to maintain order (FIFO instead of LIFO)
|
|
144
217
|
const { component, layoutParent } = stack.shift();
|
|
218
|
+
// ✅ Clean conditions before processing
|
|
219
|
+
if (component.conditions) {
|
|
220
|
+
const cleanedConditions = {};
|
|
221
|
+
let hasValidConditions = false;
|
|
222
|
+
if (component.conditions.visibleWhen) {
|
|
223
|
+
const cleaned = cleanConditionRules(component.conditions.visibleWhen, validFieldIds);
|
|
224
|
+
if (cleaned) {
|
|
225
|
+
cleanedConditions.visibleWhen = cleaned;
|
|
226
|
+
hasValidConditions = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (component.conditions.hideWhen) {
|
|
230
|
+
const cleaned = cleanConditionRules(component.conditions.hideWhen, validFieldIds);
|
|
231
|
+
if (cleaned) {
|
|
232
|
+
cleanedConditions.hideWhen = cleaned;
|
|
233
|
+
hasValidConditions = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (component.conditions.readOnlyWhen) {
|
|
237
|
+
const cleaned = cleanConditionRules(component.conditions.readOnlyWhen, validFieldIds);
|
|
238
|
+
if (cleaned) {
|
|
239
|
+
cleanedConditions.readOnlyWhen = cleaned;
|
|
240
|
+
hasValidConditions = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (component.conditions.disabledWhen) {
|
|
244
|
+
const cleaned = cleanConditionRules(component.conditions.disabledWhen, validFieldIds);
|
|
245
|
+
if (cleaned) {
|
|
246
|
+
cleanedConditions.disabledWhen = cleaned;
|
|
247
|
+
hasValidConditions = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (component.conditions.requiredWhen) {
|
|
251
|
+
const cleaned = cleanConditionRules(component.conditions.requiredWhen, validFieldIds);
|
|
252
|
+
if (cleaned) {
|
|
253
|
+
cleanedConditions.requiredWhen = cleaned;
|
|
254
|
+
hasValidConditions = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (component.conditions.setValueWhen) {
|
|
258
|
+
const cleaned = cleanConditionRules(component.conditions.setValueWhen.condition, validFieldIds);
|
|
259
|
+
if (cleaned && component.conditions.setValueWhen.value !== undefined && component.conditions.setValueWhen.value !== '') {
|
|
260
|
+
cleanedConditions.setValueWhen = {
|
|
261
|
+
condition: cleaned,
|
|
262
|
+
value: component.conditions.setValueWhen.value
|
|
263
|
+
};
|
|
264
|
+
hasValidConditions = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (component.conditions?.choiceBasedField) {
|
|
268
|
+
const rule = component.conditions.choiceBasedField;
|
|
269
|
+
// Validate that primary field exists
|
|
270
|
+
if (rule.primaryFieldId && validFieldIds.has(rule.primaryFieldId)) {
|
|
271
|
+
cleanedConditions.choiceBasedField = rule;
|
|
272
|
+
hasValidConditions = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (component.conditions.conditionalData && Array.isArray(component.conditions.conditionalData)) {
|
|
276
|
+
const cleanedDataRules = component.conditions.conditionalData
|
|
277
|
+
.map((rule) => {
|
|
278
|
+
const cleaned = cleanConditionRules(rule.condition, validFieldIds);
|
|
279
|
+
if (cleaned && rule.dataSource) {
|
|
280
|
+
return {
|
|
281
|
+
condition: cleaned,
|
|
282
|
+
dataSource: rule.dataSource,
|
|
283
|
+
columns: rule.columns,
|
|
284
|
+
includeColumns: rule.includeColumns
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
})
|
|
289
|
+
.filter((rule) => rule !== null && rule !== undefined);
|
|
290
|
+
if (cleanedDataRules.length > 0) {
|
|
291
|
+
cleanedConditions.conditionalData = cleanedDataRules;
|
|
292
|
+
hasValidConditions = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Update component conditions (or remove if empty)
|
|
296
|
+
component.conditions = hasValidConditions ? cleanedConditions : undefined;
|
|
297
|
+
}
|
|
145
298
|
// Get semantic type mapping (may be null for presentation-only widgets)
|
|
146
299
|
const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[component.type];
|
|
147
300
|
// Handle layout containers
|
|
@@ -149,13 +302,15 @@ export function splitSchema(heartSchema) {
|
|
|
149
302
|
const tableNode = {
|
|
150
303
|
type: 'table',
|
|
151
304
|
id: component.id,
|
|
305
|
+
name: component.name,
|
|
152
306
|
label: component.label,
|
|
153
307
|
cssClass: component.cssClass,
|
|
154
308
|
hideBorders: component.hideBorders,
|
|
155
309
|
rows: component.tableCells?.length || 0,
|
|
156
310
|
cols: component.tableCells?.[0]?.length || 0,
|
|
157
311
|
cells: [],
|
|
158
|
-
columnWidths: component.columnWidths
|
|
312
|
+
columnWidths: component.columnWidths,
|
|
313
|
+
...(component.conditions ? { conditions: component.conditions } : {})
|
|
159
314
|
};
|
|
160
315
|
// Process table cells
|
|
161
316
|
component.tableCells?.forEach((row, rowIndex) => {
|
|
@@ -177,11 +332,13 @@ export function splitSchema(heartSchema) {
|
|
|
177
332
|
const panelNode = {
|
|
178
333
|
type: 'panel',
|
|
179
334
|
id: component.id,
|
|
335
|
+
name: component.name,
|
|
180
336
|
label: component.label,
|
|
181
337
|
cssClass: component.cssClass,
|
|
182
338
|
hideBorders: component.hideBorders,
|
|
183
339
|
hideLabel: component.hideLabel,
|
|
184
|
-
children: []
|
|
340
|
+
children: [],
|
|
341
|
+
...(component.conditions ? { conditions: component.conditions } : {})
|
|
185
342
|
};
|
|
186
343
|
// Process panel children
|
|
187
344
|
// Reverse children and use unshift() to maintain order with depth-first processing
|
|
@@ -194,11 +351,13 @@ export function splitSchema(heartSchema) {
|
|
|
194
351
|
if (component.type === 'tabs') {
|
|
195
352
|
const tabsNode = {
|
|
196
353
|
type: 'tabs',
|
|
354
|
+
name: component.name,
|
|
197
355
|
id: component.id,
|
|
198
356
|
label: component.label,
|
|
199
357
|
cssClass: component.cssClass,
|
|
200
358
|
tabOptions: component.tabOptions,
|
|
201
|
-
tabs: []
|
|
359
|
+
tabs: [],
|
|
360
|
+
...(component.conditions ? { conditions: component.conditions } : {})
|
|
202
361
|
};
|
|
203
362
|
// Process tabs
|
|
204
363
|
component.tabItems?.forEach((tab) => {
|
|
@@ -216,6 +375,7 @@ export function splitSchema(heartSchema) {
|
|
|
216
375
|
const cardNode = {
|
|
217
376
|
type: 'card',
|
|
218
377
|
id: component.id,
|
|
378
|
+
name: component.name,
|
|
219
379
|
label: component.label,
|
|
220
380
|
cssClass: component.cssClass,
|
|
221
381
|
hideLabel: component.hideLabel,
|
|
@@ -224,7 +384,8 @@ export function splitSchema(heartSchema) {
|
|
|
224
384
|
enableAction: component.enableAction,
|
|
225
385
|
actionButtonLabel: component.actionButtonLabel,
|
|
226
386
|
actionButtonType: component.actionButtonType,
|
|
227
|
-
children: []
|
|
387
|
+
children: [],
|
|
388
|
+
...(component.conditions ? { conditions: component.conditions } : {})
|
|
228
389
|
};
|
|
229
390
|
// Process card children
|
|
230
391
|
// Reverse children and use unshift() to maintain order with depth-first processing
|
|
@@ -253,20 +414,29 @@ export function splitSchema(heartSchema) {
|
|
|
253
414
|
// Add property reference to layout (use fieldKey instead of component ID)
|
|
254
415
|
layoutParent.push({ type: 'field', propertyId: fieldKey });
|
|
255
416
|
}
|
|
417
|
+
// ✅ Merge data and UI fields into unified properties
|
|
418
|
+
const unifiedProperties = {};
|
|
419
|
+
Object.keys(dataFields).forEach(fieldKey => {
|
|
420
|
+
const dataField = dataFields[fieldKey];
|
|
421
|
+
const uiField = uiFields[fieldKey];
|
|
422
|
+
unifiedProperties[fieldKey] = {
|
|
423
|
+
...dataField,
|
|
424
|
+
...uiField,
|
|
425
|
+
// Ensure widget is always set
|
|
426
|
+
widget: uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, dataField.options !== undefined)
|
|
427
|
+
};
|
|
428
|
+
});
|
|
256
429
|
return {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
uiSchema: {
|
|
262
|
-
properties: stripConfiguredFlag(uiFields),
|
|
263
|
-
layout
|
|
264
|
-
}
|
|
430
|
+
version: heartSchema.version,
|
|
431
|
+
properties: stripConfiguredFlag(unifiedProperties),
|
|
432
|
+
layout,
|
|
433
|
+
...(heartSchema.settings ? { settings: heartSchema.settings } : {})
|
|
265
434
|
};
|
|
266
435
|
}
|
|
267
436
|
function extractDataField(component, semanticType) {
|
|
268
437
|
const dataField = {
|
|
269
438
|
id: component.id,
|
|
439
|
+
name: component.name,
|
|
270
440
|
type: semanticType.type,
|
|
271
441
|
};
|
|
272
442
|
// Add format if present
|
|
@@ -278,6 +448,8 @@ function extractDataField(component, semanticType) {
|
|
|
278
448
|
dataField.label = component.label;
|
|
279
449
|
if (component.defaultValue !== undefined)
|
|
280
450
|
dataField.defaultValue = component.defaultValue;
|
|
451
|
+
if (component.expressionValue !== undefined)
|
|
452
|
+
dataField.expressionValue = component.expressionValue;
|
|
281
453
|
if (component.content !== undefined)
|
|
282
454
|
dataField.content = component.content;
|
|
283
455
|
if (component.buttonType !== undefined)
|
|
@@ -324,7 +496,7 @@ function extractUIField(component) {
|
|
|
324
496
|
};
|
|
325
497
|
// Copy all properties EXCEPT those in data schema allowed list
|
|
326
498
|
const dataSchemaProps = new Set([
|
|
327
|
-
'id', 'type', 'label', 'format', 'defaultValue', 'required', 'options',
|
|
499
|
+
'id', 'type', 'label', 'format', 'defaultValue', 'required', 'options', 'name', 'expressionValue',
|
|
328
500
|
'minLength', 'maxLength', 'minimum', 'maximum', 'minValue', 'maxValue',
|
|
329
501
|
'minDate', 'maxDate', 'minTime', 'maxTime', 'regex', 'pattern', 'content', 'buttonType', 'textboxType'
|
|
330
502
|
]);
|
|
@@ -333,8 +505,8 @@ function extractUIField(component) {
|
|
|
333
505
|
uiField[key] = component[key];
|
|
334
506
|
}
|
|
335
507
|
});
|
|
336
|
-
// Special handling: Filter
|
|
337
|
-
if (component.type === '
|
|
508
|
+
// Special handling: Filter dataGrid columns to keep only required properties
|
|
509
|
+
if (component.type === 'dataGrid' && uiField.columns && Array.isArray(uiField.columns)) {
|
|
338
510
|
const requiredColumnProps = [
|
|
339
511
|
'field',
|
|
340
512
|
'headerText',
|
|
@@ -360,30 +532,15 @@ function extractUIField(component) {
|
|
|
360
532
|
// MERGE ALGORITHM
|
|
361
533
|
// ============================================================================
|
|
362
534
|
/**
|
|
363
|
-
* Merge
|
|
364
|
-
*
|
|
535
|
+
* Merge unified schema back into heart schema
|
|
536
|
+
* Reconstructs FormComponent array from unified properties and layout
|
|
365
537
|
*/
|
|
366
|
-
export function mergeSchema(
|
|
367
|
-
// Create lookup map for O(1) access to
|
|
368
|
-
const
|
|
369
|
-
Object.entries(
|
|
370
|
-
|
|
371
|
-
});
|
|
372
|
-
// Use existing UI schema if provided (for re-import), otherwise use split UI schema
|
|
373
|
-
let uiSchema = existingUISchema || split.uiSchema;
|
|
374
|
-
// ✅ Ensure all UI properties have _configured flag (add if missing from imported JSON)
|
|
375
|
-
const enhancedUIProperties = {};
|
|
376
|
-
Object.entries(uiSchema.properties).forEach(([key, uiField]) => {
|
|
377
|
-
enhancedUIProperties[key] = {
|
|
378
|
-
...uiField,
|
|
379
|
-
_configured: uiField._configured !== undefined ? uiField._configured : true
|
|
380
|
-
};
|
|
538
|
+
export function mergeSchema(unified) {
|
|
539
|
+
// Create lookup map for O(1) access to unified properties
|
|
540
|
+
const propertyMap = new Map();
|
|
541
|
+
Object.entries(unified.properties).forEach(([id, field]) => {
|
|
542
|
+
propertyMap.set(id, field);
|
|
381
543
|
});
|
|
382
|
-
// Use enhanced UI schema with _configured flags
|
|
383
|
-
uiSchema = {
|
|
384
|
-
...uiSchema,
|
|
385
|
-
properties: enhancedUIProperties
|
|
386
|
-
};
|
|
387
544
|
// Track processed field IDs to prevent duplicates
|
|
388
545
|
const processedFieldIds = new Set();
|
|
389
546
|
// Recursive function to process layout nodes
|
|
@@ -394,30 +551,31 @@ export function mergeSchema(split, existingUISchema) {
|
|
|
394
551
|
const fieldNode = node;
|
|
395
552
|
// ✅ DEDUPLICATION: Skip if we've already processed this field
|
|
396
553
|
if (processedFieldIds.has(fieldNode.propertyId)) {
|
|
397
|
-
//console.log(`⚠️ [mergeSchema] Skipping duplicate field: ${fieldNode.propertyId}`);
|
|
398
554
|
continue;
|
|
399
555
|
}
|
|
400
556
|
processedFieldIds.add(fieldNode.propertyId);
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const fallbackDataField = {
|
|
557
|
+
const unifiedField = propertyMap.get(fieldNode.propertyId);
|
|
558
|
+
if (!unifiedField) {
|
|
559
|
+
console.warn(`Field ${fieldNode.propertyId} in layout not found in unified schema`);
|
|
560
|
+
// Generate fallback component
|
|
561
|
+
const fallbackComponent = {
|
|
407
562
|
id: fieldNode.propertyId,
|
|
408
|
-
|
|
409
|
-
|
|
563
|
+
name: generateName(''),
|
|
564
|
+
type: 'textbox',
|
|
565
|
+
label: 'Unknown Field',
|
|
566
|
+
_configured: true
|
|
410
567
|
};
|
|
411
|
-
components.push(
|
|
568
|
+
components.push(fallbackComponent);
|
|
412
569
|
}
|
|
413
570
|
else {
|
|
414
|
-
components.push(
|
|
571
|
+
components.push(mergeUnifiedField(unifiedField));
|
|
415
572
|
}
|
|
416
573
|
}
|
|
417
574
|
else if (node.type === 'table') {
|
|
418
575
|
const tableNode = node;
|
|
419
576
|
const tableComponent = {
|
|
420
577
|
id: tableNode.id,
|
|
578
|
+
name: tableNode.name,
|
|
421
579
|
type: 'table',
|
|
422
580
|
label: tableNode.label || 'Table',
|
|
423
581
|
cssClass: tableNode.cssClass,
|
|
@@ -426,13 +584,13 @@ export function mergeSchema(split, existingUISchema) {
|
|
|
426
584
|
columns: tableNode.cols,
|
|
427
585
|
columnWidths: tableNode.columnWidths,
|
|
428
586
|
tableCells: [],
|
|
429
|
-
_configured: true
|
|
587
|
+
_configured: true,
|
|
588
|
+
...(tableNode.conditions ? { conditions: tableNode.conditions } : {})
|
|
430
589
|
};
|
|
431
590
|
// Process table cells recursively
|
|
432
591
|
tableNode.cells.forEach((row) => {
|
|
433
592
|
const cellRow = [];
|
|
434
593
|
row.forEach((cell) => {
|
|
435
|
-
// Recursively process children in each cell
|
|
436
594
|
const cellComponents = processLayoutNodes(cell.children);
|
|
437
595
|
cellRow.push(cellComponents);
|
|
438
596
|
});
|
|
@@ -445,12 +603,14 @@ export function mergeSchema(split, existingUISchema) {
|
|
|
445
603
|
const panelComponent = {
|
|
446
604
|
id: panelNode.id,
|
|
447
605
|
type: 'panel',
|
|
606
|
+
name: panelNode.name,
|
|
448
607
|
cssClass: panelNode.cssClass,
|
|
449
608
|
hideBorders: panelNode.hideBorders,
|
|
450
609
|
hideLabel: panelNode.hideLabel,
|
|
451
610
|
label: panelNode.label || 'Panel',
|
|
452
611
|
children: processLayoutNodes(panelNode.children),
|
|
453
|
-
_configured: true
|
|
612
|
+
_configured: true,
|
|
613
|
+
...(panelNode.conditions ? { conditions: panelNode.conditions } : {})
|
|
454
614
|
};
|
|
455
615
|
components.push(panelComponent);
|
|
456
616
|
}
|
|
@@ -459,17 +619,19 @@ export function mergeSchema(split, existingUISchema) {
|
|
|
459
619
|
const tabsComponent = {
|
|
460
620
|
id: tabsNode.id,
|
|
461
621
|
type: 'tabs',
|
|
622
|
+
name: tabsNode.name,
|
|
462
623
|
label: tabsNode.label || 'Tabs',
|
|
463
624
|
cssClass: tabsNode.cssClass,
|
|
464
625
|
tabOptions: tabsNode.tabOptions,
|
|
465
626
|
tabItems: [],
|
|
466
|
-
_configured: true
|
|
627
|
+
_configured: true,
|
|
628
|
+
...(tabsNode.conditions ? { conditions: tabsNode.conditions } : {})
|
|
467
629
|
};
|
|
468
630
|
// Process tabs recursively
|
|
469
631
|
tabsNode.tabs.forEach((tab) => {
|
|
470
632
|
tabsComponent.tabItems.push({
|
|
471
633
|
header: tab.header,
|
|
472
|
-
content: processLayoutNodes(tab.children)
|
|
634
|
+
content: processLayoutNodes(tab.children)
|
|
473
635
|
});
|
|
474
636
|
});
|
|
475
637
|
components.push(tabsComponent);
|
|
@@ -479,11 +641,13 @@ export function mergeSchema(split, existingUISchema) {
|
|
|
479
641
|
const cardComponent = {
|
|
480
642
|
id: cardNode.id,
|
|
481
643
|
type: 'card',
|
|
644
|
+
name: cardNode.name,
|
|
482
645
|
label: cardNode.label || 'Card',
|
|
483
646
|
cssClass: cardNode.cssClass,
|
|
484
647
|
hideLabel: cardNode.hideLabel,
|
|
485
648
|
children: processLayoutNodes(cardNode.children),
|
|
486
|
-
_configured: true
|
|
649
|
+
_configured: true,
|
|
650
|
+
...(cardNode.conditions ? { conditions: cardNode.conditions } : {})
|
|
487
651
|
};
|
|
488
652
|
// Add card-specific properties
|
|
489
653
|
if (cardNode.cardTitle !== undefined)
|
|
@@ -503,72 +667,76 @@ export function mergeSchema(split, existingUISchema) {
|
|
|
503
667
|
};
|
|
504
668
|
// Process root layout nodes
|
|
505
669
|
let components;
|
|
506
|
-
if (!
|
|
507
|
-
// If layout is undefined or empty, generate a flat layout from
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const uiField = uiSchema.properties[fieldKey];
|
|
511
|
-
return mergeField(dataField, uiField);
|
|
670
|
+
if (!unified.layout || unified.layout.length === 0) {
|
|
671
|
+
// If layout is undefined or empty, generate a flat layout from properties
|
|
672
|
+
components = Object.entries(unified.properties).map(([, unifiedField]) => {
|
|
673
|
+
return mergeUnifiedField(unifiedField);
|
|
512
674
|
});
|
|
513
675
|
}
|
|
514
676
|
else {
|
|
515
|
-
components = processLayoutNodes(
|
|
677
|
+
components = processLayoutNodes(unified.layout);
|
|
516
678
|
}
|
|
517
679
|
return {
|
|
518
|
-
version:
|
|
519
|
-
components
|
|
680
|
+
version: unified.version,
|
|
681
|
+
components,
|
|
682
|
+
...(unified.settings ? { settings: unified.settings } : {})
|
|
520
683
|
};
|
|
521
684
|
}
|
|
522
685
|
/**
|
|
523
|
-
* Merge a single field
|
|
686
|
+
* Merge a single unified field into FormComponent
|
|
524
687
|
*/
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
|
|
688
|
+
function mergeUnifiedField(unifiedField) {
|
|
689
|
+
const hasOptions = unifiedField.options !== undefined && unifiedField.options.length > 0;
|
|
528
690
|
const component = {
|
|
529
|
-
id:
|
|
530
|
-
|
|
531
|
-
|
|
691
|
+
id: unifiedField.id,
|
|
692
|
+
name: unifiedField.name,
|
|
693
|
+
type: (unifiedField.widget || inferWidgetFromType(unifiedField.type, unifiedField.format, hasOptions)),
|
|
694
|
+
label: unifiedField.label || unifiedField.id,
|
|
532
695
|
};
|
|
533
|
-
// Copy data properties
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
696
|
+
// Copy all data and UI properties from unified field
|
|
697
|
+
const dataSchemaProps = new Set([
|
|
698
|
+
'id', 'type', 'name', 'label', 'format', 'widget', 'defaultValue', 'required', 'options', 'expressionValue',
|
|
699
|
+
'minLength', 'maxLength', 'minimum', 'maximum', 'minValue', 'maxValue',
|
|
700
|
+
'minDate', 'maxDate', 'minTime', 'maxTime', 'regex', 'pattern', 'content', 'buttonType', 'textboxType'
|
|
701
|
+
]);
|
|
702
|
+
Object.keys(unifiedField).forEach(key => {
|
|
703
|
+
if (!dataSchemaProps.has(key) && key !== 'id' && key !== 'name' && key !== 'type' && key !== 'label') {
|
|
704
|
+
component[key] = unifiedField[key];
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// Copy specific data properties
|
|
708
|
+
if (unifiedField.defaultValue !== undefined)
|
|
709
|
+
component.defaultValue = unifiedField.defaultValue;
|
|
710
|
+
if (unifiedField.expressionValue !== undefined)
|
|
711
|
+
component.expressionValue = unifiedField.expressionValue;
|
|
712
|
+
if (unifiedField.required !== undefined)
|
|
713
|
+
component.required = unifiedField.required;
|
|
714
|
+
if (unifiedField.content !== undefined)
|
|
715
|
+
component.content = unifiedField.content;
|
|
716
|
+
if (unifiedField.buttonType !== undefined)
|
|
717
|
+
component.buttonType = unifiedField.buttonType;
|
|
718
|
+
if (unifiedField.textboxType !== undefined)
|
|
719
|
+
component.textboxType = unifiedField.textboxType;
|
|
720
|
+
if (unifiedField.options !== undefined)
|
|
721
|
+
component.options = unifiedField.options;
|
|
722
|
+
if (unifiedField.minLength !== undefined)
|
|
723
|
+
component.minLength = unifiedField.minLength;
|
|
724
|
+
if (unifiedField.maxLength !== undefined)
|
|
725
|
+
component.maxLength = unifiedField.maxLength;
|
|
726
|
+
if (unifiedField.minValue !== undefined)
|
|
727
|
+
component.minValue = unifiedField.minValue;
|
|
728
|
+
if (unifiedField.maxValue !== undefined)
|
|
729
|
+
component.maxValue = unifiedField.maxValue;
|
|
730
|
+
if (unifiedField.minDate !== undefined)
|
|
731
|
+
component.minDate = unifiedField.minDate;
|
|
732
|
+
if (unifiedField.maxDate !== undefined)
|
|
733
|
+
component.maxDate = unifiedField.maxDate;
|
|
734
|
+
if (unifiedField.minTime !== undefined)
|
|
735
|
+
component.minTime = unifiedField.minTime;
|
|
736
|
+
if (unifiedField.maxTime !== undefined)
|
|
737
|
+
component.maxTime = unifiedField.maxTime;
|
|
738
|
+
if (unifiedField.pattern !== undefined)
|
|
739
|
+
component.regex = unifiedField.pattern;
|
|
572
740
|
return component;
|
|
573
741
|
}
|
|
574
742
|
/**
|
|
@@ -577,11 +745,11 @@ function mergeField(dataField, uiField) {
|
|
|
577
745
|
export function inferWidgetFromType(type, format, hasOptions) {
|
|
578
746
|
if (type === 'string') {
|
|
579
747
|
if (format === 'date')
|
|
580
|
-
return '
|
|
748
|
+
return 'date';
|
|
581
749
|
if (format === 'date-time')
|
|
582
|
-
return '
|
|
750
|
+
return 'dateTime';
|
|
583
751
|
if (format === 'time')
|
|
584
|
-
return '
|
|
752
|
+
return 'time';
|
|
585
753
|
if (format === 'email')
|
|
586
754
|
return 'email';
|
|
587
755
|
if (format === 'url')
|
|
@@ -606,13 +774,13 @@ export function inferWidgetFromType(type, format, hasOptions) {
|
|
|
606
774
|
if (type === 'sign')
|
|
607
775
|
return 'signature';
|
|
608
776
|
if (type === 'image')
|
|
609
|
-
return '
|
|
777
|
+
return 'imageEditor';
|
|
610
778
|
if (type === 'file')
|
|
611
|
-
return '
|
|
779
|
+
return 'fileUpload';
|
|
612
780
|
if (type === 'color')
|
|
613
|
-
return '
|
|
781
|
+
return 'colorPicker';
|
|
614
782
|
if (type === 'grid')
|
|
615
|
-
return '
|
|
783
|
+
return 'dataGrid';
|
|
616
784
|
if (type === 'any')
|
|
617
785
|
return 'textbox';
|
|
618
786
|
return 'textbox';
|
|
@@ -844,21 +1012,23 @@ export function mergeWithIDRemapping(newDataSchema, existingUISchema) {
|
|
|
844
1012
|
//console.log(`📐 Layout rebuilt: new fields added at end`);
|
|
845
1013
|
}
|
|
846
1014
|
}
|
|
847
|
-
// Merge
|
|
1015
|
+
// Merge to FormSchema using unified schema
|
|
1016
|
+
const unifiedProperties = {};
|
|
1017
|
+
Object.keys(remappedDataFields).forEach(fieldKey => {
|
|
1018
|
+
const dataField = remappedDataFields[fieldKey];
|
|
1019
|
+
const uiField = enhancedUIProperties[fieldKey];
|
|
1020
|
+
unifiedProperties[fieldKey] = {
|
|
1021
|
+
...dataField,
|
|
1022
|
+
...uiField,
|
|
1023
|
+
widget: uiField?.widget || inferWidgetFromType(dataField.type, dataField.format, dataField.options !== undefined)
|
|
1024
|
+
};
|
|
1025
|
+
});
|
|
848
1026
|
const mergedSchema = mergeSchema({
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
}
|
|
853
|
-
uiSchema: {
|
|
854
|
-
properties: enhancedUIProperties,
|
|
855
|
-
layout: remappedLayout
|
|
856
|
-
}
|
|
1027
|
+
version: newDataSchema.version,
|
|
1028
|
+
properties: unifiedProperties,
|
|
1029
|
+
layout: remappedLayout,
|
|
1030
|
+
...(newDataSchema.settings ? { settings: newDataSchema.settings } : {})
|
|
857
1031
|
});
|
|
858
|
-
// Log summary
|
|
859
|
-
//const remappedCount = matchResults.filter(m => m.newDataId !== m.existingId).length;
|
|
860
|
-
//const newFieldsCount = matchResults.filter(m => m.method === 'new').length;
|
|
861
|
-
//console.log(`Merge complete: ${matchResults.length} fields, ${remappedCount} remapped, ${newFieldsCount} new`);
|
|
862
1032
|
return mergedSchema;
|
|
863
1033
|
}
|
|
864
1034
|
/**
|
|
@@ -911,9 +1081,6 @@ function remapLayoutIds(layout, idMap) {
|
|
|
911
1081
|
return node;
|
|
912
1082
|
});
|
|
913
1083
|
}
|
|
914
|
-
// ============================================================================
|
|
915
|
-
// UTILITY FUNCTIONS
|
|
916
|
-
// ============================================================================
|
|
917
1084
|
/**
|
|
918
1085
|
* Extract all field IDs from layout tree
|
|
919
1086
|
*/
|
|
@@ -940,14 +1107,6 @@ export function extractFieldIdsFromLayout(layout) {
|
|
|
940
1107
|
}
|
|
941
1108
|
return fieldIds;
|
|
942
1109
|
}
|
|
943
|
-
/**
|
|
944
|
-
* Find unmatched fields (in data schema but not in layout)
|
|
945
|
-
*/
|
|
946
|
-
export function findUnmatchedFields(dataSchema, layout) {
|
|
947
|
-
const layoutFieldIds = extractFieldIdsFromLayout(layout);
|
|
948
|
-
const dataFieldIds = Object.keys(dataSchema.properties);
|
|
949
|
-
return dataFieldIds.filter(id => !layoutFieldIds.has(id));
|
|
950
|
-
}
|
|
951
1110
|
// ============================================================================
|
|
952
1111
|
// PARSER OUTPUT CONVERSION
|
|
953
1112
|
// ============================================================================
|
|
@@ -1048,133 +1207,3 @@ export function convertComponentsToDataSchema(components) {
|
|
|
1048
1207
|
properties
|
|
1049
1208
|
};
|
|
1050
1209
|
}
|
|
1051
|
-
/**
|
|
1052
|
-
* Convert parser output to a simple linear layout
|
|
1053
|
-
* Used when no existing UI schema is present
|
|
1054
|
-
*/
|
|
1055
|
-
export function convertComponentsToUISchema(components) {
|
|
1056
|
-
const properties = {};
|
|
1057
|
-
const layout = [];
|
|
1058
|
-
// Helper to recursively process components and build layout
|
|
1059
|
-
function processComponent(comp) {
|
|
1060
|
-
// Extract UI field
|
|
1061
|
-
const uiField = extractUIField(comp);
|
|
1062
|
-
properties[comp.id] = uiField;
|
|
1063
|
-
// Handle different component types
|
|
1064
|
-
if (comp.type === 'table' && comp.tableCells) {
|
|
1065
|
-
const tableCells = comp.tableCells.map((row, rowIndex) => row.map((cellComponents, colIndex) => ({
|
|
1066
|
-
row: rowIndex,
|
|
1067
|
-
col: colIndex,
|
|
1068
|
-
// cellComponents is FormComponent[], so map over it directly
|
|
1069
|
-
children: cellComponents.map(processComponent)
|
|
1070
|
-
})));
|
|
1071
|
-
return {
|
|
1072
|
-
type: 'table',
|
|
1073
|
-
id: comp.id,
|
|
1074
|
-
label: comp.label,
|
|
1075
|
-
cssClass: comp.cssClass,
|
|
1076
|
-
hideBorders: comp.hideBorders,
|
|
1077
|
-
rows: comp.tableCells.length,
|
|
1078
|
-
cols: comp.tableCells[0]?.length || 0,
|
|
1079
|
-
cells: tableCells,
|
|
1080
|
-
columnWidths: comp.columnWidths
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
if (comp.type === 'panel' && comp.children) {
|
|
1084
|
-
return {
|
|
1085
|
-
type: 'panel',
|
|
1086
|
-
id: comp.id,
|
|
1087
|
-
label: comp.label,
|
|
1088
|
-
cssClass: comp.cssClass,
|
|
1089
|
-
hideBorders: comp.hideBorders,
|
|
1090
|
-
hideLabel: comp.hideLabel,
|
|
1091
|
-
children: comp.children.map(processComponent)
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
if (comp.type === 'tabs' && comp.tabItems) {
|
|
1095
|
-
// tabs is an array of { header, children }
|
|
1096
|
-
const tabs = comp.tabItems.map((tab) => ({
|
|
1097
|
-
header: tab.header,
|
|
1098
|
-
children: tab.content?.map(processComponent) || []
|
|
1099
|
-
}));
|
|
1100
|
-
return {
|
|
1101
|
-
type: 'tabs',
|
|
1102
|
-
id: comp.id,
|
|
1103
|
-
cssClass: comp.cssClass,
|
|
1104
|
-
tabs
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
if (comp.type === 'card' && comp.children) {
|
|
1108
|
-
return {
|
|
1109
|
-
type: 'card',
|
|
1110
|
-
id: comp.id,
|
|
1111
|
-
label: comp.label,
|
|
1112
|
-
cssClass: comp.cssClass,
|
|
1113
|
-
hideLabel: comp.hideLabel,
|
|
1114
|
-
cardTitle: comp.cardTitle,
|
|
1115
|
-
cardSubtitle: comp.cardSubtitle,
|
|
1116
|
-
enableAction: comp.enableAction,
|
|
1117
|
-
actionButtonLabel: comp.actionButtonLabel,
|
|
1118
|
-
actionButtonType: comp.actionButtonType,
|
|
1119
|
-
children: comp.children.map(processComponent)
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
// Regular field
|
|
1123
|
-
return {
|
|
1124
|
-
type: 'field',
|
|
1125
|
-
propertyId: comp.id
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
// Process all root components
|
|
1129
|
-
for (const comp of components) {
|
|
1130
|
-
layout.push(processComponent(comp));
|
|
1131
|
-
}
|
|
1132
|
-
return {
|
|
1133
|
-
properties,
|
|
1134
|
-
layout
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
// ============================================================================
|
|
1138
|
-
// RENDERER HELPER FUNCTIONS
|
|
1139
|
-
// ============================================================================
|
|
1140
|
-
/**
|
|
1141
|
-
* Generate default UI properties for each data field
|
|
1142
|
-
* Used by renderers when only data schema is provided
|
|
1143
|
-
*/
|
|
1144
|
-
export function generateUIPropertiesFromData(dataSchema) {
|
|
1145
|
-
const properties = {};
|
|
1146
|
-
Object.entries(dataSchema.properties).forEach(([fieldId, dataField]) => {
|
|
1147
|
-
const hasOptions = dataField.options !== undefined && dataField.options.length > 0;
|
|
1148
|
-
const widget = inferWidgetFromType(dataField.type, dataField.format, hasOptions);
|
|
1149
|
-
properties[fieldId] = { widget };
|
|
1150
|
-
});
|
|
1151
|
-
return properties;
|
|
1152
|
-
}
|
|
1153
|
-
/**
|
|
1154
|
-
* Generate sequential layout from data schema
|
|
1155
|
-
* Used by renderers when only data schema is provided
|
|
1156
|
-
*/
|
|
1157
|
-
export function generateLayoutFromData(dataSchema) {
|
|
1158
|
-
return Object.keys(dataSchema.properties).map(fieldId => ({
|
|
1159
|
-
type: 'field',
|
|
1160
|
-
propertyId: fieldId
|
|
1161
|
-
}));
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Generate minimal data schema from UI schema
|
|
1165
|
-
* Used by renderers when only UI schema is provided
|
|
1166
|
-
*/
|
|
1167
|
-
export function generateDataSchemaFromUI(uiSchema) {
|
|
1168
|
-
const dataProperties = {};
|
|
1169
|
-
Object.entries(uiSchema.properties).forEach(([fieldId, uiField]) => {
|
|
1170
|
-
dataProperties[fieldId] = {
|
|
1171
|
-
id: fieldId,
|
|
1172
|
-
type: 'string',
|
|
1173
|
-
label: uiField.label || fieldId
|
|
1174
|
-
};
|
|
1175
|
-
});
|
|
1176
|
-
return {
|
|
1177
|
-
version: '1.0.0-beta',
|
|
1178
|
-
properties: dataProperties
|
|
1179
|
-
};
|
|
1180
|
-
}
|