@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.
@@ -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 renderFormField = ({ component, value, onChange, onBlur, ref, hasError = false, onClick, locale }) => {
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
- return (_jsx(NumericTextBoxComponent, { id: component.id, name: component.id, locale: locale, ...commonProps, value: value !== undefined ? 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));
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.defaultValue ?? (component.checked ?? false), indeterminate: component.indeterminate || false, disabled: component.disabled, cssClass: cssClass, htmlAttributes: htmlAttrs, change: (args) => onChange(args.checked), ref: ref }, component.id));
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
- return (_jsx(CheckboxGroup, { component: component, value: value, onChange: (v) => onChange(v), optionRef: ref }, component.id));
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 || ['Option 1']).map((option, idx) => {
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
- return (_jsx(DropDownListComponent, { id: component.id, name: component.id, ...commonProps, locale: locale, cssClass: cssClass, dataSource: component.options || ["Option 1"], 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));
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
- return (_jsx(MultiSelectComponent, { id: component.id, name: component.id, locale: locale, ...commonProps, value: value !== undefined ? value : (component.defaultValue ?? null), placeholder: component.placeholder, dataSource: component.options || ["Option 1"], 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));
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, { ...commonProps, id: component.id, 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] }) }, component.id));
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, 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));
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.defaultValue ?? (component.checked ?? false), disabled: component.disabled, onLabel: component.onLabel || '', offLabel: component.offLabel || '', cssClass: cssClass, htmlAttributes: htmlAttrs, change: (args) => onChange(args.checked), ref: ref }, component.id));
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
- 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: dg.columns || [], dataSource: value !== undefined ? value : (dg.dataSource ?? dg.dataSource ?? []), height: dg.height || '200px', width: dg.width || '100%', pageSize: dg.pageSize || 6, allowPaging: dg.allowPaging !== false, allowAdding: dg.allowAdding !== false, allowEditing: dg.allowEditing !== false, allowDeleting: dg.allowDeleting !== false, onDataChange: (data) => onChange(data), cssClass: cssClass, ref: ref }) }, dg.id));
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
  }