@meridian-ui/meridian 1.0.0

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.
Files changed (61) hide show
  1. package/README.md +63 -0
  2. package/package.json +52 -0
  3. package/postcss.config.mjs +5 -0
  4. package/rollup.config.js +51 -0
  5. package/src/assets/add-tab.svg +4 -0
  6. package/src/assets/chevron-right.svg +4 -0
  7. package/src/assets/delete-tab.svg +3 -0
  8. package/src/assets/dummy-data/skeleton.json +42 -0
  9. package/src/assets/dummy-data/skeleton.ts +28 -0
  10. package/src/assets/meridian-toggle.svg +4 -0
  11. package/src/components/attributes/attribute-price.tsx +17 -0
  12. package/src/components/detail-views/detail-basic.tsx +121 -0
  13. package/src/components/detail-views/detail-view.scss +187 -0
  14. package/src/components/item-views/item-compact.tsx +72 -0
  15. package/src/components/item-views/item-pin.tsx +131 -0
  16. package/src/components/item-views/item-profile.tsx +140 -0
  17. package/src/components/item-views/item-vertical.tsx +145 -0
  18. package/src/components/item-views/item-view.scss +277 -0
  19. package/src/components/malleability/console/console-setting.tsx +184 -0
  20. package/src/components/malleability/console/console-view.tsx +47 -0
  21. package/src/components/malleability/console/detail-view-component.tsx +262 -0
  22. package/src/components/malleability/console/malleability-component.tsx +104 -0
  23. package/src/components/malleability/console/malleability-console.scss +285 -0
  24. package/src/components/malleability/console/overview-component.tsx +174 -0
  25. package/src/components/malleability/malleability-content-toggle.tsx +32 -0
  26. package/src/components/malleability/malleability-overview-tabs.tsx +212 -0
  27. package/src/components/malleability/malleability-toolbar.tsx +15 -0
  28. package/src/components/malleability/malleability.scss +199 -0
  29. package/src/components/overviews/overivew-basic-table.tsx +127 -0
  30. package/src/components/overviews/overview-basic-grid.tsx +27 -0
  31. package/src/components/overviews/overview-basic-list.tsx +61 -0
  32. package/src/components/overviews/overview-basic-map.tsx +358 -0
  33. package/src/components/overviews/overview-basic.scss +88 -0
  34. package/src/components/ui/dropdown-menu.tsx +61 -0
  35. package/src/helpers/attribute-set.helper.ts +4 -0
  36. package/src/helpers/attribute.helper.ts +334 -0
  37. package/src/helpers/spec.helper.ts +92 -0
  38. package/src/helpers/utils.helper.ts +22 -0
  39. package/src/helpers/view.helper.ts +184 -0
  40. package/src/index.css +149 -0
  41. package/src/index.ts +1 -0
  42. package/src/renderer/attribute.scss +59 -0
  43. package/src/renderer/attribute.tsx +305 -0
  44. package/src/renderer/renderer.data-bind.ts +573 -0
  45. package/src/renderer/renderer.defaults.ts +194 -0
  46. package/src/renderer/renderer.denormalize.ts +273 -0
  47. package/src/renderer/renderer.filter.ts +211 -0
  48. package/src/renderer/renderer.props.ts +21 -0
  49. package/src/renderer/renderer.scss +72 -0
  50. package/src/renderer/renderer.tsx +450 -0
  51. package/src/renderer/wrapper.tsx +225 -0
  52. package/src/spec/spec.internal.ts +76 -0
  53. package/src/spec/spec.ts +195 -0
  54. package/src/store/odi-malleability.store.ts +337 -0
  55. package/src/store/odi-navigation.store.ts +44 -0
  56. package/src/store/odi.store.ts +210 -0
  57. package/tailwind.config.js +31 -0
  58. package/tsconfig.json +24 -0
  59. package/types/svg.d.ts +6 -0
  60. package/vercel.json +5 -0
  61. package/webpack.config.js +18 -0
@@ -0,0 +1,573 @@
1
+ // Functions that will map a JSON list object to FetchedItemType[], using a DataBinding object
2
+
3
+ import { isAttributeType } from "../helpers/spec.helper";
4
+ import { AttributeTransformType, AttributeConditionType, AttributeType, BindingItemType, ODI, Overview, DataBindingType } from "../spec/spec";
5
+ import { FetchedAttributeType, FetchedAttributeGroupType, FetchedAttributeValueType, FetchedItemType, FetchedODI } from "../spec/spec.internal";
6
+ import { denormalizeODI } from "./renderer.denormalize";
7
+
8
+ // ---- ---------------- ----
9
+ // ---- HELPER FUNCTIONS ----
10
+ // ---- ---------------- ----
11
+
12
+ /**
13
+ * Resolve a value from an object using a path string.
14
+ * Supports simple dot-notation and array indices.
15
+ * E.g. ".details.photos[0]?.images.original.url"
16
+ */
17
+ export function resolveValue(obj: any, path: string | undefined): any {
18
+ if (!path) return undefined;
19
+ // Ensure path is a string
20
+ if (typeof path !== 'string') {
21
+ console.warn('resolveValue received non-string path:', path);
22
+ return undefined;
23
+ }
24
+ // Remove a leading '.' if present.
25
+ if (path[0] === '.') path = path.substring(1);
26
+
27
+ const parts = path.split('.').filter(part => part !== '');
28
+ let current = obj;
29
+ for (let part of parts) {
30
+ if (current == null) return undefined;
31
+ // Handle optional chaining if provided (e.g., "photos[0]?" or "[0]?")
32
+ part = part.replace(/\?$/, "");
33
+ // Check for array notation e.g., "photos[0]"
34
+ const arrayMatch = part.match(/(.*?)\[(\d+)\]$/);
35
+ if (arrayMatch) {
36
+ const prop = arrayMatch[1];
37
+ const index = parseInt(arrayMatch[2], 10);
38
+ current = current[prop];
39
+ if (Array.isArray(current)) {
40
+ current = current[index];
41
+ } else {
42
+ return null;
43
+ }
44
+ } else {
45
+ current = current[part];
46
+ }
47
+ }
48
+ return current;
49
+ }
50
+
51
+ /**
52
+ * Recursively resolves an internalAttributes value.
53
+ * If the value is a string starting with a dot, it is treated as a binding path.
54
+ * Arrays and objects are processed recursively.
55
+ */
56
+ function resolveInternalAttributes(internalAttrs: any, item: any): any {
57
+ if (typeof internalAttrs === "string") {
58
+ // If the string starts with a dot, treat it as a binding path.
59
+ if (internalAttrs.startsWith(".")) {
60
+ return resolveValue(item, internalAttrs);
61
+ } else {
62
+ return internalAttrs;
63
+ }
64
+ } else if (Array.isArray(internalAttrs)) {
65
+ return internalAttrs.map(attr => resolveInternalAttributes(attr, item));
66
+ } else if (internalAttrs !== null && typeof internalAttrs === "object") {
67
+ const result: any = {};
68
+ Object.keys(internalAttrs).forEach(key => {
69
+ result[key] = resolveInternalAttributes(internalAttrs[key], item);
70
+ });
71
+ return result;
72
+ }
73
+ return internalAttrs;
74
+ }
75
+
76
+ /**
77
+ * Apply any transforms to a value.
78
+ * Each transform can specify:
79
+ * - A new value via a path (t.value)
80
+ * - A mapping operation (t.map) that works on arrays or objects
81
+ * - Can be a string path or an object with attributes for nested mapping
82
+ * - A filter operation (t.filter) that filters an array based on a condition string or AttributeConditionType.
83
+ * - A slice operation (t.slice) that extracts a subset of an array using start and end indices.
84
+ */
85
+ function applyTransform(value: any, transforms: AttributeTransformType[], item: any, parentId?: string): any {
86
+ let transformed = value;
87
+ transforms.forEach(t => {
88
+ if (t.value) {
89
+ // Replace the value with the one resolved from the item.
90
+ transformed = resolveValue(item, t.value);
91
+ }
92
+ if (t.map) {
93
+ if (typeof t.map === 'string') {
94
+ // If transformed is an array, map each element using the string path
95
+ if (Array.isArray(transformed)) {
96
+ transformed = transformed.map(elem => {
97
+ // Special case: if map is just ".", return the element itself
98
+ if (t.map === '.') {
99
+ return elem;
100
+ }
101
+ return resolveValue(elem, t.map as string);
102
+ });
103
+ } else {
104
+ // Special case: if map is just ".", return the value itself
105
+ if (t.map === '.') {
106
+ transformed = transformed;
107
+ } else {
108
+ transformed = resolveValue(transformed, t.map as string);
109
+ }
110
+ }
111
+ } else if (typeof t.map === 'object' && t.map?.attributes) {
112
+ // Handle the case where map contains attributes for nested mapping
113
+ if (Array.isArray(transformed)) {
114
+ // Map each element using the attributes definition
115
+ transformed = transformed.map(elem => {
116
+ // Process each attribute with its transforms before mapping
117
+ const processedAttributes = (t.map as {attributes: AttributeType[]}).attributes.map(attr => {
118
+ // Create a copy of the attribute to avoid modifying the original
119
+ const attrCopy = {...attr};
120
+ return attrCopy;
121
+ });
122
+ // Return a properly structured object with attributes instead of just the mapped attributes
123
+ return {
124
+ attributes: mapAttributes(elem, 0, processedAttributes, parentId)
125
+ };
126
+ });
127
+ } else if (transformed) {
128
+ // Apply to a single object
129
+ const processedAttributes = (t.map as {attributes: AttributeType[]}).attributes.map(attr => {
130
+ // Create a copy of the attribute to avoid modifying the original
131
+ const attrCopy = {...attr};
132
+ return attrCopy;
133
+ });
134
+ transformed = mapAttributes(transformed, 0, processedAttributes, parentId);
135
+ }
136
+ }
137
+ }
138
+ if (t.filter) {
139
+ // For a filter, we assume transformed is an array.
140
+ if (Array.isArray(transformed)) {
141
+ if (typeof t.filter === 'string') {
142
+ // Use the string-based filter evaluator
143
+ transformed = transformed.filter(elem => {
144
+ return evalFilter(t.filter as string, elem);
145
+ });
146
+ } else {
147
+ // Use the condition-based filter
148
+ transformed = transformed.filter(elem => {
149
+ return evaluateCondition(elem, t.filter as AttributeConditionType);
150
+ });
151
+ }
152
+ }
153
+ }
154
+ if (t.slice && Array.isArray(transformed)) {
155
+ // Apply slice operation to extract a subset of the array
156
+ const start = t.slice.start || 0;
157
+ const end = t.slice.end !== undefined ? t.slice.end : transformed.length;
158
+ transformed = transformed.slice(start, end);
159
+ }
160
+ });
161
+ return transformed;
162
+ }
163
+
164
+ /**
165
+ * A very basic filter evaluator.
166
+ * WARNING: For a production system, use a safe evaluation method.
167
+ */
168
+ function evalFilter(filterExpr: string, datum: any): boolean {
169
+ try {
170
+ // `datum` is passed in as a parameter to the function.
171
+ return Function('datum', `return ${filterExpr}`)(datum);
172
+ } catch (e) {
173
+ console.error("Error evaluating filter:", filterExpr, e);
174
+ return false;
175
+ }
176
+ }
177
+
178
+
179
+ // ---- -------------------- ----
180
+ // ---- CONDITION EVALUATION ----
181
+ // ---- -------------------- ----
182
+
183
+ /**
184
+ * Evaluates an AttributeConditionType against an item.
185
+ */
186
+ function evaluateCondition(item: any, condition: AttributeConditionType): boolean {
187
+ // Check an "exists" condition.
188
+ if (condition.exists) {
189
+ const existsValue = resolveValue(item, condition.exists);
190
+ if (!existsValue) return false;
191
+ }
192
+ // Check a "comparison" condition.
193
+ if (condition.comparison) {
194
+ const { field, operator, value } = condition.comparison;
195
+ const fieldValue = resolveValue(item, field);
196
+ switch (operator) {
197
+ case '==': if (fieldValue != value) return false; break;
198
+ case '!=': if (fieldValue == value) return false; break;
199
+ case '>': if (fieldValue <= value) return false; break;
200
+ case '<': if (fieldValue >= value) return false; break;
201
+ case '>=': if (fieldValue < value) return false; break;
202
+ case '<=': if (fieldValue > value) return false; break;
203
+ }
204
+ }
205
+ // Evaluate "and" conditions.
206
+ if (condition.and) {
207
+ for (const cond of condition.and) {
208
+ if (!evaluateCondition(item, cond)) return false;
209
+ }
210
+ }
211
+ // Evaluate "or" conditions.
212
+ if (condition.or) {
213
+ let any = false;
214
+ for (const cond of condition.or) {
215
+ if (evaluateCondition(item, cond)) {
216
+ any = true;
217
+ break;
218
+ }
219
+ }
220
+ if (!any) return false;
221
+ }
222
+ // Evaluate "not" conditions (if any condition is true, return false).
223
+ if (condition.not) {
224
+ for (const cond of condition.not) {
225
+ if (evaluateCondition(item, cond)) return false;
226
+ }
227
+ }
228
+ return true;
229
+ }
230
+
231
+ // ---- ----------------- ----
232
+ // ---- ATTRIBUTE MAPPING ----
233
+ // ---- ----------------- ----
234
+
235
+ /**
236
+ * Maps an array of AttributeType to an array of FetchedAttributeType.
237
+ * This function handles both value attributes and groups (nested attributes).
238
+ */
239
+ function mapAttributes(item: any, itemIndex: number, attributes: AttributeType[], parentId?: string): FetchedAttributeType[] {
240
+ const mapped: FetchedAttributeType[] = [];
241
+ attributes.forEach((attr, attrIndex) => {
242
+ // If a condition is defined and fails, skip this attribute.
243
+ if (attr.condition && !evaluateCondition(item, attr.condition)) {
244
+ return;
245
+ }
246
+ const id = attr.type === 'overview' ? attr.id??'' : (parentId && parentId !== 'undefined') ? `${parentId}-${attrIndex}` : `${attrIndex}`;
247
+
248
+ // If there are nested attributes, treat this as a group.
249
+ if (attr.attributes && attr.attributes.length > 0) {
250
+ const group: FetchedAttributeGroupType = {
251
+ id,
252
+ index: attrIndex,
253
+ overviewIndex: -1, // To be assigned overviewIndex in denormalizer
254
+ itemIndex,
255
+ label: attr.label,
256
+ roles: attr.roles,
257
+ path: attr.value || '',
258
+ type: attr.type,
259
+ attributes: mapAttributes(item, itemIndex, attr.attributes, id)
260
+ };
261
+ mapped.push(group);
262
+ } else {
263
+ // For a simple value attribute.
264
+ let val = resolveValue(item, attr.value || '');
265
+
266
+ // Apply transforms if present
267
+ if (attr.transform) {
268
+ val = applyTransform(val, attr.transform, item, id);
269
+ }
270
+
271
+ // Special case for the 'features' role with map: '.' transform
272
+ // Check if this is a features attribute with a transform that maps to '.'
273
+ if (attr.roles?.includes('features') &&
274
+ attr.transform?.some(t => t.map === '.') &&
275
+ Array.isArray(val)) {
276
+ // Create a group attribute to hold the features
277
+ const group: FetchedAttributeGroupType = {
278
+ id,
279
+ index: attrIndex,
280
+ overviewIndex: -1,
281
+ itemIndex,
282
+ label: attr.label,
283
+ roles: attr.roles,
284
+ path: attr.value || '',
285
+ type: attr.type || 'group',
286
+ attributes: val.map((feature, idx) => ({
287
+ id: `${id}-${idx}`,
288
+ index: idx,
289
+ itemIndex: itemIndex,
290
+ overviewIndex: -1,
291
+ label: '',
292
+ roles: [],
293
+ path: '',
294
+ value: feature,
295
+ type: 'text'
296
+ }))
297
+ };
298
+ mapped.push(group);
299
+ } else if (attr.type === 'overview' && Array.isArray(val) && val.length > 0 && val[0]?.attributes) {
300
+
301
+ // Create a group attribute to hold the list of items with attributes
302
+ const group: FetchedAttributeGroupType = {
303
+ id,
304
+ index: attrIndex,
305
+ overviewIndex: -1, // To be assigned overviewIndex in denormalizer
306
+ itemIndex,
307
+ label: attr.label,
308
+ roles: attr.roles,
309
+ path: attr.value || '',
310
+ type: attr.type,
311
+ attributes: val.map((item, idx) => {
312
+ const overviewAttributes = mapAttributes(item, itemIndex, item.attributes, `${id}-${idx}`)
313
+ const attribute = overviewAttributes.find(a => a && 'path' in a && a.path === attr.itemId)
314
+ return {
315
+ id: `${id}-${idx}`,
316
+ index: idx,
317
+ overviewIndex: idx,
318
+ itemId: attribute ? (isAttributeType(attribute) ? attribute.value : attribute.itemId) : undefined,
319
+ attributes: overviewAttributes
320
+ }
321
+ }) as FetchedAttributeType[]
322
+ };
323
+ mapped.push(group);
324
+ } else if (Array.isArray(val) && val.length > 0 && val[0]?.attributes) {
325
+ // Create a group attribute to hold the list of items with attributes
326
+ const group: FetchedAttributeGroupType = {
327
+ id,
328
+ index: attrIndex,
329
+ overviewIndex: -1, // To be assigned overviewIndex in denormalizer
330
+ itemIndex,
331
+ label: attr.label,
332
+ roles: attr.roles,
333
+ path: attr.value || '',
334
+ type: attr.type,
335
+ attributes: val.map((item, idx) => ({
336
+ id: `${id}-${idx}`,
337
+ index: idx,
338
+ itemIndex: itemIndex,
339
+ itemId: attr.itemId,
340
+ attributes: mapAttributes(item, itemIndex, item.attributes, `${id}-${idx}`)
341
+ })) as FetchedAttributeType[]
342
+ };
343
+ mapped.push(group);
344
+ } else {
345
+ // Handle as a regular value attribute
346
+ // if (attr.value === '.term') {
347
+ // console.log('val', val, attr)
348
+ // }
349
+
350
+
351
+
352
+ const fetchedValue: FetchedAttributeValueType = {
353
+ id,
354
+ index: attrIndex,
355
+ itemIndex,
356
+ overviewIndex: -1, // To be assigned overviewIndex in denormalizer
357
+ label: attr.label,
358
+ roles: attr.roles,
359
+ path: 'path' in attr ? attr.path as string : attr.value || '',
360
+ value: val ?? attr.value, // ?? attr.value,
361
+ type: attr.type
362
+ };
363
+
364
+
365
+ // if (itemIndex === 0 &&'itemIndex' in attr && attr.itemIndex === 0 && attr.id === 'synonyms-list-0') {
366
+ // if (val) {
367
+ // console.log('val', attr.value, fetchedValue)
368
+ // } else {
369
+ // console.log('no val', attr.value, fetchedValue)
370
+ // }
371
+ // }
372
+
373
+
374
+ mapped.push(fetchedValue);
375
+ }
376
+ }
377
+ });
378
+ return mapped;
379
+ }
380
+
381
+ // ---- --------------------- ----
382
+ // ---- MAIN MAPPING FUNCTION ----
383
+ // ---- --------------------- ----
384
+
385
+ /**
386
+ * Maps a list of JSON objects to a FetchedItemType[] list
387
+ * This can be either a JSON list or a CSV list.
388
+ * based on a DataBinding configuration.
389
+ */
390
+ export const mapDataToFetchedItems = (data: any[], binding: BindingItemType): FetchedItemType[] => {
391
+ // console.log('json', jsonList)
392
+ return data.map((item, index) => {
393
+ const fetchedItem: FetchedItemType = {
394
+ itemId: resolveValue(item, binding.itemId),
395
+ index,
396
+ attributes: mapAttributes(item, index, binding.attributes),
397
+ internalAttributes: mapAttributes(item, index, binding.internalAttributes ?? []),
398
+ };
399
+
400
+ return fetchedItem;
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Recursively finds all overview objects that reference their ancestors.
406
+ * @param overview The current overview to check
407
+ * @param ancestors Set of ancestor IDs to check against
408
+ * @returns Array of recursive overviews
409
+ */
410
+ const findRecursiveOverviews = (
411
+ overview: Overview,
412
+ ancestors: Set<string> = new Set()
413
+ ): Overview[] => {
414
+ const recursiveViews: Overview[] = [];
415
+ const currentAncestors = new Set(ancestors);
416
+
417
+ // Add current overview ID to ancestors
418
+ if (overview.id) {
419
+ currentAncestors.add(overview.id);
420
+ }
421
+
422
+ // Check if any detail view references an ancestor
423
+ if (overview.detailViews) {
424
+ for (const detailView of overview.detailViews) {
425
+ // Extract the ID whether detailView is a string or an object
426
+ const detailViewId = typeof detailView === 'string' ? detailView : detailView.id;
427
+ if (detailViewId && currentAncestors.has(detailViewId)) {
428
+ // This overview references an ancestor, so it's recursive
429
+ recursiveViews.push(overview);
430
+ break;
431
+ }
432
+ }
433
+ }
434
+
435
+ // Recursively check nested overviews if they exist
436
+ if (overview.overviews) {
437
+ for (const nestedItem of overview.overviews) {
438
+ // Skip if it's just a string ID reference
439
+ if (typeof nestedItem === 'string') continue;
440
+
441
+ const nestedRecursiveViews = findRecursiveOverviews(nestedItem, currentAncestors);
442
+ recursiveViews.push(...nestedRecursiveViews);
443
+ }
444
+ }
445
+
446
+ return recursiveViews;
447
+ }
448
+
449
+ /**
450
+ * Finds all recursive views in an ODI specification.
451
+ * A view is recursive if it references one of its ancestors.
452
+ * @param odi The ODI specification
453
+ * @returns Array of recursive overviews
454
+ */
455
+ export const getRecursiveAttributes = (odi: ODI | FetchedODI): AttributeType[] => {
456
+ const recursiveViews: Overview[] = [];
457
+
458
+ // Check top-level overviews
459
+ if (odi.overviews) {
460
+ for (const overview of odi.overviews) {
461
+ const foundViews = findRecursiveOverviews(overview);
462
+ recursiveViews.push(...foundViews);
463
+ }
464
+ }
465
+
466
+ // Check detail views that might contain overviews
467
+ if (odi.detailViews) {
468
+ for (const detailView of odi.detailViews) {
469
+ if (detailView.overviews) {
470
+ for (const overview of detailView.overviews) {
471
+ if (typeof overview === 'string') continue;
472
+ // For detail view overviews, include the detail view ID in ancestors
473
+ const ancestors = new Set<string>();
474
+ if (detailView.id) {
475
+ ancestors.add(detailView.id);
476
+ }
477
+ const foundViews = findRecursiveOverviews(overview, ancestors);
478
+ recursiveViews.push(...foundViews);
479
+ }
480
+ }
481
+ }
482
+ }
483
+
484
+ // Turn the recursive views into attributes with the type 'overview' and roles being the value of showIn.
485
+ const recursiveAttributes = recursiveViews.map((view) => {
486
+ return {
487
+ id: view.id,
488
+ label: 'Overview',
489
+ roles: view.showIn,
490
+ path: '',
491
+ value: view.attributeBindingId,
492
+ type: 'overview'
493
+ }
494
+ });
495
+
496
+ return recursiveAttributes;
497
+ }
498
+
499
+ const getGetFetchedBindingsOfViews = (recursiveAttributes: AttributeType[], dataBinding: DataBindingType[]) => {
500
+
501
+ const newDataBinding = dataBinding.map(db => {
502
+ // Create new attributes for each recursive attribute by duplicating the referenced attribute
503
+ const newAttributes = recursiveAttributes.flatMap(recAttr => {
504
+ // Find the original attribute to duplicate (like 'synonyms')
505
+ const originalAttr = db.binding.attributes.find(attr => attr.id === recAttr.value);
506
+ if (!originalAttr) return [];
507
+
508
+ // Create a new attribute based on the original one
509
+ return {
510
+ ...JSON.parse(JSON.stringify(originalAttr)), // Deep clone
511
+ ...recAttr,
512
+ path: originalAttr.id,
513
+ value: originalAttr.value,
514
+
515
+ };
516
+ });
517
+
518
+ return {
519
+ ...db,
520
+ binding: {
521
+ ...db.binding,
522
+ attributes: db.binding.attributes.map(attr => {
523
+ const newAttr = newAttributes.find(newAttr => newAttr.path === attr.id);
524
+ return newAttr ? {
525
+ ...attr,
526
+ ...newAttr
527
+ } : attr;
528
+ })
529
+ }
530
+ };
531
+ });
532
+
533
+ return newDataBinding;
534
+ }
535
+
536
+ export const getFetchedODIFromData = (data: any, odi: ODI): FetchedODI | null => {
537
+
538
+
539
+ // Find recursive views that will need special handling
540
+ const recursiveAttributes = getRecursiveAttributes(odi);
541
+
542
+ const newBinding = getGetFetchedBindingsOfViews(recursiveAttributes, odi.dataBinding);
543
+
544
+ // console.log('newBinding', newBinding);
545
+
546
+ // Create a mapped ODI with fetched items for each data binding
547
+ const mappedODI: FetchedODI = {
548
+ ...odi,
549
+ dataBinding: newBinding.map((source) => {
550
+ // Get the collection of items using pathToItems (default to the data object itself)
551
+ const pathToItems = source.binding.pathToItems || '.';
552
+ const items = resolveValue(data, pathToItems);
553
+
554
+ if (!items) {
555
+ console.error(`Could not find items at path: ${pathToItems}`);
556
+ return {
557
+ ...source,
558
+ items: []
559
+ };
560
+ }
561
+
562
+ // Map the items using the binding configuration
563
+ return {
564
+ ...source,
565
+ items: mapDataToFetchedItems(Array.isArray(items) ? items : [items], source.binding)
566
+ };
567
+ })
568
+ };
569
+
570
+ console.log('mappedODI', mappedODI);
571
+
572
+ return denormalizeODI(mappedODI);
573
+ }