@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/renderFormField.js
CHANGED
|
@@ -12,8 +12,9 @@ import { MessageComponent } from '@syncfusion/ej2-react-notifications';
|
|
|
12
12
|
import { ImageEditorComponent } from '@syncfusion/ej2-react-image-editor';
|
|
13
13
|
import { DataGridComponent } from './DataGridComponent';
|
|
14
14
|
import { RichTextEditorComponent, Toolbar, HtmlEditor, Inject as RTEInject, Link, QuickToolbar } from '@syncfusion/ej2-react-richtexteditor';
|
|
15
|
-
import { mapButtonStyleToClass, mapPositionToJustifyContent, resolveButtonType } from './utils';
|
|
15
|
+
import { mapButtonStyleToClass, mapPositionToJustifyContent, resolveButtonType, resolveConditionalData, resolveOptions, useFetchOptions } from './utils';
|
|
16
16
|
import CheckboxGroup from './CheckboxGroup';
|
|
17
|
+
import { ConditionalRuleEngine } from './ConditionalRuleEngine';
|
|
17
18
|
/**
|
|
18
19
|
* Converts htmlAttributes from array format to object format
|
|
19
20
|
*/
|
|
@@ -33,7 +34,7 @@ const normalizeAttributes = (attributes) => {
|
|
|
33
34
|
/**
|
|
34
35
|
* Renders a single form field component
|
|
35
36
|
*/
|
|
36
|
-
export const
|
|
37
|
+
export const RenderFormField = ({ component, value, onChange, onBlur, ref, hasError = false, onClick, locale, formState }) => {
|
|
37
38
|
const commonProps = {
|
|
38
39
|
floatLabelType: (component.floatLabelType || 'Never'),
|
|
39
40
|
placeholder: component.placeholder || '',
|
|
@@ -63,38 +64,54 @@ export const renderFormField = ({ component, value, onChange, onBlur, ref, hasEr
|
|
|
63
64
|
case 'textarea':
|
|
64
65
|
return (_jsx(TextAreaComponent, { id: component.id, name: component.id, locale: locale, ...commonProps, value: value !== undefined ? value : (component.defaultValue || ''), cols: component.cols || 200, rows: component.textareaRows || 4, showClearButton: component.showClearButton || false, readonly: component.readOnly || false, maxLength: component?.maxLength || 255, cssClass: cssClass, htmlAttributes: htmlAttrs, appendTemplate: component.suffix, prependTemplate: component.prefix, resizeMode: 'Vertical', ...createValidationHandlers((args) => args.value || ''), ref: ref }, component.id));
|
|
65
66
|
case 'number':
|
|
66
|
-
|
|
67
|
+
value = value === "" ? undefined : value;
|
|
68
|
+
return (_jsx(NumericTextBoxComponent, { id: component.id, name: component.id, locale: locale, ...commonProps, value: value !== undefined || value !== "" ? value : (component.defaultValue || 0), decimals: component.decimals || 2, currency: component.currency, format: component.numberFormat || 'n2', step: component.step || 1, min: component.minValue || undefined, max: component.maxValue || undefined, readonly: component.readOnly || false, showClearButton: component.showClearButton || false, cssClass: cssClass, htmlAttributes: htmlAttrs, appendTemplate: component.suffix, prependTemplate: component.prefix, ...createValidationHandlers((args) => args.value || ''), ref: ref }, `${component.id}-${component.minValue}-${component.maxValue}`));
|
|
67
69
|
case 'checkbox':
|
|
68
70
|
const checkboxLabel = component.label && component.required
|
|
69
71
|
? `${component.label} *`
|
|
70
72
|
: component.label;
|
|
71
|
-
return (_jsx(CheckBoxComponent, { id: component.id, locale: locale, name: component.id, label: checkboxLabel, labelPosition: component.checkboxLabelPosition, checked: component.
|
|
73
|
+
return (_jsx(CheckBoxComponent, { id: component.id, locale: locale, name: component.id, label: checkboxLabel, labelPosition: component.checkboxLabelPosition, checked: (component.checked ?? false) || (value ?? false), indeterminate: component.indeterminate || false, disabled: component.disabled, cssClass: cssClass, htmlAttributes: htmlAttrs, change: (args) => onChange(args.checked), ref: ref }, component.id));
|
|
72
74
|
case 'checkboxGroup':
|
|
73
|
-
|
|
75
|
+
const fetchedCheckboxOptions = useFetchOptions(component.options);
|
|
76
|
+
const resolvedCheckboxOptions = resolveOptions(component.options, fetchedCheckboxOptions);
|
|
77
|
+
const finalCheckboxOptions = resolvedCheckboxOptions.length > 0 ? resolvedCheckboxOptions : component.options || ['Option 1'];
|
|
78
|
+
return (_jsx(CheckboxGroup, { component: { ...component, options: finalCheckboxOptions }, value: value, onChange: (v) => onChange(v), optionRef: ref }, component.id));
|
|
74
79
|
case 'radio':
|
|
75
|
-
return (_jsx("div", { id: `${component.id}-group`, className: `e-radio-group ${errorClass}`, children: (component.options
|
|
80
|
+
return (_jsx("div", { id: `${component.id}-group`, className: `e-radio-group ${errorClass}`, children: (Array.isArray(component.options) ? component.options : ['Option 1']).map((option, idx) => {
|
|
76
81
|
const optionValue = option;
|
|
77
82
|
const optionText = option;
|
|
78
|
-
return (_jsx("div", { style: { marginBottom: '8px' }, children: _jsx(RadioButtonComponent, { id: `${component.id}_${idx}_${optionValue}`, name: component.id, label: optionText, locale: locale, disabled: component.disabled, value: optionValue, htmlAttributes: htmlAttrs, labelPosition: component.fieldLabelPosition, checked: component.defaultValue === optionValue, cssClass: cssClass, change: (args) => {
|
|
83
|
+
return (_jsx("div", { style: { marginBottom: '8px' }, children: _jsx(RadioButtonComponent, { id: `${component.id}_${idx}_${optionValue}`, name: component.id, label: optionText, locale: locale, disabled: component.disabled, value: optionValue, htmlAttributes: htmlAttrs, labelPosition: component.fieldLabelPosition, checked: component.defaultValue === optionValue || value === optionValue, cssClass: cssClass, change: (args) => {
|
|
79
84
|
if (args.value === optionValue) {
|
|
80
85
|
onChange(optionValue);
|
|
81
86
|
}
|
|
82
87
|
}, ref: ref ? (el) => ref(el, idx) : undefined }, component.id) }, idx));
|
|
83
88
|
}) }));
|
|
84
89
|
case 'dropdown':
|
|
85
|
-
|
|
90
|
+
const fetchedDropdownOptions = useFetchOptions(component.options);
|
|
91
|
+
const resolvedDropdownOptions = resolveOptions(component.options, fetchedDropdownOptions);
|
|
92
|
+
let finalDropdownOptions = resolvedDropdownOptions.length > 0 ? resolvedDropdownOptions : component.options || ['Option 1'];
|
|
93
|
+
if (component.conditions?.choiceBasedField) {
|
|
94
|
+
finalDropdownOptions = ConditionalRuleEngine.getChoiceBasedOptions(component.conditions.choiceBasedField, finalDropdownOptions, formState?.values, component.conditions.choiceBasedField.primaryFieldId);
|
|
95
|
+
if (finalDropdownOptions.length > 0 && value != null && !finalDropdownOptions.includes(value)) {
|
|
96
|
+
formState.onChange(component.id, { value: null });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return (_jsx(DropDownListComponent, { id: component.id, name: component.id, ...commonProps, locale: locale, cssClass: cssClass, dataSource: finalDropdownOptions, value: value !== undefined ? value : (component.defaultValue ?? null), showClearButton: component.showClearButton || false, readonly: component.readOnly || false, enableVirtualization: component.enableVirtualization || false, popupHeight: component.popupHeight || '300px', popupWidth: component.popupWidth || '100%', htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref }, component.id));
|
|
86
100
|
case 'multiselect':
|
|
87
|
-
|
|
101
|
+
const fetchedMultiSelectOptions = useFetchOptions(component.options);
|
|
102
|
+
const resolvedMultiSelectOptions = resolveOptions(component.options, fetchedMultiSelectOptions);
|
|
103
|
+
const finalMultiSelectOptions = resolvedMultiSelectOptions.length > 0 ? resolvedMultiSelectOptions : component.options || ['Option 1'];
|
|
104
|
+
return (_jsx(MultiSelectComponent, { id: component.id, name: component.id, locale: locale, ...commonProps, value: value !== undefined ? value : (component.defaultValue ?? null), placeholder: component.placeholder, dataSource: finalMultiSelectOptions, fields: { text: 'text', value: 'value' }, showClearButton: component.showClearButton || false, readonly: component.readOnly || false, allowCustomValue: component.allowCustomValue || false, addTagOnBlur: component.addTagOnBlur || false, delimiterChar: component.delimiterChar || ',', showSelectAll: component.showSelectAll || false, mode: component.showSelectAll ? 'CheckBox' : undefined, selectAllText: component.selectAllText, unSelectAllText: component.unSelectAllText, showDropDownIcon: component.showDropDownIcon !== false, enableVirtualization: component.enableVirtualization || false, popupHeight: component.popupHeight || '300px', popupWidth: component.popupWidth || '100%', cssClass: cssClass, htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref, children: _jsx(Inject, { services: [CheckBoxSelection] }) }, component.id));
|
|
88
105
|
case 'date':
|
|
89
|
-
return (_jsx(DatePickerComponent, { ...commonProps, id: component.id, name: component.id, locale: locale, value: value !== undefined ? value : (component.defaultValue ?? null), floatLabelType: component.floatLabelType || 'Never', format: component.format || 'MM/dd/yyyy', placeholder: component.placeholder || '', readOnly: component.readOnly || false, showClearButton: component.showClearButton !== false ? true : false, showTodayButton: component.showTodayButton !== false ? true : false, calendarMode: component.calendarMode || 'Gregorian', dayHeaderFormat: component.dayHeaderFormat || 'Short', depth: component.depth || 'Month', start: component.start || 'Month', firstDayOfWeek: component.firstDayOfWeek || 0, min: component?.minDate ? new Date(component.minDate) : undefined, max: component?.maxDate ? new Date(component.maxDate) : undefined, cssClass: cssClass, htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref, children: _jsx(Inject, { services: [Islamic] }) }, component.id));
|
|
106
|
+
return (_jsx(DatePickerComponent, { ...commonProps, id: component.id, name: component.id, locale: locale, value: value !== undefined ? value : (component.defaultValue ?? null), floatLabelType: component.floatLabelType || 'Never', format: component.format || 'MM/dd/yyyy', placeholder: component.placeholder || '', readOnly: component.readOnly || false, showClearButton: component.showClearButton !== false ? true : false, showTodayButton: component.showTodayButton !== false ? true : false, calendarMode: component.calendarMode || 'Gregorian', dayHeaderFormat: component.dayHeaderFormat || 'Short', depth: component.depth || 'Month', start: component.start || 'Month', firstDayOfWeek: component.firstDayOfWeek || 0, min: component?.minDate ? new Date(component.minDate) : undefined, max: component?.maxDate ? new Date(component.maxDate) : undefined, cssClass: cssClass, htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref, children: _jsx(Inject, { services: [Islamic] }) }, `${component.id}-${component.minDate}-${component.maxDate}`));
|
|
90
107
|
case 'dateTime':
|
|
91
|
-
return (_jsx(DateTimePickerComponent, {
|
|
108
|
+
return (_jsx(DateTimePickerComponent, { id: `${component.id}-${component.minTime}-${component.maxTime}-${component.minDate}-${component.maxDate}`, ...commonProps, locale: locale, name: component.id, floatLabelType: component.floatLabelType || 'Never', dayHeaderFormat: component.dayHeaderFormat || 'Short', value: value !== undefined ? value : (component.defaultValue ?? null), format: component.format || 'MM/dd/yyyy hh:mm a', showClearButton: component.showClearButton !== false, showTodayButton: component.showTodayButton !== false ? true : false, depth: component.depth || 'Month', start: component.start || 'Month', step: component.step || 1, readonly: component.readOnly || false, min: component?.minDate ? new Date(component.minDate) : undefined, max: component?.maxDate ? new Date(component.maxDate) : undefined, minTime: component?.minTime ? new Date(`2000-01-01 ${component.minTime}`) : undefined, maxTime: component?.maxTime ? new Date(`2000-01-01 ${component.maxTime}`) : undefined, enableMask: false, cssClass: cssClass, htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref, children: _jsx(Inject, { services: [Islamic] }) }));
|
|
92
109
|
case 'time':
|
|
93
|
-
return (_jsx(TimePickerComponent, { id: component.id
|
|
110
|
+
return (_jsx(TimePickerComponent, { id: `${component.id}-${component.minTime}-${component.maxTime}`, locale: locale, name: component.id, floatLabelType: component.floatLabelType || 'Never', placeholder: component.placeholder || '', enabled: !component.disabled, value: value !== undefined ? value : (component.defaultValue ?? null), format: component.format || 'hh:mm a', showClearButton: component.showClearButton !== false, step: component.step || 30, readonly: component.readOnly || false, min: component?.minTime ? new Date(`2000-01-01 ${component.minTime}`) : undefined, max: component?.maxTime ? new Date(`2000-01-01 ${component.maxTime}`) : undefined, enableMask: false, cssClass: cssClass, htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref }, component.id));
|
|
94
111
|
case 'dateRange':
|
|
95
112
|
return (_jsx(DateRangePickerComponent, { id: component.id, locale: locale, name: component.id, floatLabelType: component.floatLabelType || 'Never', dayHeaderFormat: component.dayHeaderFormat || 'Short', placeholder: component.placeholder || '', enabled: !component.disabled, value: value !== undefined ? value : (component.defaultValue ?? null), depth: component.depth || 'Month', start: component.start || 'Month', min: component?.minDate ? new Date(component.minDate) : undefined, max: component?.maxDate ? new Date(component.maxDate) : undefined, format: component.format || 'MM/dd/yyyy', separator: component.separator || ' - ', showClearButton: component.showClearButton !== false, readonly: component.readOnly || false, cssClass: cssClass, htmlAttributes: htmlAttrs, ...createValidationHandlers((args) => args.value || ''), ref: ref }, component.id));
|
|
96
113
|
case 'switch':
|
|
97
|
-
return (_jsx(SwitchComponent, { id: component.id, locale: locale, value: component.defaultValue, name: component.id, checked: component.
|
|
114
|
+
return (_jsx(SwitchComponent, { id: component.id, locale: locale, value: component.defaultValue, name: component.id, checked: (component.checked ?? false) || (value ?? false), disabled: component.disabled, onLabel: component.onLabel || '', offLabel: component.offLabel || '', cssClass: cssClass, htmlAttributes: htmlAttrs, change: (args) => onChange(args.checked), ref: ref }, component.id));
|
|
98
115
|
case 'button':
|
|
99
116
|
// If buttonsGroups is provided, render a button group in-place of a single button
|
|
100
117
|
const groupButtons = (component.buttonsGroups || component.buttons || []);
|
|
@@ -190,7 +207,13 @@ export const renderFormField = ({ component, value, onChange, onBlur, ref, hasEr
|
|
|
190
207
|
return (_jsx(MessageComponent, { id: component.id, locale: locale, severity: component.severity || component.messageType || 'info', variant: component.variant, content: component.content || 'Message', showCloseIcon: component.showCloseIcon, showIcon: (component.showIcon === undefined ? false : component.showIcon) || false, disabled: component.disabled, cssClass: cssClass, ref: ref }, component.id));
|
|
191
208
|
case 'dataGrid':
|
|
192
209
|
const dg = component;
|
|
193
|
-
|
|
210
|
+
const conditionalData = resolveConditionalData(dg.conditions, formState?.values || {})?.dataSource;
|
|
211
|
+
const conditionalColumns = resolveConditionalData(dg.conditions, formState?.values || {})?.columns;
|
|
212
|
+
const fetchedDataSource = conditionalData ? useFetchOptions(conditionalData) : useFetchOptions(dg.dataSource);
|
|
213
|
+
const resolvedDataSource = conditionalData ? resolveOptions(conditionalData, fetchedDataSource) : resolveOptions(dg.dataSource, fetchedDataSource);
|
|
214
|
+
const finalDataSource = resolvedDataSource.length > 0 ? resolvedDataSource : (dg.dataSource ?? []);
|
|
215
|
+
const finalColumns = conditionalColumns && conditionalColumns.length > 0 ? conditionalColumns : (dg.columns ?? []);
|
|
216
|
+
return (_jsx("div", { id: dg.id, className: dg.cssClass, children: _jsx(DataGridComponent, { id: dg.id, type: 'dataGrid', locale: locale, label: dg.label || '', name: dg.name || '', columns: finalColumns, dataSource: value !== undefined ? value : finalDataSource, height: dg.height || '200px', width: dg.width || '100%', pageSize: dg.pageSize || 6, allowPaging: dg.allowPaging !== false, allowFiltering: dg.allowFiltering, allowAdding: dg.allowAdding !== false, allowEditing: dg.allowEditing !== false, allowDeleting: dg.allowDeleting !== false, onDataChange: (data) => onChange(data), cssClass: cssClass, ref: ref }) }, dg.id));
|
|
194
217
|
case 'richTextEditor':
|
|
195
218
|
const toolbarSettings = {
|
|
196
219
|
items: ['Undo', 'Redo', '|', 'Bold', 'Italic', 'Underline', 'StrikeThrough', 'InlineCode', '|', 'CreateLink', 'Blockquote', '|', 'BulletFormatList', 'NumberFormatList', '|', 'Formats', 'Alignments', '|', 'Outdent', 'Indent', '|',
|
|
@@ -19,6 +19,11 @@ export interface CompositeConditionalRule {
|
|
|
19
19
|
rules: (ConditionalRule | CompositeConditionalRule)[];
|
|
20
20
|
}
|
|
21
21
|
export type ConditionType = ConditionalRule | CompositeConditionalRule;
|
|
22
|
+
export interface ChoiceBasedFieldRule {
|
|
23
|
+
primaryFieldId: string;
|
|
24
|
+
choiceMapping: Record<string, string[]>;
|
|
25
|
+
showAllWhenNotMapped?: boolean;
|
|
26
|
+
}
|
|
22
27
|
export interface FormComponentConditions {
|
|
23
28
|
visibleWhen?: ConditionType;
|
|
24
29
|
hideWhen?: ConditionType;
|
|
@@ -29,10 +34,10 @@ export interface FormComponentConditions {
|
|
|
29
34
|
condition: ConditionType;
|
|
30
35
|
value: any;
|
|
31
36
|
};
|
|
37
|
+
choiceBasedField?: ChoiceBasedFieldRule;
|
|
32
38
|
}
|
|
33
39
|
export interface FormSchema {
|
|
34
40
|
version: string;
|
|
35
|
-
theme: string;
|
|
36
41
|
components: FormComponent[];
|
|
37
42
|
}
|
|
38
43
|
export interface FormComponent {
|
|
@@ -180,6 +185,7 @@ export interface FormComponent {
|
|
|
180
185
|
tableCells?: FormComponent[][][];
|
|
181
186
|
parentPath?: string;
|
|
182
187
|
properties?: Record<string, any>;
|
|
188
|
+
expressionValue?: string;
|
|
183
189
|
conditions?: FormComponentConditions;
|
|
184
190
|
_configured?: boolean;
|
|
185
191
|
}
|
package/src/utils.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { Internationalization } from '@syncfusion/ej2-base';
|
|
3
|
+
import { DataManager, Query, UrlAdaptor, WebApiAdaptor, ODataV4Adaptor } from '@syncfusion/ej2-data';
|
|
4
|
+
import { ConditionalRuleEngine } from "./ConditionalRuleEngine";
|
|
5
|
+
import { UI_TYPE_TO_SEMANTIC_TYPE } from './json-converter';
|
|
2
6
|
/**
|
|
3
7
|
* Maps button style to Syncfusion CSS classes
|
|
4
8
|
* @param style - Button style ('primary' | 'flat' | 'information' | 'error' | 'success' | 'warning')
|
|
@@ -144,3 +148,261 @@ export function formatDateValues(component, value) {
|
|
|
144
148
|
}
|
|
145
149
|
return value;
|
|
146
150
|
}
|
|
151
|
+
export const getComponentFromSchema = (id, formSchema) => {
|
|
152
|
+
if (!id || !formSchema)
|
|
153
|
+
return null;
|
|
154
|
+
const findInComponents = (components) => {
|
|
155
|
+
for (const comp of components) {
|
|
156
|
+
if (comp.id === id || comp.name === id)
|
|
157
|
+
return comp;
|
|
158
|
+
if (comp.children) {
|
|
159
|
+
const found = findInComponents(comp.children);
|
|
160
|
+
if (found)
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
if (comp.tableCells) {
|
|
164
|
+
for (const row of comp.tableCells) {
|
|
165
|
+
for (const cell of row) {
|
|
166
|
+
const found = findInComponents(cell);
|
|
167
|
+
if (found)
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (comp.tabItems) {
|
|
173
|
+
for (const tab of comp.tabItems) {
|
|
174
|
+
if (tab.content) {
|
|
175
|
+
const found = findInComponents(tab.content);
|
|
176
|
+
if (found)
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
};
|
|
184
|
+
return findInComponents(formSchema.components || []);
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Helper function to get the appropriate adaptor instance
|
|
188
|
+
* @param adaptorType - Type of adaptor ('WebApiAdaptor' | 'UrlAdaptor' | 'ODataV4Adaptor')
|
|
189
|
+
* @returns Adaptor instance
|
|
190
|
+
*/
|
|
191
|
+
export const getAdaptorInstance = (adaptorType) => {
|
|
192
|
+
switch (adaptorType) {
|
|
193
|
+
case 'WebApiAdaptor':
|
|
194
|
+
return new WebApiAdaptor();
|
|
195
|
+
case 'ODataV4Adaptor':
|
|
196
|
+
return new ODataV4Adaptor();
|
|
197
|
+
case 'UrlAdaptor':
|
|
198
|
+
default:
|
|
199
|
+
return new UrlAdaptor();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Extracts data from a nested path using dot notation and array indices
|
|
204
|
+
* @param data - The data object or array
|
|
205
|
+
* @param path - Path to the data (e.g., 'results.items', '[0].data', 'response[0].values')
|
|
206
|
+
* @returns Extracted data or empty array
|
|
207
|
+
*/
|
|
208
|
+
export const extractDataFromPath = (data, path) => {
|
|
209
|
+
if (!path || data === null || data === undefined)
|
|
210
|
+
return data;
|
|
211
|
+
// Split by dots but preserve array indices
|
|
212
|
+
const parts = path.split(/\.(?![^\[]*\])/); // Split by dots not inside brackets
|
|
213
|
+
let current = data;
|
|
214
|
+
for (const part of parts) {
|
|
215
|
+
if (current === null || current === undefined) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
// Handle array index notation like [0] or results[0]
|
|
219
|
+
const arrayMatch = part.match(/^(\w*)\[(\d+)\]$/);
|
|
220
|
+
if (arrayMatch) {
|
|
221
|
+
const propName = arrayMatch[1];
|
|
222
|
+
const index = parseInt(arrayMatch[2]);
|
|
223
|
+
// If propName is empty, current should be an array
|
|
224
|
+
if (!propName) {
|
|
225
|
+
current = current[index];
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
current = current[propName]?.[index];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Regular property access
|
|
233
|
+
current = current[part];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return current;
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Custom hook to fetch options from URL with custom headers
|
|
240
|
+
* @param componentOptions - The component options object which may contain URL and headers
|
|
241
|
+
* @param adaptorType - Type of adaptor to use ('WebApiAdaptor' | 'UrlAdaptor' | 'ODataV4Adaptor')
|
|
242
|
+
* @returns Array of fetched options
|
|
243
|
+
*/
|
|
244
|
+
export const useFetchOptions = (componentOptions) => {
|
|
245
|
+
const [fetchedOptions, setFetchedOptions] = React.useState([]);
|
|
246
|
+
React.useEffect(() => {
|
|
247
|
+
const isUrlType = componentOptions &&
|
|
248
|
+
typeof componentOptions === 'object' &&
|
|
249
|
+
!Array.isArray(componentOptions) &&
|
|
250
|
+
componentOptions.type === 'url' &&
|
|
251
|
+
componentOptions.url;
|
|
252
|
+
if (isUrlType) {
|
|
253
|
+
const urlConfig = componentOptions;
|
|
254
|
+
try {
|
|
255
|
+
const headers = {};
|
|
256
|
+
if (urlConfig.headers && Array.isArray(urlConfig.headers)) {
|
|
257
|
+
urlConfig.headers.forEach((h) => {
|
|
258
|
+
if (h.key?.trim()) {
|
|
259
|
+
headers[h.key] = h.value;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// ✅ Create DataManager with selected adaptor
|
|
264
|
+
const dataManager = new DataManager({
|
|
265
|
+
url: urlConfig.url,
|
|
266
|
+
adaptor: getAdaptorInstance(urlConfig.adaptorType),
|
|
267
|
+
headers: Object.keys(headers).length ? headers : undefined
|
|
268
|
+
});
|
|
269
|
+
// ✅ Build Query (dynamic support)
|
|
270
|
+
let query = new Query();
|
|
271
|
+
if (urlConfig.search && urlConfig.search.enabled && urlConfig.search.fields && urlConfig.search.fields.length > 0) {
|
|
272
|
+
const searchKey = urlConfig.search.key || 'search';
|
|
273
|
+
query = query.search(searchKey, urlConfig.search.fields);
|
|
274
|
+
}
|
|
275
|
+
// Apply sort query
|
|
276
|
+
if (urlConfig.sort && urlConfig.sort.enabled && urlConfig.sort.field) {
|
|
277
|
+
const sortDirection = urlConfig.sort.direction === 'descending' ? 'descending' : 'ascending';
|
|
278
|
+
query = query.sortBy(urlConfig.sort.field, sortDirection);
|
|
279
|
+
}
|
|
280
|
+
// Apply filter queries
|
|
281
|
+
if (urlConfig.filters && Array.isArray(urlConfig.filters) && urlConfig.filters.length > 0) {
|
|
282
|
+
urlConfig.filters.forEach((f) => {
|
|
283
|
+
if (f.field && f.operator && f.value) {
|
|
284
|
+
query = query.where(f.field, f.operator, f.value);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// Example: add params (query string)
|
|
289
|
+
if (urlConfig.queryParams) {
|
|
290
|
+
Object.keys(urlConfig.queryParams).forEach(key => {
|
|
291
|
+
query = query.addParams(key, urlConfig.queryParams[key]);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// Example: paging
|
|
295
|
+
if (urlConfig.skip)
|
|
296
|
+
query = query.skip(urlConfig.skip);
|
|
297
|
+
if (urlConfig.take)
|
|
298
|
+
query = query.take(urlConfig.take);
|
|
299
|
+
// ✅ Execute query
|
|
300
|
+
dataManager.executeQuery(query).then((res) => {
|
|
301
|
+
let data = res.result;
|
|
302
|
+
if (urlConfig.dataPath) {
|
|
303
|
+
data = extractDataFromPath(data, urlConfig.dataPath);
|
|
304
|
+
}
|
|
305
|
+
if (Array.isArray(data)) {
|
|
306
|
+
setFetchedOptions(data);
|
|
307
|
+
}
|
|
308
|
+
else if (typeof data === 'string') {
|
|
309
|
+
setFetchedOptions([data]);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
setFetchedOptions([]);
|
|
313
|
+
}
|
|
314
|
+
}).catch((err) => {
|
|
315
|
+
console.error('DataManager fetch error:', err);
|
|
316
|
+
setFetchedOptions([]);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
console.error('Setup error:', err);
|
|
321
|
+
setFetchedOptions([]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}, [componentOptions]);
|
|
325
|
+
return fetchedOptions;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* Resolves component options from various sources
|
|
329
|
+
* Handles array options, URL-fetched options, and raw data options
|
|
330
|
+
* @param options - Component options (can be array, URL config, or raw data config)
|
|
331
|
+
* @param fetchedOptions - Pre-fetched options from URL
|
|
332
|
+
* @returns Resolved array of options
|
|
333
|
+
*/
|
|
334
|
+
export const resolveOptions = (options, fetchedOptions) => {
|
|
335
|
+
// If URL type with fetched data, return fetched options
|
|
336
|
+
if (options && typeof options === 'object' && !Array.isArray(options) && options.type === 'url') {
|
|
337
|
+
return fetchedOptions.length > 0 ? fetchedOptions : [];
|
|
338
|
+
}
|
|
339
|
+
// If already an array, return as-is
|
|
340
|
+
if (Array.isArray(options)) {
|
|
341
|
+
return options;
|
|
342
|
+
}
|
|
343
|
+
// If it's raw data type
|
|
344
|
+
if (options && typeof options === 'object' && !Array.isArray(options) && options.type === 'raw') {
|
|
345
|
+
return Array.isArray(options.data) ? options.data : [];
|
|
346
|
+
}
|
|
347
|
+
return [];
|
|
348
|
+
};
|
|
349
|
+
export const collectAvailableFieldNames = (components) => {
|
|
350
|
+
const fieldNames = [];
|
|
351
|
+
const traverse = (comps) => {
|
|
352
|
+
comps.forEach(comp => {
|
|
353
|
+
// Only add fields that have a name and aren't layout components
|
|
354
|
+
if (comp.name && !['panel', 'table', 'tabs', 'card', 'message', 'button', 'splitButton', 'staticHtml'].includes(comp.type)) {
|
|
355
|
+
// Get semantic type mapping for this component
|
|
356
|
+
const semanticType = UI_TYPE_TO_SEMANTIC_TYPE[comp.type];
|
|
357
|
+
if (semanticType) {
|
|
358
|
+
fieldNames.push({
|
|
359
|
+
name: comp.name,
|
|
360
|
+
type: semanticType.type,
|
|
361
|
+
...(semanticType.format && { format: semanticType.format })
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Traverse nested components
|
|
366
|
+
if (comp.children)
|
|
367
|
+
traverse(comp.children);
|
|
368
|
+
if (comp.tabItems) {
|
|
369
|
+
comp.tabItems.forEach(tab => {
|
|
370
|
+
if (tab.content)
|
|
371
|
+
traverse(tab.content);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (comp.tableCells) {
|
|
375
|
+
comp.tableCells.forEach(row => {
|
|
376
|
+
row.forEach(cell => traverse(cell));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
traverse(components);
|
|
382
|
+
return fieldNames;
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Resolve conditional data for a DataGrid component
|
|
386
|
+
* Evaluates all conditionalData rules and applies the first matching one
|
|
387
|
+
* Returns both dataSource and custom columns if defined
|
|
388
|
+
*
|
|
389
|
+
* @param conditions - Component conditions including conditionalData rules
|
|
390
|
+
* @param formValues - Current form field values
|
|
391
|
+
* @returns Object with dataSource and columns, or undefined if no condition matches
|
|
392
|
+
*/
|
|
393
|
+
export const resolveConditionalData = (conditions, formValues) => {
|
|
394
|
+
if (!conditions?.conditionalData || !Array.isArray(conditions.conditionalData)) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
// Evaluate each conditional data rule in order
|
|
398
|
+
for (const rule of conditions.conditionalData) {
|
|
399
|
+
if (ConditionalRuleEngine.evaluateCondition(rule.condition, formValues)) {
|
|
400
|
+
// Condition is met - resolve and return both data source and columns
|
|
401
|
+
return {
|
|
402
|
+
dataSource: rule.dataSource,
|
|
403
|
+
columns: rule.includeColumns ? rule.columns : undefined
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return undefined;
|
|
408
|
+
};
|
|
@@ -422,6 +422,16 @@
|
|
|
422
422
|
line-height: 16px;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
.form-title h1,
|
|
426
|
+
.form-title h2,
|
|
427
|
+
.form-title h3 {
|
|
428
|
+
margin-top: 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.form-title {
|
|
432
|
+
text-align: center;
|
|
433
|
+
}
|
|
434
|
+
|
|
425
435
|
.e-card .e-card-content {
|
|
426
436
|
padding: 12px 18px 12px 18px;
|
|
427
437
|
}
|