@startinblox/components-ds4go 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 (56) hide show
  1. package/.gitlab-ci.yml +57 -0
  2. package/.storybook/main.ts +17 -0
  3. package/.storybook/preview-head.html +8 -0
  4. package/.storybook/preview.ts +22 -0
  5. package/LICENSE +21 -0
  6. package/README.md +129 -0
  7. package/biome.json +50 -0
  8. package/cypress/component/solid-boilerplate.cy.ts +9 -0
  9. package/cypress/support/component-index.html +12 -0
  10. package/cypress/support/component.ts +17 -0
  11. package/cypress.config.ts +11 -0
  12. package/dist/components-ds4go.css +1 -0
  13. package/dist/index.js +1634 -0
  14. package/lit-localize.json +15 -0
  15. package/locales/en.xlf +28 -0
  16. package/package.json +93 -0
  17. package/postcss.config.js +8 -0
  18. package/src/component.d.ts +167 -0
  19. package/src/components/catalog/ds4go-fact-bundle-holder.ts +162 -0
  20. package/src/components/modal/ds4go-fact-bundle-modal.ts +82 -0
  21. package/src/components/solid-fact-bundle.ts +225 -0
  22. package/src/context.json +1 -0
  23. package/src/helpers/components/ResourceMapper.ts +450 -0
  24. package/src/helpers/components/componentObjectHandler.ts +22 -0
  25. package/src/helpers/components/componentObjectsHandler.ts +14 -0
  26. package/src/helpers/components/dspComponent.ts +243 -0
  27. package/src/helpers/components/orbitComponent.ts +273 -0
  28. package/src/helpers/components/setupCacheInvalidation.ts +44 -0
  29. package/src/helpers/components/setupCacheOnResourceReady.ts +39 -0
  30. package/src/helpers/components/setupComponentSubscriptions.ts +73 -0
  31. package/src/helpers/components/setupOnSaveReset.ts +20 -0
  32. package/src/helpers/datas/dataBuilder.ts +43 -0
  33. package/src/helpers/datas/filterGenerator.ts +29 -0
  34. package/src/helpers/datas/filterObjectByDateAfter.ts +80 -0
  35. package/src/helpers/datas/filterObjectById.ts +54 -0
  36. package/src/helpers/datas/filterObjectByInterval.ts +133 -0
  37. package/src/helpers/datas/filterObjectByNamedValue.ts +103 -0
  38. package/src/helpers/datas/filterObjectByType.ts +30 -0
  39. package/src/helpers/datas/filterObjectByValue.ts +81 -0
  40. package/src/helpers/datas/sort.ts +40 -0
  41. package/src/helpers/i18n/configureLocalization.ts +17 -0
  42. package/src/helpers/index.ts +43 -0
  43. package/src/helpers/mappings/dsp-mapping-config.ts +545 -0
  44. package/src/helpers/ui/formatDate.ts +18 -0
  45. package/src/helpers/ui/lipsum.ts +12 -0
  46. package/src/helpers/utils/requestNavigation.ts +12 -0
  47. package/src/helpers/utils/uniq.ts +6 -0
  48. package/src/index.ts +14 -0
  49. package/src/initializer.ts +11 -0
  50. package/src/mocks/orbit.mock.ts +33 -0
  51. package/src/mocks/user.mock.ts +67 -0
  52. package/src/styles/_helpers/flex.scss +39 -0
  53. package/src/styles/index.scss +14 -0
  54. package/src/styles/modal/ds4go-fact-bundle-modal.scss +89 -0
  55. package/tsconfig.json +36 -0
  56. package/vite.config.ts +48 -0
@@ -0,0 +1,225 @@
1
+ import { formatDate, OrbitComponent, requestNavigation, sort } from "@helpers";
2
+ import { msg } from "@lit/localize";
3
+ import { Task } from "@lit/task";
4
+ import type { PropertiesPicker, Resource, SearchObject } from "@src/component";
5
+ import { css, html, nothing } from "lit";
6
+ import { customElement, property, state } from "lit/decorators.js";
7
+
8
+ @customElement("solid-fact-bundle")
9
+ export class SolidFactBundle extends OrbitComponent {
10
+ static styles = css`
11
+ .modal {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ right: 0;
16
+ bottom: 0;
17
+ background-color: rgba(0, 2, 49, 0.2);
18
+ z-index: 9999;
19
+ display: flex;
20
+ justify-content: center;
21
+ align-items: center;
22
+ }
23
+ `;
24
+
25
+ @property({ attribute: "header", type: String })
26
+ header?: string = "DS4GO Fact Bundling";
27
+
28
+ @state()
29
+ search: SearchObject[] = [];
30
+
31
+ @state()
32
+ resultCount = this.objects?.length || 0;
33
+
34
+ cherryPickedProperties: PropertiesPicker[] = [
35
+ { key: "created_at", value: "created_at", cast: formatDate },
36
+ { key: "updated_at", value: "updated_at", cast: formatDate },
37
+ { key: "name", value: "name" },
38
+ { key: "description", value: "description" },
39
+ ];
40
+
41
+ async _responseAdaptator(response: Resource): Promise<Resource> {
42
+ if (response?._originalResource?.hasType("ds4go:FactBundle")) {
43
+ response.facts = await Promise.all(
44
+ (await response._originalResource["ldp:contains"]).map(
45
+ (fact: Resource) => {
46
+ return this._getProxyValue(fact["@id"], false, [
47
+ { key: "updated_at", value: "updated_at", cast: formatDate },
48
+ { key: "name", value: "name" },
49
+ { key: "description", value: "description" },
50
+ {
51
+ key: "categories",
52
+ value: "categories",
53
+ expand: true,
54
+ cast: (c: Resource[]) => sort(c, "name"),
55
+ },
56
+ { key: "author", value: "author" },
57
+ { key: "link", value: "link" },
58
+ { key: "enclosure", value: "enclosure" },
59
+ { key: "medias", value: "medias", expand: true },
60
+ { key: "url", value: "url" },
61
+ { key: "file_size", value: "file_size" },
62
+ { key: "file_type", value: "file_type" },
63
+ { key: "width", value: "width" },
64
+ { key: "height", value: "height" },
65
+ // Can't get raw review json, as store does not support datas without ids
66
+ // { key: "review", value: "review" },
67
+ ]);
68
+ },
69
+ ),
70
+ );
71
+ console.log(response.facts);
72
+ }
73
+ return Promise.resolve(response);
74
+ }
75
+
76
+ async _afterAttach() {
77
+ this.menuComponent = document.querySelector(
78
+ `[uniq="${this.orbit?.getComponent("menu")?.uniq}"]`,
79
+ );
80
+
81
+ return Promise.resolve();
82
+ }
83
+
84
+ _getResource = new Task(this, {
85
+ task: async ([dataSrc, objSrc]) => {
86
+ if (
87
+ (!dataSrc && !objSrc) ||
88
+ !this.orbit ||
89
+ (!this.noRouter &&
90
+ this.route &&
91
+ this.currentRoute &&
92
+ !this.route.startsWith(this.currentRoute))
93
+ )
94
+ return;
95
+
96
+ if (!this.menuComponent.ready) {
97
+ await new Promise((resolve) => {
98
+ this.menuComponent.addEventListener("user-ready", () => {
99
+ resolve(true);
100
+ });
101
+ });
102
+ }
103
+
104
+ this.displayFiltering = !this.component.parameters.disableFiltering;
105
+
106
+ if (!this.hasCachedDatas || this.oldDataSrc !== dataSrc) {
107
+ if (!dataSrc) return;
108
+
109
+ this.objects = (await this._getProxyValue(dataSrc)) as Resource[];
110
+ this.hasCachedDatas = true;
111
+ }
112
+
113
+ if (this.oldDataSrc !== dataSrc) {
114
+ this.oldDataSrc = dataSrc;
115
+ }
116
+
117
+ if (!Array.isArray(this.objects)) {
118
+ this.objects = [];
119
+ }
120
+
121
+ this.object = this.objects.find((obj: Resource) => obj["@id"] === objSrc);
122
+
123
+ return sort(this.objects, "name", "asc");
124
+ },
125
+ args: () => [
126
+ this.defaultDataSrc,
127
+ this.dataSrc,
128
+ this.caching,
129
+ this.currentRoute,
130
+ ],
131
+ });
132
+
133
+ _search(e: Event) {
134
+ e.preventDefault();
135
+ this.search = e.detail;
136
+ this.filterCount = this.search.filter((s) => s.name !== "search").length;
137
+ }
138
+
139
+ _openModal(e: Event) {
140
+ e.preventDefault();
141
+ if (this.route) {
142
+ if ("use-id" in (this.component.routeAttributes || {})) {
143
+ requestNavigation(this.route, e.detail["@id"]);
144
+ } else {
145
+ const rdfType = e.detail["@type"]?.at(-1) ?? e.detail["@type"];
146
+ if (rdfType) {
147
+ const compatibleComponents = window.orbit?.components?.filter(
148
+ (c) => c?.routeAttributes?.["rdf-type"] === rdfType,
149
+ );
150
+
151
+ if (compatibleComponents?.[0]?.route) {
152
+ requestNavigation(compatibleComponents[0]?.route, e.detail["@id"]);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ _closeModal(e: Event) {
160
+ e.preventDefault();
161
+ if (this.route) requestNavigation(this.route, this.defaultDataSrc);
162
+ }
163
+
164
+ _closeModalFromBackground(e: Event) {
165
+ e.preventDefault();
166
+ if (this.route && e.target?.classList.contains("modal"))
167
+ requestNavigation(this.route, this.defaultDataSrc);
168
+ }
169
+
170
+ _resultCountUpdate(e: Event) {
171
+ this.resultCount = e.detail ?? 0;
172
+ }
173
+
174
+ render() {
175
+ return (
176
+ this.gatekeeper() ||
177
+ this._getResource.render({
178
+ pending: () => html`<solid-loader></solid-loader>`,
179
+ error: (e) => {
180
+ console.warn("[solid-fact-bundle] Task error:", e);
181
+ return nothing;
182
+ },
183
+ complete: (datas) => {
184
+ return html`<tems-viewport>
185
+ <tems-header slot="header" heading=${this.header}>
186
+ <div slot="cta">
187
+ <tems-button
188
+ type="primary"
189
+ label=${msg("Create a bundle")}
190
+ ></tems-button>
191
+ </div>
192
+ </tems-header>
193
+ <div slot="content">
194
+ <tems-catalog-filter-holder
195
+ .displayFiltering=${this.displayFiltering}
196
+ @search=${this._search}
197
+ .search=${this.search}
198
+ .objects=${datas}
199
+ .resultCount=${this.resultCount}
200
+ .filterCount=${this.filterCount}
201
+ ></tems-catalog-filter-holder>
202
+ <ds4go-fact-bundle-holder
203
+ .objects=${datas}
204
+ .search=${this.search}
205
+ @clicked=${this._openModal}
206
+ @result-count=${this._resultCountUpdate}
207
+ ></ds4go-fact-bundle-holder>
208
+ ${this.object
209
+ ? html`<div
210
+ class="modal"
211
+ @click=${this._closeModalFromBackground}
212
+ >
213
+ <ds4go-fact-bundle-modal
214
+ .object=${this.object}
215
+ @close=${this._closeModal}
216
+ ></ds4go-fact-bundle-modal>
217
+ </div>`
218
+ : nothing}
219
+ </div>
220
+ </tems-viewport>`;
221
+ },
222
+ })
223
+ );
224
+ }
225
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,450 @@
1
+ /**
2
+ * Resource Mapper Service
3
+ *
4
+ * Provides unified, configurable mapping from source data formats (FC self-descriptions,
5
+ * DSP datasets, etc.) to TEMS-compatible resource objects with LDP containers.
6
+ *
7
+ * Architecture:
8
+ * - Stores (sib-core): Fetch raw data
9
+ * - ResourceMapper (solid-tems): Transform raw data to TEMS format
10
+ * - Components (solid-tems): Configure mapper and render UI
11
+ *
12
+ * Usage:
13
+ * ```typescript
14
+ * const mapper = new ResourceMapper(fcMappingConfig);
15
+ * const temsResource = mapper.map(sourceData, context);
16
+ * ```
17
+ */
18
+
19
+ // import type { Resource } from '@startinblox/core';
20
+
21
+ // ============================================================================
22
+ // Types and Interfaces
23
+ // ============================================================================
24
+
25
+ export interface ResourceMapperConfig {
26
+ /**
27
+ * Base field mappings (simple property extractions)
28
+ */
29
+ baseFields: Record<string, FieldMapping>;
30
+
31
+ /**
32
+ * LDP Container field mappings (arrays wrapped in ldp:Container)
33
+ */
34
+ containerFields?: Record<string, ContainerFieldMapping>;
35
+
36
+ /**
37
+ * Nested object mappings
38
+ */
39
+ nestedObjects?: Record<string, NestedObjectMapping>;
40
+
41
+ /**
42
+ * Contract negotiation field mappings
43
+ */
44
+ contractFields?: Record<string, FieldMapping>;
45
+
46
+ /**
47
+ * Optional custom post-processing function
48
+ */
49
+ postProcess?: (resource: Resource, source: any, context: MappingContext) => Resource;
50
+ }
51
+
52
+ export interface FieldMapping {
53
+ /**
54
+ * Path to source value (array of keys to traverse)
55
+ */
56
+ source: string[];
57
+
58
+ /**
59
+ * Optional transform function
60
+ */
61
+ transform?: (value: any, source: any, context: MappingContext) => any;
62
+
63
+ /**
64
+ * Optional fallback if source is not found
65
+ */
66
+ fallback?: string | ((source: any, context: MappingContext) => any);
67
+
68
+ /**
69
+ * Optional default value if source and fallback are not found
70
+ */
71
+ defaultValue?: any;
72
+ }
73
+
74
+ export interface ContainerFieldMapping {
75
+ /**
76
+ * Path to source array
77
+ */
78
+ source: string[];
79
+
80
+ /**
81
+ * LDP container type (e.g., 'ldp:Container')
82
+ */
83
+ containerType: string;
84
+
85
+ /**
86
+ * Item type for contained objects (e.g., 'tems:Category')
87
+ */
88
+ itemType: string;
89
+
90
+ /**
91
+ * Field mappings for each item
92
+ */
93
+ itemFields: Record<string, (item: any, index: number, context: MappingContext) => any>;
94
+
95
+ /**
96
+ * Optional filter function to include/exclude items
97
+ */
98
+ filter?: (item: any, index: number, context: MappingContext) => boolean;
99
+
100
+ /**
101
+ * Optional transform for the entire array before item mapping
102
+ */
103
+ transform?: (value: any, source: any, context: MappingContext) => any[];
104
+ }
105
+
106
+ export interface NestedObjectMapping {
107
+ /**
108
+ * Path to source object
109
+ */
110
+ source: string[];
111
+
112
+ /**
113
+ * Field mappings for the nested object
114
+ */
115
+ fields: Record<string, string[] | FieldMapping>;
116
+
117
+ /**
118
+ * Optional type for the nested object
119
+ */
120
+ type?: string;
121
+ }
122
+
123
+ export interface MappingContext {
124
+ /**
125
+ * Base URL for generating @id values
126
+ */
127
+ temsServiceBase?: string;
128
+ temsCategoryBase?: string;
129
+ temsImageBase?: string;
130
+ temsProviderBase?: string;
131
+
132
+ /**
133
+ * Additional context data (provider info, etc.)
134
+ */
135
+ [key: string]: any;
136
+ }
137
+
138
+ // ============================================================================
139
+ // Transform Functions
140
+ // ============================================================================
141
+
142
+ /**
143
+ * Strips URN prefixes from IDs
144
+ */
145
+ export function stripUrnPrefix(value: string | undefined): string {
146
+ if (!value) return '';
147
+ return value
148
+ .replace(/^urn:uuid:/i, '')
149
+ .replace(/^urn:tems:/i, '');
150
+ }
151
+
152
+ /**
153
+ * Processes ODRL policy: strips URN prefixes and adds target field
154
+ */
155
+ export function processPolicyTransform(
156
+ policy: any,
157
+ source: any,
158
+ _context: MappingContext
159
+ ): any {
160
+ if (!policy) return null;
161
+
162
+ const processedPolicy = JSON.parse(JSON.stringify(policy)); // Deep clone
163
+
164
+ // Strip URN prefix from policy @id
165
+ if (processedPolicy['@id']) {
166
+ processedPolicy['@id'] = stripUrnPrefix(processedPolicy['@id']);
167
+ }
168
+
169
+ // Add target field (asset ID)
170
+ const assetId = getNestedValue(source, ['@id']) || '';
171
+ processedPolicy['target'] = stripUrnPrefix(assetId);
172
+
173
+ return processedPolicy;
174
+ }
175
+
176
+ /**
177
+ * Extracts first value from array or returns the value itself
178
+ */
179
+ export function firstOrSelf(value: any): any {
180
+ return Array.isArray(value) && value.length > 0 ? value[0] : value;
181
+ }
182
+
183
+ /**
184
+ * Generates unique ID based on name
185
+ */
186
+ export function generateIdFromName(name: string, base: string): string {
187
+ const slug = name
188
+ .toLowerCase()
189
+ .replace(/[^a-z0-9]+/g, '-')
190
+ .replace(/^-+|-+$/g, '');
191
+ return `${base}${slug}`;
192
+ }
193
+
194
+ // ============================================================================
195
+ // Helper Functions
196
+ // ============================================================================
197
+
198
+ /**
199
+ * Gets nested value from object using path array
200
+ */
201
+ export function getNestedValue(obj: any, path: string[]): any {
202
+ // If path is empty, return undefined (don't return the entire object)
203
+ // Empty paths are used when the field should be computed via transform or use defaultValue
204
+ if (path.length === 0) {
205
+ return undefined;
206
+ }
207
+
208
+ let current = obj;
209
+ for (const key of path) {
210
+ if (current === null || current === undefined) {
211
+ return undefined;
212
+ }
213
+ current = current[key];
214
+ }
215
+ return current;
216
+ }
217
+
218
+ /**
219
+ * Sets nested value in object using path array
220
+ */
221
+ export function setNestedValue(obj: any, path: string[], value: any): void {
222
+ if (path.length === 0) return;
223
+
224
+ let current = obj;
225
+ for (let i = 0; i < path.length - 1; i++) {
226
+ const key = path[i];
227
+ if (!(key in current)) {
228
+ current[key] = {};
229
+ }
230
+ current = current[key];
231
+ }
232
+
233
+ current[path[path.length - 1]] = value;
234
+ }
235
+
236
+ // ============================================================================
237
+ // ResourceMapper Class
238
+ // ============================================================================
239
+
240
+ export class ResourceMapper {
241
+ private config: ResourceMapperConfig;
242
+
243
+ constructor(config: ResourceMapperConfig) {
244
+ this.config = config;
245
+ }
246
+
247
+ /**
248
+ * Maps source data to TEMS-compatible resource
249
+ */
250
+ map(source: any, context: MappingContext = {}): Resource {
251
+ const resource: Resource = {};
252
+
253
+ // Map base fields
254
+ for (const [destKey, mapping] of Object.entries(this.config.baseFields)) {
255
+ const value = this.mapField(source, mapping, context);
256
+ if (value !== undefined) {
257
+ resource[destKey] = value;
258
+ }
259
+ }
260
+
261
+ // Map container fields
262
+ if (this.config.containerFields) {
263
+ for (const [destKey, mapping] of Object.entries(this.config.containerFields)) {
264
+ const container = this.mapContainerField(source, mapping, context);
265
+ if (container) {
266
+ resource[destKey] = container;
267
+ }
268
+ }
269
+ }
270
+
271
+ // Map nested objects
272
+ if (this.config.nestedObjects) {
273
+ for (const [destKey, mapping] of Object.entries(this.config.nestedObjects)) {
274
+ const nestedObj = this.mapNestedObject(source, mapping, context);
275
+ if (nestedObj) {
276
+ resource[destKey] = nestedObj;
277
+ }
278
+ }
279
+ }
280
+
281
+ // Map contract fields
282
+ if (this.config.contractFields) {
283
+ for (const [destKey, mapping] of Object.entries(this.config.contractFields)) {
284
+ const value = this.mapField(source, mapping, context);
285
+ if (value !== undefined) {
286
+ resource[destKey] = value;
287
+ }
288
+ }
289
+ }
290
+
291
+ // Apply post-processing
292
+ if (this.config.postProcess) {
293
+ return this.config.postProcess(resource, source, context);
294
+ }
295
+
296
+ return resource;
297
+ }
298
+
299
+ /**
300
+ * Maps a single field
301
+ */
302
+ private mapField(source: any, mapping: FieldMapping, context: MappingContext): any {
303
+ // Get source value
304
+ let value = getNestedValue(source, mapping.source);
305
+
306
+ // Apply transform
307
+ if (mapping.transform) {
308
+ value = mapping.transform(value, source, context);
309
+ }
310
+
311
+ // Apply fallback
312
+ if (value === undefined || value === null || value === '') {
313
+ if (mapping.fallback) {
314
+ if (typeof mapping.fallback === 'function') {
315
+ value = mapping.fallback(source, context);
316
+ } else {
317
+ // Fallback is a path to another field
318
+ value = getNestedValue(source, [mapping.fallback]);
319
+ }
320
+ }
321
+ }
322
+
323
+ // Apply default value
324
+ if (value === undefined || value === null || value === '') {
325
+ value = mapping.defaultValue;
326
+ }
327
+
328
+ return value;
329
+ }
330
+
331
+ /**
332
+ * Maps a container field (array wrapped in LDP container)
333
+ */
334
+ private mapContainerField(
335
+ source: any,
336
+ mapping: ContainerFieldMapping,
337
+ context: MappingContext
338
+ ): any {
339
+ // Get source array
340
+ let sourceArray = getNestedValue(source, mapping.source);
341
+
342
+ if (!sourceArray) {
343
+ return null;
344
+ }
345
+
346
+ // Apply array-level transform
347
+ if (mapping.transform) {
348
+ sourceArray = mapping.transform(sourceArray, source, context);
349
+ }
350
+
351
+ // Ensure it's an array
352
+ if (!Array.isArray(sourceArray)) {
353
+ sourceArray = [sourceArray];
354
+ }
355
+
356
+ // Filter items
357
+ if (mapping.filter) {
358
+ sourceArray = sourceArray.filter((item, index) =>
359
+ mapping.filter!(item, index, context)
360
+ );
361
+ }
362
+
363
+ // Map items
364
+ const items = sourceArray.map((item, index) => {
365
+ const mappedItem: any = {
366
+ '@type': mapping.itemType,
367
+ };
368
+
369
+ for (const [fieldKey, fieldFn] of Object.entries(mapping.itemFields)) {
370
+ const fieldValue = fieldFn(item, index, context);
371
+ if (fieldValue !== undefined) {
372
+ mappedItem[fieldKey] = fieldValue;
373
+ }
374
+ }
375
+
376
+ return mappedItem;
377
+ });
378
+
379
+ // Return LDP container
380
+ return {
381
+ '@type': mapping.containerType,
382
+ 'ldp:contains': items,
383
+ };
384
+ }
385
+
386
+ /**
387
+ * Maps a nested object
388
+ */
389
+ private mapNestedObject(
390
+ source: any,
391
+ mapping: NestedObjectMapping,
392
+ context: MappingContext
393
+ ): any {
394
+ // Get source object
395
+ const sourceObj = getNestedValue(source, mapping.source);
396
+
397
+ if (!sourceObj) {
398
+ return null;
399
+ }
400
+
401
+ const nestedObj: any = {};
402
+
403
+ // Add type if specified
404
+ if (mapping.type) {
405
+ nestedObj['@type'] = mapping.type;
406
+ }
407
+
408
+ // Map fields
409
+ for (const [destKey, fieldMapping] of Object.entries(mapping.fields)) {
410
+ let value: any;
411
+
412
+ if (Array.isArray(fieldMapping)) {
413
+ // Simple path mapping
414
+ value = getNestedValue(sourceObj, fieldMapping);
415
+ } else {
416
+ // Full FieldMapping
417
+ value = this.mapField(sourceObj, fieldMapping, context);
418
+ }
419
+
420
+ if (value !== undefined) {
421
+ nestedObj[destKey] = value;
422
+ }
423
+ }
424
+
425
+ return Object.keys(nestedObj).length > (mapping.type ? 1 : 0) ? nestedObj : null;
426
+ }
427
+
428
+ /**
429
+ * Unwraps LDP containers (converts ldp:contains arrays to plain arrays)
430
+ *
431
+ * This is typically called by components after mapping to simplify data structure
432
+ * for rendering.
433
+ */
434
+ static unwrapLdpContainers(resource: Resource): Resource {
435
+ const unwrapped: Resource = { ...resource };
436
+
437
+ for (const [key, value] of Object.entries(unwrapped)) {
438
+ if (
439
+ value &&
440
+ typeof value === 'object' &&
441
+ value['@type'] === 'ldp:Container' &&
442
+ value['ldp:contains']
443
+ ) {
444
+ unwrapped[key] = value['ldp:contains'];
445
+ }
446
+ }
447
+
448
+ return unwrapped;
449
+ }
450
+ }
@@ -0,0 +1,22 @@
1
+ import type {
2
+ LimitedResource,
3
+ Resource,
4
+ } from "@src/component";
5
+ import { LitElement } from "lit";
6
+
7
+ export class ComponentObjectHandler extends LitElement {
8
+ object?: LimitedResource = {
9
+ "@id": ""
10
+ };
11
+
12
+ isType = (type: string, obj: Resource = (this.object as Resource)) => {
13
+ const typeValue = obj["@type"];
14
+ if (Array.isArray(typeValue)) {
15
+ return typeValue.includes(type);
16
+ }
17
+ if (typeof typeValue === "string") {
18
+ return typeValue === type;
19
+ }
20
+ return false;
21
+ };
22
+ }
@@ -0,0 +1,14 @@
1
+ import type { Resource } from "@src/component";
2
+ import { LitElement } from "lit";
3
+
4
+ import { property } from "lit/decorators.js";
5
+
6
+ export class ComponentObjectsHandler extends LitElement {
7
+ @property({ attribute: false })
8
+ objects?: Resource[] = [];
9
+
10
+ hasType = (type: string, objs: Resource[] = this.objects as Resource[]) => {
11
+ const typeValue = new Set(objs.flatMap((o) => o["@type"]));
12
+ return typeValue.has(type);
13
+ };
14
+ }