@syncfusion/ej2-react-form-renderer 0.1.1-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 +2 -2
- package/src/CheckboxGroup.js +2 -1
- package/src/ConditionalRuleEngine.d.ts +43 -1
- package/src/ConditionalRuleEngine.js +77 -0
- package/src/DataGridComponent.js +8 -5
- package/src/FormRenderer.d.ts +1 -7
- package/src/FormRenderer.js +236 -178
- 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 +22 -31
- package/src/json-converter.js +141 -261
- package/src/parser.js +4 -1
- package/src/renderFormField.d.ts +2 -1
- package/src/renderFormField.js +37 -14
- package/src/types/FormSchema.d.ts +7 -1
- package/src/utils.js +262 -0
- package/styles/form-renderer.base.css +10 -0
package/src/FormRenderer.js
CHANGED
|
@@ -6,15 +6,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
7
7
|
import { Form, FormField } from './FormValidator';
|
|
8
8
|
import { enableRipple } from '@syncfusion/ej2-base';
|
|
9
|
-
import {
|
|
9
|
+
import { RenderFormField } from './renderFormField';
|
|
10
10
|
import { CustomTabComponent } from './CustomTabComponent';
|
|
11
|
-
import { mergeSchema
|
|
11
|
+
import { mergeSchema } from './json-converter';
|
|
12
12
|
import { UniversalJsonParser } from './parser';
|
|
13
13
|
import { TooltipComponent } from '@syncfusion/ej2-react-popups';
|
|
14
14
|
import { VALIDATION_REGEX } from './FormValidator';
|
|
15
15
|
import StaticHtmlRenderer from './StaticHtmlRenderer';
|
|
16
16
|
import { formatDateValues, splitWidths } from './utils';
|
|
17
17
|
import { ConditionalRuleEngine } from './ConditionalRuleEngine';
|
|
18
|
+
import { ExpressionEngine } from './expressions/ExpressionEngine';
|
|
19
|
+
import { MinMaxRuleEngine } from './MinMaxRuleEngine';
|
|
18
20
|
enableRipple(true);
|
|
19
21
|
/**
|
|
20
22
|
* Form layout options (only applies when UI schema is NOT provided)
|
|
@@ -25,147 +27,45 @@ export const FormLayout = {
|
|
|
25
27
|
ThreeColumn: 'threecolumn',
|
|
26
28
|
FourColumn: 'fourcolumn'
|
|
27
29
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Helper: Normalize DataSchema input
|
|
30
|
-
* Converts any object to JSON string and parses with UniversalJsonParser
|
|
31
|
-
* Handles FormSchema, DataSchema, or raw data - parser infers the format
|
|
32
|
-
*/
|
|
33
|
-
function normalizeDataSchema(input) {
|
|
34
|
-
try {
|
|
35
|
-
// Convert object to JSON string
|
|
36
|
-
const jsonString = JSON.stringify(input);
|
|
37
|
-
const parser = new UniversalJsonParser();
|
|
38
|
-
const parseResult = parser.parse(jsonString);
|
|
39
|
-
if (parseResult.success && parseResult.schema) {
|
|
40
|
-
const dataSchema = convertComponentsToDataSchema(parseResult.schema.components);
|
|
41
|
-
// console.log(`✅ Parsed object as ${parseResult.metadata.detectedFormat} and converted to DataSchema`);
|
|
42
|
-
return dataSchema;
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
console.error('❌ Failed to parse object:', parseResult.errors);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
console.error('❌ DataSchema normalization error:', error);
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Helper: Parse string input using UniversalJsonParser
|
|
56
|
-
* Returns DataSchema if parseable, otherwise null
|
|
57
|
-
* Validates that UI schema was not accidentally provided
|
|
58
|
-
*/
|
|
59
|
-
function parseStringToDataSchema(input) {
|
|
60
|
-
try {
|
|
61
|
-
// First check if user accidentally pasted UI schema in data schema field
|
|
62
|
-
const testParse = JSON.parse(input);
|
|
63
|
-
if (testParse.layout || (testParse.theme && testParse.properties && testParse.layout)) {
|
|
64
|
-
console.error('The provided schema is a UI Schema in the data schema prop. Please provide a valid Data Schema with field definitions and validation rules.');
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const parser = new UniversalJsonParser();
|
|
68
|
-
const parseResult = parser.parse(input);
|
|
69
|
-
if (parseResult.success && parseResult.schema) {
|
|
70
|
-
const dataSchema = convertComponentsToDataSchema(parseResult.schema.components);
|
|
71
|
-
return dataSchema;
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
console.error('Failed to parse input:', parseResult.errors);
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
console.error('Parser error:', error);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Helper: Parse string input as UISchema
|
|
85
|
-
* Validates JSON structure matches UISchema format
|
|
86
|
-
* Detects if data schema was accidentally provided
|
|
87
|
-
*/
|
|
88
|
-
function parseStringToUISchema(input) {
|
|
89
|
-
try {
|
|
90
|
-
const parsed = JSON.parse(input);
|
|
91
|
-
// Check if data schema was pasted in UI schema field
|
|
92
|
-
if (parsed.properties && !parsed.layout && !parsed.theme) {
|
|
93
|
-
const hasValidationProps = Object.values(parsed.properties).some((prop) => prop.required !== undefined || prop.minLength !== undefined || prop.maxLength !== undefined ||
|
|
94
|
-
prop.minValue !== undefined || prop.maxValue !== undefined || prop.pattern !== undefined);
|
|
95
|
-
if (hasValidationProps) {
|
|
96
|
-
console.error('The provided schema is a Data Schema in the UI schema prop. Please provide a valid UI Schema with theme, properties, and layout definitions.');
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Validate UISchema structure
|
|
101
|
-
if (!parsed.theme || !parsed.properties || !parsed.layout) {
|
|
102
|
-
console.error('Invalid UISchema: missing theme, properties, or layout');
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
return parsed;
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
console.error('JSON parse error:', error);
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
30
|
// ForwardRef to expose public methods
|
|
113
|
-
export const FormRenderer = React.forwardRef(({
|
|
31
|
+
export const FormRenderer = React.forwardRef(({ schema, locale, onSubmit, onChange, onButtonClick, className = '', layout = FormLayout.SingleColumn, created, ...restProps }, ref) => {
|
|
114
32
|
const processedSchema = React.useMemo(() => {
|
|
115
33
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
:
|
|
127
|
-
: undefined;
|
|
128
|
-
// Scenario 1: Both dataSchema and uiSchema provided - full merge
|
|
129
|
-
if (parsedDataSchema && parsedUISchema) {
|
|
130
|
-
const uiFieldIds = new Set(Object.keys(parsedUISchema.properties));
|
|
131
|
-
const dataFieldIds = Object.keys(parsedDataSchema.properties);
|
|
132
|
-
const missingFields = dataFieldIds.filter(id => !uiFieldIds.has(id));
|
|
133
|
-
if (missingFields.length > 0) {
|
|
134
|
-
console.warn(`⚠️ Found ${missingFields.length} fields in data schema missing from UI schema`);
|
|
34
|
+
if (!schema) {
|
|
35
|
+
return { version: '1.0.0', components: [] };
|
|
36
|
+
}
|
|
37
|
+
let parsedInput = schema;
|
|
38
|
+
if (typeof schema === 'string') {
|
|
39
|
+
try {
|
|
40
|
+
parsedInput = JSON.parse(schema);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error('FormRenderer: Failed to parse schema string', err);
|
|
44
|
+
return { version: '1.0.0', components: [] };
|
|
135
45
|
}
|
|
136
|
-
const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: parsedUISchema });
|
|
137
|
-
return merged;
|
|
138
46
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
layout: generateLayoutFromData(parsedDataSchema)
|
|
144
|
-
};
|
|
145
|
-
const merged = mergeSchema({ dataSchema: parsedDataSchema, uiSchema: autoUISchema });
|
|
146
|
-
return merged;
|
|
47
|
+
const isUnifiedSchema = parsedInput && typeof parsedInput === 'object' &&
|
|
48
|
+
parsedInput.properties && Array.isArray(parsedInput.layout);
|
|
49
|
+
if (isUnifiedSchema) {
|
|
50
|
+
return mergeSchema(parsedInput);
|
|
147
51
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.warn(
|
|
52
|
+
else {
|
|
53
|
+
const parser = new UniversalJsonParser();
|
|
54
|
+
const parseResult = parser.parse(typeof schema === 'string' ? schema : JSON.stringify(schema));
|
|
55
|
+
if (parseResult.success && parseResult.schema) {
|
|
56
|
+
return parseResult.schema;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.warn('FormRenderer: Failed to parse schema', parseResult.errors);
|
|
60
|
+
return { version: '1.0.0', components: [] };
|
|
156
61
|
}
|
|
157
|
-
const merged = mergeSchema({ dataSchema: autoDataSchema, uiSchema: parsedUISchema });
|
|
158
|
-
return merged;
|
|
159
62
|
}
|
|
160
|
-
// Scenario 4: Neither provided - return empty form
|
|
161
|
-
console.warn('FormRenderer: No schema provided (dataSchema or uiSchema)');
|
|
162
|
-
return { version: '0.1.0', theme: 'tailwind3', components: [] };
|
|
163
63
|
}
|
|
164
64
|
catch (error) {
|
|
165
65
|
console.error('FormRenderer schema processing error:', error);
|
|
166
|
-
return { version: '
|
|
66
|
+
return { version: '1.0.0', components: [] };
|
|
167
67
|
}
|
|
168
|
-
}, [
|
|
68
|
+
}, [schema]);
|
|
169
69
|
const [formState, setFormState] = useState(undefined);
|
|
170
70
|
const isInitializedRef = useRef(false);
|
|
171
71
|
const formLocale = locale || 'en-US';
|
|
@@ -175,6 +75,12 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
175
75
|
const inputRefs = useRef({});
|
|
176
76
|
// Track which fields have active conditional values applied (to prevent re-applying while condition stays true)
|
|
177
77
|
const conditionalValueAppliedRef = useRef({});
|
|
78
|
+
// Expression Engine initialization
|
|
79
|
+
const expressionEngineRef = useRef(null);
|
|
80
|
+
const expressionDependencyGraphRef = useRef({});
|
|
81
|
+
const expressionEvaluationInProgressRef = useRef(false);
|
|
82
|
+
const previousFormValuesRef = useRef({});
|
|
83
|
+
const computedFieldNamesRef = useRef(new Set());
|
|
178
84
|
// Check if UI schema has layout components (table, tabs, or panel)
|
|
179
85
|
const hasLayoutComponents = React.useMemo(() => {
|
|
180
86
|
if (!processedSchema.components)
|
|
@@ -351,7 +257,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
351
257
|
const componentIds = new Set();
|
|
352
258
|
allComponents.forEach(comp => {
|
|
353
259
|
// Do NOT store dataGrid data inside the form state; grids manage their own internal data.
|
|
354
|
-
if (comp.type === 'dataGrid' || comp.type === 'richTextEditor') {
|
|
260
|
+
if (comp.type === 'dataGrid' || comp.type === 'richTextEditor' || comp.type === 'staticHtml') {
|
|
355
261
|
componentIds.add(comp.id);
|
|
356
262
|
return;
|
|
357
263
|
}
|
|
@@ -375,7 +281,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
375
281
|
});
|
|
376
282
|
}
|
|
377
283
|
return values;
|
|
378
|
-
}, [processedSchema, collectAllComponents,
|
|
284
|
+
}, [processedSchema, collectAllComponents, schema]);
|
|
379
285
|
// Update the useEffect that sets default values
|
|
380
286
|
useEffect(() => {
|
|
381
287
|
if (formState && !isInitializedRef.current) {
|
|
@@ -390,6 +296,167 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
390
296
|
});
|
|
391
297
|
}
|
|
392
298
|
}, [formState, processedSchema, initialValues]);
|
|
299
|
+
/**
|
|
300
|
+
* Initialize Expression Engine and build dependency graph
|
|
301
|
+
* Runs when schema changes
|
|
302
|
+
*/
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (!processedSchema.components || processedSchema.components.length === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
// Initialize expression engine if not already done
|
|
309
|
+
if (!expressionEngineRef.current) {
|
|
310
|
+
expressionEngineRef.current = new ExpressionEngine();
|
|
311
|
+
}
|
|
312
|
+
const engine = expressionEngineRef.current;
|
|
313
|
+
const allComponents = collectAllComponents(processedSchema.components);
|
|
314
|
+
// Build dependency graph from all expressions
|
|
315
|
+
const graph = engine.buildDependencyGraph(allComponents);
|
|
316
|
+
// Check for circular dependencies
|
|
317
|
+
const cycles = engine.detectCircularDependencies(graph);
|
|
318
|
+
if (cycles.length > 0) {
|
|
319
|
+
//console.warn('⚠️ Circular dependencies detected in expressions:', cycles);
|
|
320
|
+
}
|
|
321
|
+
// Store dependency graph
|
|
322
|
+
expressionDependencyGraphRef.current = graph;
|
|
323
|
+
// Track which fields are computed by expressions
|
|
324
|
+
const computedFields = new Set();
|
|
325
|
+
allComponents.forEach(comp => {
|
|
326
|
+
if (comp.expressionValue) {
|
|
327
|
+
computedFields.add(comp.id);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
computedFieldNamesRef.current = computedFields;
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error('Error initializing expression engine:', error);
|
|
334
|
+
}
|
|
335
|
+
}, [processedSchema, collectAllComponents]);
|
|
336
|
+
/**
|
|
337
|
+
* Evaluate expressions when form values change
|
|
338
|
+
* Runs expressions in topological order to ensure dependencies are satisfied
|
|
339
|
+
*/
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (!formState || !expressionEngineRef.current) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Prevent recursive evaluation loops
|
|
345
|
+
if (expressionEvaluationInProgressRef.current) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
expressionEvaluationInProgressRef.current = true;
|
|
350
|
+
const engine = expressionEngineRef.current;
|
|
351
|
+
const dependencyGraph = expressionDependencyGraphRef.current;
|
|
352
|
+
const formValues = formState.values || {};
|
|
353
|
+
const allComponents = collectAllComponents(processedSchema.components);
|
|
354
|
+
const previousValues = previousFormValuesRef.current;
|
|
355
|
+
const computedFieldNames = computedFieldNamesRef.current;
|
|
356
|
+
// Create a mapping of component id -> component name for context building
|
|
357
|
+
const idToNameMap = {};
|
|
358
|
+
allComponents.forEach(comp => {
|
|
359
|
+
if (comp.name) {
|
|
360
|
+
idToNameMap[comp.id] = comp.name;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// Convert formValues from id-based keys to name-based keys for expression context
|
|
364
|
+
const contextValues = {};
|
|
365
|
+
Object.entries(formValues).forEach(([componentId, value]) => {
|
|
366
|
+
const fieldName = idToNameMap[componentId];
|
|
367
|
+
if (fieldName) {
|
|
368
|
+
contextValues[fieldName] = value;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
// Capture previous state before checking for changes
|
|
372
|
+
const previousContextValues = { ...previousValues };
|
|
373
|
+
// Update previous values immediately with current context for next cycle
|
|
374
|
+
// This ensures stale values don't affect the next evaluation cycle
|
|
375
|
+
Object.assign(previousValues, contextValues);
|
|
376
|
+
// Find which fields have changed (using captured previous state)
|
|
377
|
+
const changedFields = new Set();
|
|
378
|
+
Object.keys(contextValues).forEach(fieldName => {
|
|
379
|
+
const fieldId = Object.keys(idToNameMap).find(id => idToNameMap[id] === fieldName);
|
|
380
|
+
if (fieldId && contextValues[fieldName] !== previousContextValues[fieldName] && !computedFieldNames.has(fieldId)) {
|
|
381
|
+
changedFields.add(fieldName);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// If no non-computed fields changed, skip evaluation
|
|
385
|
+
if (changedFields.size === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// Get topological order for evaluation
|
|
389
|
+
const evaluationOrder = engine.getTopologicalOrder(dependencyGraph);
|
|
390
|
+
evaluationOrder.forEach(fieldName => {
|
|
391
|
+
// Find component with this name
|
|
392
|
+
const component = allComponents.find(c => c.name === fieldName);
|
|
393
|
+
if (component && component.expressionValue) {
|
|
394
|
+
// Mark this field as computed
|
|
395
|
+
computedFieldNames.add(component.id);
|
|
396
|
+
try {
|
|
397
|
+
// Only evaluate if field is visible and not read-only
|
|
398
|
+
const isVisible = component.visible !== false
|
|
399
|
+
? ConditionalRuleEngine.isFieldVisible(component.conditions, formValues)
|
|
400
|
+
: false;
|
|
401
|
+
if (isVisible) {
|
|
402
|
+
const currentValues = {};
|
|
403
|
+
Object.entries(formValues).forEach(([componentId, value]) => {
|
|
404
|
+
const fieldName = idToNameMap[componentId];
|
|
405
|
+
if (fieldName) {
|
|
406
|
+
currentValues[fieldName] = value;
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
// Use context values (name-based) for evaluation
|
|
410
|
+
let result = engine.evaluate(component.expressionValue, currentValues);
|
|
411
|
+
result = typeof (result) === 'string' && result.includes('undefined') ? result.replace(/undefined/g, '') : result;
|
|
412
|
+
// Coerce result to appropriate type based on field type
|
|
413
|
+
if (result !== null && result !== undefined && result !== '') {
|
|
414
|
+
switch (component.type) {
|
|
415
|
+
case 'number':
|
|
416
|
+
// Ensure numeric fields return numbers (but preserve empty string)
|
|
417
|
+
const numResult = Number(result);
|
|
418
|
+
result = isNaN(numResult) ? 0 : numResult;
|
|
419
|
+
result = result === 0 ? "" : result;
|
|
420
|
+
break;
|
|
421
|
+
case 'checkbox':
|
|
422
|
+
case 'switch':
|
|
423
|
+
// Ensure boolean fields return booleans
|
|
424
|
+
result = Boolean(result);
|
|
425
|
+
break;
|
|
426
|
+
case 'textbox':
|
|
427
|
+
case 'textarea':
|
|
428
|
+
// Ensure text fields return strings
|
|
429
|
+
result = String(result);
|
|
430
|
+
break;
|
|
431
|
+
case 'dateRange':
|
|
432
|
+
case 'checkboxGroup':
|
|
433
|
+
case "multiselect":
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
// Other types use result as-is
|
|
437
|
+
result = String(result);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// Apply result if different from current value
|
|
442
|
+
if (result !== null && result !== undefined && result !== formValues[component.id]) {
|
|
443
|
+
formState.values[component.id] = result;
|
|
444
|
+
formState.onChange(component.id, { value: result });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.error(`Error evaluating expression for field "${fieldName}":`, error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
// Update previous values for next comparison
|
|
454
|
+
Object.assign(previousValues, contextValues);
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
expressionEvaluationInProgressRef.current = false;
|
|
458
|
+
}
|
|
459
|
+
}, [formState?.values, processedSchema, collectAllComponents]);
|
|
393
460
|
/**
|
|
394
461
|
* Render a single field with label and error
|
|
395
462
|
*/
|
|
@@ -406,6 +473,21 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
406
473
|
const computedReadOnly = component.readOnly || conditionalReadOnly;
|
|
407
474
|
const computedDisabled = component.disabled || conditionalDisabled;
|
|
408
475
|
const computedRequired = component.required || conditionalRequired;
|
|
476
|
+
// Calculate min/max constraints from minMaxRange
|
|
477
|
+
let minMaxConstraints = MinMaxRuleEngine.getConstraints(component.minMaxRange, formValues, component.type);
|
|
478
|
+
// For dateTime, also get date and time constraints separately
|
|
479
|
+
if (component.type === 'dateTime') {
|
|
480
|
+
const dateConstraints = MinMaxRuleEngine.getConstraints(component.minMaxRange, formValues, 'date');
|
|
481
|
+
const timeConstraints = MinMaxRuleEngine.getConstraints(component.minMaxTimeRange, formValues, 'time');
|
|
482
|
+
minMaxConstraints = {
|
|
483
|
+
minValue: undefined,
|
|
484
|
+
maxValue: undefined,
|
|
485
|
+
minDate: dateConstraints.minDate,
|
|
486
|
+
maxDate: dateConstraints.maxDate,
|
|
487
|
+
minTime: timeConstraints.minTime,
|
|
488
|
+
maxTime: timeConstraints.maxTime
|
|
489
|
+
};
|
|
490
|
+
}
|
|
409
491
|
const labelPos = component.labelPosition || 'top';
|
|
410
492
|
const labelWidth = component.labelWidth || 'auto';
|
|
411
493
|
const showLabel = !component.hideLabel && component.label && !['checkbox', 'button', 'staticHtml'].includes(component.type);
|
|
@@ -448,38 +530,33 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
448
530
|
justifyContent: 'center',
|
|
449
531
|
flexShrink: 0,
|
|
450
532
|
} }) }))] })), _jsxs("div", { className: "field-input", children: [(component.type === 'radio' || component.type === 'checkboxGroup') && Array.isArray(component.options)
|
|
451
|
-
?
|
|
452
|
-
component: { ...component, readOnly: computedReadOnly, disabled: computedDisabled, required: computedRequired },
|
|
453
|
-
value: formState?.values[component.id],
|
|
454
|
-
onChange: (value) => {
|
|
533
|
+
? (_jsx(RenderFormField, { component: { ...component, readOnly: computedReadOnly, disabled: computedDisabled, required: computedRequired }, value: formState?.values[component.id], onChange: (value) => {
|
|
455
534
|
formState?.onChange(component.id, { value });
|
|
456
535
|
if (onChange) {
|
|
457
536
|
onChange({ field: component.label || component.id, value });
|
|
458
537
|
}
|
|
459
|
-
},
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
538
|
+
}, locale: formLocale, onBlur: () => formState?.onBlur(component.id), ref: (el, idx) => setInputRef(el, idx), hasError: hasError }))
|
|
539
|
+
: (_jsx(RenderFormField, { component: {
|
|
540
|
+
...component,
|
|
541
|
+
readOnly: computedReadOnly,
|
|
542
|
+
disabled: computedDisabled,
|
|
543
|
+
required: computedRequired,
|
|
544
|
+
...(minMaxConstraints.minValue !== undefined && { minValue: minMaxConstraints.minValue }),
|
|
545
|
+
...(minMaxConstraints.maxValue !== undefined && { maxValue: minMaxConstraints.maxValue }),
|
|
546
|
+
...(minMaxConstraints.minDate !== undefined && { minDate: minMaxConstraints.minDate }),
|
|
547
|
+
...(minMaxConstraints.maxDate !== undefined && { maxDate: minMaxConstraints.maxDate }),
|
|
548
|
+
...(minMaxConstraints.minTime !== undefined && { minTime: minMaxConstraints.minTime }),
|
|
549
|
+
...(minMaxConstraints.maxTime !== undefined && { maxTime: minMaxConstraints.maxTime })
|
|
550
|
+
}, formState: formState, value: formState?.values[component.id], onChange: (value) => {
|
|
551
|
+
if (component.type !== 'dataGrid' && component.type !== 'signature' && component.type !== 'richTextEditor' && component.type !== 'staticHtml') {
|
|
470
552
|
value = formatDateValues(component, value);
|
|
471
553
|
formState?.onChange(component.id, { value });
|
|
472
554
|
if (onChange) {
|
|
473
555
|
onChange({ field: component.label || component.id, value });
|
|
474
556
|
}
|
|
475
557
|
}
|
|
476
|
-
},
|
|
477
|
-
|
|
478
|
-
onBlur: () => { if (component.type !== 'dataGrid' && component.type !== 'richTextEditor')
|
|
479
|
-
formState?.onBlur(component.id); },
|
|
480
|
-
ref: setInputRef,
|
|
481
|
-
hasError,
|
|
482
|
-
}), hasError && _jsx("div", { className: "e-error", children: formState.errors[component.id] })] }), component.description && _jsx("div", { className: 'field-description', children: component.description })] }) }) }, component.id));
|
|
558
|
+
}, locale: formLocale, onBlur: () => { if (component.type !== 'dataGrid' && component.type !== 'richTextEditor' && component.type !== 'staticHtml')
|
|
559
|
+
formState?.onBlur(component.id); }, ref: setInputRef, hasError: hasError })), hasError && _jsx("div", { className: "e-error", children: formState.errors[component.id] })] }), component.description && _jsx("div", { className: 'field-description', children: component.description })] }) }) }, component.id));
|
|
483
560
|
};
|
|
484
561
|
/**
|
|
485
562
|
* Render component (including layouts)
|
|
@@ -525,12 +602,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
525
602
|
case 'staticHtml':
|
|
526
603
|
return (_jsx(StaticHtmlRenderer, { id: component.id, content: component.defaultValue || '<p>HTML content</p>', enableHtmlSanitizer: component.enableHtmlSanitizer }, component.id));
|
|
527
604
|
case 'message':
|
|
528
|
-
return (_jsx("div", { className: "form-group", children:
|
|
529
|
-
component,
|
|
530
|
-
locale: formLocale,
|
|
531
|
-
value: undefined,
|
|
532
|
-
onChange: () => { },
|
|
533
|
-
}) }, component.id));
|
|
605
|
+
return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: component, locale: formLocale, value: undefined, onChange: () => { } }) }, component.id));
|
|
534
606
|
case 'button':
|
|
535
607
|
const buttonlDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
|
|
536
608
|
const buttonComputed = component.disabled || buttonlDisabled;
|
|
@@ -542,21 +614,14 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
542
614
|
});
|
|
543
615
|
}
|
|
544
616
|
};
|
|
545
|
-
return (_jsx("div", { className: "form-group", children:
|
|
546
|
-
component: { ...component, disabled: buttonComputed },
|
|
547
|
-
value: undefined,
|
|
548
|
-
onChange: () => { },
|
|
549
|
-
locale: formLocale,
|
|
550
|
-
onClick: (e) => { handleButtonClick(e); },
|
|
551
|
-
ref: (el, idx) => {
|
|
617
|
+
return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: { ...component, disabled: buttonComputed }, value: undefined, onChange: () => { }, locale: formLocale, onClick: (e) => { handleButtonClick(e); }, ref: (el, idx) => {
|
|
552
618
|
if (idx !== undefined) {
|
|
553
619
|
inputRefs.current[`${component.id}_${idx}`] = el;
|
|
554
620
|
}
|
|
555
621
|
else {
|
|
556
622
|
inputRefs.current[component.id] = el;
|
|
557
623
|
}
|
|
558
|
-
}
|
|
559
|
-
}) }, component.id));
|
|
624
|
+
} }) }, component.id));
|
|
560
625
|
case 'splitButton':
|
|
561
626
|
const splitButtonDisabled = ConditionalRuleEngine.isFieldDisabled(component.conditions, formValues);
|
|
562
627
|
const computedDisabled = component.disabled || splitButtonDisabled;
|
|
@@ -574,14 +639,7 @@ export const FormRenderer = React.forwardRef(({ dataSchema, uiSchema, locale, on
|
|
|
574
639
|
});
|
|
575
640
|
}
|
|
576
641
|
};
|
|
577
|
-
return (_jsx("div", { className: "form-group", children:
|
|
578
|
-
component: { ...component, disabled: computedDisabled },
|
|
579
|
-
value: undefined,
|
|
580
|
-
locale: formLocale,
|
|
581
|
-
onChange: () => { },
|
|
582
|
-
onClick: (e) => { handleSplitButtonEvent(e); },
|
|
583
|
-
ref: setInputRef
|
|
584
|
-
}) }, component.id));
|
|
642
|
+
return (_jsx("div", { className: "form-group", children: _jsx(RenderFormField, { component: { ...component, disabled: computedDisabled }, value: undefined, locale: formLocale, onChange: () => { }, onClick: (e) => { handleSplitButtonEvent(e); }, ref: setInputRef }) }, component.id));
|
|
585
643
|
default:
|
|
586
644
|
return renderField(component);
|
|
587
645
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export interface MinMaxRangeItem {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
minValue?: number | string | {
|
|
4
|
+
field: string;
|
|
5
|
+
};
|
|
6
|
+
maxValue?: number | string | {
|
|
7
|
+
field: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface MinMaxConstraints {
|
|
11
|
+
minValue?: number | string;
|
|
12
|
+
maxValue?: number | string;
|
|
13
|
+
minDate?: string;
|
|
14
|
+
maxDate?: string;
|
|
15
|
+
minTime?: string;
|
|
16
|
+
maxTime?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class MinMaxRuleEngine {
|
|
19
|
+
/**
|
|
20
|
+
* Extract min/max constraints from minMaxRange array
|
|
21
|
+
* Handles three cases:
|
|
22
|
+
* 1. Direct values (enabled: false)
|
|
23
|
+
* 2. Single field reference (enabled: true, 1 range)
|
|
24
|
+
* 3. Multiple ranges (enabled: true, 2+ ranges) - takes UTMOST
|
|
25
|
+
*
|
|
26
|
+
* @param minMaxRange - Array of range configurations
|
|
27
|
+
* @param formValues - Current form state values (id → value)
|
|
28
|
+
* @param componentType - 'number' | 'date' | 'dateRange'
|
|
29
|
+
* @returns Constraints object with appropriate keys
|
|
30
|
+
*/
|
|
31
|
+
static getConstraints(minMaxRange: MinMaxRangeItem[] | undefined, formValues: Record<string, any>, componentType?: string): MinMaxConstraints;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a single value - either direct or field reference
|
|
34
|
+
*
|
|
35
|
+
* @param value - Direct value or field reference object
|
|
36
|
+
* @param formValues - Form state values for field lookup
|
|
37
|
+
* @returns Resolved value or undefined
|
|
38
|
+
*/
|
|
39
|
+
private static resolveValue;
|
|
40
|
+
/**
|
|
41
|
+
* Get the most permissive (widest) minimum value
|
|
42
|
+
* For numbers: MIN of all mins
|
|
43
|
+
* For dates: earliest date (MIN string comparison on ISO format)
|
|
44
|
+
* For times: earliest time (MIN string comparison on 24-hour format)
|
|
45
|
+
*
|
|
46
|
+
* @param ranges - Array of {min, max} values
|
|
47
|
+
* @param componentType - Type of component
|
|
48
|
+
* @returns Most permissive minimum value
|
|
49
|
+
*/
|
|
50
|
+
private static getPermissiveMin;
|
|
51
|
+
/**
|
|
52
|
+
* Get the most permissive (widest) maximum value
|
|
53
|
+
* For numbers: MAX of all maxs
|
|
54
|
+
* For dates: latest date (MAX string comparison on ISO format)
|
|
55
|
+
* For times: latest time (MAX string comparison on 24-hour format)
|
|
56
|
+
*
|
|
57
|
+
* @param ranges - Array of {min, max} values
|
|
58
|
+
* @param componentType - Type of component
|
|
59
|
+
* @returns Most permissive maximum value
|
|
60
|
+
*/
|
|
61
|
+
private static getPermissiveMax;
|
|
62
|
+
/**
|
|
63
|
+
* Convert MM/DD/YYYY date format to ISO (YYYY-MM-DD) for comparison
|
|
64
|
+
* Handles both formats gracefully
|
|
65
|
+
*
|
|
66
|
+
* @param dateString - Date in MM/DD/YYYY or ISO format
|
|
67
|
+
* @returns ISO date string (YYYY-MM-DD)
|
|
68
|
+
*/
|
|
69
|
+
private static convertToISO;
|
|
70
|
+
/**
|
|
71
|
+
* Convert 12-hour time format to 24-hour format for comparison
|
|
72
|
+
* Handles both 12-hour (hh:mm a) and 24-hour (HH:mm) formats
|
|
73
|
+
*
|
|
74
|
+
* @param timeString - Time in 12-hour (hh:mm a/p) or 24-hour (HH:mm) format
|
|
75
|
+
* @returns 24-hour format time string (HH:mm)
|
|
76
|
+
*/
|
|
77
|
+
private static convertTimeTo24Hour;
|
|
78
|
+
/**
|
|
79
|
+
* Get empty constraints object based on component type
|
|
80
|
+
*/
|
|
81
|
+
private static getEmptyConstraints;
|
|
82
|
+
/**
|
|
83
|
+
* Validate if a value passes all min/max constraints (optional)
|
|
84
|
+
* Can be used for client-side validation before submission
|
|
85
|
+
*
|
|
86
|
+
* @param value - Value to validate
|
|
87
|
+
* @param constraints - Constraints to check against
|
|
88
|
+
* @param componentType - Type of component
|
|
89
|
+
* @returns Validation result with error message if invalid
|
|
90
|
+
*/
|
|
91
|
+
static validateValue(value: any, constraints: MinMaxConstraints, componentType: string): {
|
|
92
|
+
isValid: boolean;
|
|
93
|
+
error?: string;
|
|
94
|
+
};
|
|
95
|
+
}
|