@syncfusion/ej2-react-form-renderer 0.1.0-beta → 0.1.2-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.js CHANGED
@@ -1,3 +1,8 @@
1
+ import React from "react";
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';
1
6
  /**
2
7
  * Maps button style to Syncfusion CSS classes
3
8
  * @param style - Button style ('primary' | 'flat' | 'information' | 'error' | 'success' | 'warning')
@@ -58,3 +63,346 @@ export function splitWidths(input) {
58
63
  // Convert any NaN (shouldn't remain) to 0 and round knowns
59
64
  return result.map(v => (isNaN(v) ? 0 : Math.round(v)));
60
65
  }
66
+ export function extractAllDescendantIds(component) {
67
+ const childIds = [];
68
+ function traverse(comps) {
69
+ for (const comp of comps) {
70
+ childIds.push(comp.id);
71
+ // Recursively traverse nested structures
72
+ if (comp.children && Array.isArray(comp.children)) {
73
+ traverse(comp.children);
74
+ }
75
+ // Handle table cells
76
+ if (comp.tableCells && Array.isArray(comp.tableCells)) {
77
+ comp.tableCells.forEach((row) => {
78
+ if (Array.isArray(row)) {
79
+ row.forEach((cell) => {
80
+ if (Array.isArray(cell)) {
81
+ traverse(cell);
82
+ }
83
+ });
84
+ }
85
+ });
86
+ }
87
+ // Handle tab items
88
+ if (comp.tabItems && Array.isArray(comp.tabItems)) {
89
+ comp.tabItems.forEach((tabItem) => {
90
+ if (tabItem.content && Array.isArray(tabItem.content)) {
91
+ traverse(tabItem.content);
92
+ }
93
+ });
94
+ }
95
+ }
96
+ }
97
+ // Start with direct children
98
+ if (component.children && Array.isArray(component.children)) {
99
+ traverse(component.children);
100
+ }
101
+ // Handle table cells
102
+ if (component.tableCells && Array.isArray(component.tableCells)) {
103
+ component.tableCells.forEach((row) => {
104
+ if (Array.isArray(row)) {
105
+ row.forEach((cell) => {
106
+ if (Array.isArray(cell)) {
107
+ traverse(cell);
108
+ }
109
+ });
110
+ }
111
+ });
112
+ }
113
+ // Handle tab items
114
+ if (component.tabItems && Array.isArray(component.tabItems)) {
115
+ component.tabItems.forEach((tabItem) => {
116
+ if (tabItem.content && Array.isArray(tabItem.content)) {
117
+ traverse(tabItem.content);
118
+ }
119
+ });
120
+ }
121
+ return childIds;
122
+ }
123
+ export function formatDateValues(component, value) {
124
+ let intl = new Internationalization();
125
+ let dFormatter = intl.getDateFormat({
126
+ format: 'MM/dd/yyyy',
127
+ type: 'date'
128
+ });
129
+ if (component.type === 'date') {
130
+ value = dFormatter(new Date(value));
131
+ }
132
+ if (component.type === 'time') {
133
+ dFormatter = intl.getDateFormat({
134
+ format: 'hh:mm a',
135
+ type: 'time'
136
+ });
137
+ value = dFormatter(new Date(value));
138
+ }
139
+ if (component.type === 'dateTime') {
140
+ dFormatter = intl.getDateFormat({
141
+ format: 'MM/dd/yyyy hh:mm a',
142
+ type: 'dateTime'
143
+ });
144
+ value = dFormatter(new Date(value));
145
+ }
146
+ if (component.type === 'dateRange' && Array.isArray(value)) {
147
+ value = value.map(date => date ? dFormatter(new Date(date)) : null);
148
+ }
149
+ return value;
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
+ };
@@ -150,6 +150,22 @@
150
150
  min-width: 0;
151
151
  }
152
152
 
153
+ .field-wrapper:has(.e-small) .field-label {
154
+ font-size: .75rem;
155
+ }
156
+
157
+ .field-wrapper:has(.e-small) .field-description {
158
+ font-size: 0.625rem;
159
+ }
160
+
161
+ .field-wrapper:has(.e-bigger) .field-label {
162
+ font-size: 1rem;
163
+ }
164
+
165
+ .field-wrapper:has(.e-bigger) .field-description {
166
+ font-size: 0.875rem;
167
+ }
168
+
153
169
  /* Label */
154
170
  .field-label {
155
171
  font-size: 0.875rem;
@@ -213,7 +229,6 @@
213
229
  border: none;
214
230
  border-radius: 8px;
215
231
  padding: 0;
216
- margin: 1rem 0;
217
232
  }
218
233
 
219
234
  .form-panel-renderer:has(.panel-legend) {
@@ -268,45 +283,45 @@
268
283
  overflow-y: hidden;
269
284
  }
270
285
 
271
- .form-table-renderer .table-row {
286
+ .form-table-renderer .e-form-table-row {
272
287
  display: table-row;
273
288
  background: transparent;
274
289
  }
275
290
 
276
- .form-table-renderer .table-cell {
291
+ .form-table-renderer .e-form-table-cell {
277
292
  display: table-cell;
278
293
  padding: 12px;
279
294
  vertical-align: top;
280
295
  }
281
296
 
282
- .table-border .table-cell {
297
+ .table-border .e-form-table-cell {
283
298
  border-bottom: 1px solid var(--sf-layout-border-color);
284
299
  border-right: 1px solid var(--sf-layout-border-color);
285
300
  }
286
301
 
287
- .table-cell:last-child {
302
+ .e-form-table-cell:last-child {
288
303
  border-right: none;
289
304
  }
290
305
 
291
- .form-table-renderer .table-row:last-child .table-cell {
306
+ .form-table-renderer .e-form-table-row:last-child .e-form-table-cell {
292
307
  border-bottom: none;
293
308
  }
294
309
 
295
- .table-cell > .form-table-renderer,
296
- .table-cell > .form-tabs-renderer,
297
- .table-cell > .form-card-renderer,
298
- .table-cell > .form-panel-renderer,
299
- .table-cell > .form-group
310
+ .e-form-table-cell > .form-table-renderer,
311
+ .e-form-table-cell > .form-tabs-renderer,
312
+ .e-form-table-cell > .form-card-renderer,
313
+ .e-form-table-cell > .form-panel-renderer,
314
+ .e-form-table-cell > .form-group
300
315
  {
301
- margin-bottom: 0.5rem;
316
+ margin-bottom: 1.5rem;
302
317
  }
303
318
 
304
319
 
305
- .table-cell > .form-table-renderer:last-child,
306
- .table-cell > .form-tabs-renderer:last-child,
307
- .table-cell > .form-card-renderer:last-child,
308
- .table-cell > .form-panel-renderer:last-child,
309
- .table-cell > .form-group:last-child
320
+ .e-form-table-cell > .form-table-renderer:last-child,
321
+ .e-form-table-cell > .form-tabs-renderer:last-child,
322
+ .e-form-table-cell > .form-card-renderer:last-child,
323
+ .e-form-table-cell > .form-panel-renderer:last-child,
324
+ .e-form-table-cell > .form-group:last-child
310
325
  {
311
326
  margin-bottom: 0;
312
327
  }
@@ -342,6 +357,7 @@
342
357
  }
343
358
 
344
359
  .tab-content-renderer {
360
+ padding-top: 10px;
345
361
  display: flex;
346
362
  flex-direction: column;
347
363
  gap: 1rem;
@@ -387,11 +403,11 @@
387
403
  border-color: #3d3d3d;
388
404
  }
389
405
 
390
- [data-theme="dark"] .table-row {
406
+ [data-theme="dark"] .e-form-table-row {
391
407
  border-color: #3d3d3d;
392
408
  }
393
409
 
394
- [data-theme="dark"] .table-cell {
410
+ [data-theme="dark"] .e-form-table-cell {
395
411
  border-color: #3d3d3d;
396
412
  }
397
413
 
@@ -406,6 +422,16 @@
406
422
  line-height: 16px;
407
423
  }
408
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
+
409
435
  .e-card .e-card-content {
410
436
  padding: 12px 18px 12px 18px;
411
437
  }
@@ -442,7 +468,7 @@
442
468
  width: 100%;
443
469
  }
444
470
 
445
- .form-table-renderer .table-cell {
471
+ .form-table-renderer .e-form-table-cell {
446
472
  display: block;
447
473
  flex: 1 1 auto;
448
474
  min-width: 250px; /* minimum width before wrapping to next row */
@@ -457,17 +483,17 @@
457
483
  flex: 1 1 auto;
458
484
  }
459
485
 
460
- .table-border .table-cell {
486
+ .table-border .e-form-table-cell {
461
487
  border-right: none;
462
488
  border-bottom: 1px solid var(--sf-layout-border-color);
463
489
  border-radius: 4px;
464
490
  }
465
491
 
466
- .form-table-renderer .table-cell:last-child {
492
+ .form-table-renderer .e-form-table-cell:last-child {
467
493
  border-bottom: 1px solid var(--sf-layout-border-color);
468
494
  }
469
495
 
470
- .form-table-renderer .row:last-child .table-cell {
496
+ .form-table-renderer .row:last-child .e-form-table-cell {
471
497
  border-bottom: none;
472
498
  }
473
499
  }