@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.
- package/README.md +63 -0
- package/package.json +52 -0
- package/postcss.config.mjs +5 -0
- package/rollup.config.js +51 -0
- package/src/assets/add-tab.svg +4 -0
- package/src/assets/chevron-right.svg +4 -0
- package/src/assets/delete-tab.svg +3 -0
- package/src/assets/dummy-data/skeleton.json +42 -0
- package/src/assets/dummy-data/skeleton.ts +28 -0
- package/src/assets/meridian-toggle.svg +4 -0
- package/src/components/attributes/attribute-price.tsx +17 -0
- package/src/components/detail-views/detail-basic.tsx +121 -0
- package/src/components/detail-views/detail-view.scss +187 -0
- package/src/components/item-views/item-compact.tsx +72 -0
- package/src/components/item-views/item-pin.tsx +131 -0
- package/src/components/item-views/item-profile.tsx +140 -0
- package/src/components/item-views/item-vertical.tsx +145 -0
- package/src/components/item-views/item-view.scss +277 -0
- package/src/components/malleability/console/console-setting.tsx +184 -0
- package/src/components/malleability/console/console-view.tsx +47 -0
- package/src/components/malleability/console/detail-view-component.tsx +262 -0
- package/src/components/malleability/console/malleability-component.tsx +104 -0
- package/src/components/malleability/console/malleability-console.scss +285 -0
- package/src/components/malleability/console/overview-component.tsx +174 -0
- package/src/components/malleability/malleability-content-toggle.tsx +32 -0
- package/src/components/malleability/malleability-overview-tabs.tsx +212 -0
- package/src/components/malleability/malleability-toolbar.tsx +15 -0
- package/src/components/malleability/malleability.scss +199 -0
- package/src/components/overviews/overivew-basic-table.tsx +127 -0
- package/src/components/overviews/overview-basic-grid.tsx +27 -0
- package/src/components/overviews/overview-basic-list.tsx +61 -0
- package/src/components/overviews/overview-basic-map.tsx +358 -0
- package/src/components/overviews/overview-basic.scss +88 -0
- package/src/components/ui/dropdown-menu.tsx +61 -0
- package/src/helpers/attribute-set.helper.ts +4 -0
- package/src/helpers/attribute.helper.ts +334 -0
- package/src/helpers/spec.helper.ts +92 -0
- package/src/helpers/utils.helper.ts +22 -0
- package/src/helpers/view.helper.ts +184 -0
- package/src/index.css +149 -0
- package/src/index.ts +1 -0
- package/src/renderer/attribute.scss +59 -0
- package/src/renderer/attribute.tsx +305 -0
- package/src/renderer/renderer.data-bind.ts +573 -0
- package/src/renderer/renderer.defaults.ts +194 -0
- package/src/renderer/renderer.denormalize.ts +273 -0
- package/src/renderer/renderer.filter.ts +211 -0
- package/src/renderer/renderer.props.ts +21 -0
- package/src/renderer/renderer.scss +72 -0
- package/src/renderer/renderer.tsx +450 -0
- package/src/renderer/wrapper.tsx +225 -0
- package/src/spec/spec.internal.ts +76 -0
- package/src/spec/spec.ts +195 -0
- package/src/store/odi-malleability.store.ts +337 -0
- package/src/store/odi-navigation.store.ts +44 -0
- package/src/store/odi.store.ts +210 -0
- package/tailwind.config.js +31 -0
- package/tsconfig.json +24 -0
- package/types/svg.d.ts +6 -0
- package/vercel.json +5 -0
- 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
|
+
}
|