@mmlogic/components 0.1.7 → 0.1.8
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/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/mosterdcomponents.cjs.js +1 -1
- package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +124 -28
- package/dist/collection/components/mrd-field/mrd-field.js +35 -16
- package/dist/collection/components/mrd-form/mrd-form.js +85 -2
- package/dist/collection/components/mrd-relation-field/mrd-relation-field.js +121 -12
- package/dist/collection/dev/api.js +198 -0
- package/dist/collection/dev/app.js +521 -0
- package/dist/collection/dev/auth.js +156 -0
- package/dist/collection/dev/example-data.js +256 -0
- package/dist/components/mrd-field2.js +1 -1
- package/dist/components/mrd-form.js +1 -1
- package/dist/components/mrd-relation-field2.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/mosterdcomponents.js +1 -1
- package/dist/esm/mrd-boolean-field_16.entry.js +125 -29
- package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
- package/dist/mosterdcomponents/p-5a453e03.entry.js +1 -0
- package/dist/types/components/mrd-field/mrd-field.d.ts +9 -0
- package/dist/types/components/mrd-form/mrd-form.d.ts +17 -0
- package/dist/types/components/mrd-relation-field/mrd-relation-field.d.ts +14 -1
- package/dist/types/components.d.ts +15 -2
- package/dist/types/types/client-layout.d.ts +1 -0
- package/package.json +1 -1
- package/dist/mosterdcomponents/p-45c40269.entry.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Host, h } from "@stencil/core";
|
|
2
2
|
import { t } from "../../utils/i18n";
|
|
3
3
|
import { validateRequired } from "../../utils/validation";
|
|
4
|
-
import { ClientLayoutItemRelationDisplayType } from "../../types";
|
|
4
|
+
import { ClientLayoutItemRelationDisplayType, ClientLayoutItemRelationEditBehavior } from "../../types";
|
|
5
5
|
export class MrdRelationField {
|
|
6
6
|
constructor() {
|
|
7
7
|
this.name = '';
|
|
@@ -19,6 +19,7 @@ export class MrdRelationField {
|
|
|
19
19
|
this.value = null;
|
|
20
20
|
this.searchQuery = '';
|
|
21
21
|
this.searchResults = [];
|
|
22
|
+
this.allRecords = [];
|
|
22
23
|
this.isLoading = false;
|
|
23
24
|
this.selectedItems = [];
|
|
24
25
|
this.showResults = false;
|
|
@@ -127,6 +128,9 @@ export class MrdRelationField {
|
|
|
127
128
|
this.mrdBlur.emit({ name: this.name, value: val });
|
|
128
129
|
};
|
|
129
130
|
}
|
|
131
|
+
async setAllRecords(records) {
|
|
132
|
+
this.allRecords = records;
|
|
133
|
+
}
|
|
130
134
|
async setSearchResults(results) {
|
|
131
135
|
this.searchResults = results;
|
|
132
136
|
this.isLoading = false;
|
|
@@ -140,24 +144,41 @@ export class MrdRelationField {
|
|
|
140
144
|
var _a;
|
|
141
145
|
// Pre-fill selectedItems when value is passed as { id, label } objects
|
|
142
146
|
// (e.g. when editing an existing record fetched from the API).
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
if (this.value) {
|
|
148
|
+
if (Array.isArray(this.value)) {
|
|
149
|
+
if (this.value.length > 0 && typeof this.value[0] === 'object') {
|
|
150
|
+
this.selectedItems = this.value;
|
|
151
|
+
this.searchQuery = '';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (typeof this.value === 'object') {
|
|
155
|
+
this.selectedItems = [this.value];
|
|
156
|
+
this.searchQuery = (_a = this.value.label) !== null && _a !== void 0 ? _a : '';
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
}
|
|
160
|
+
componentDidLoad() {
|
|
161
|
+
// Only emit when there is no commonRelation dependency — the form orchestrates those.
|
|
162
|
+
if (this.editBehavior === ClientLayoutItemRelationEditBehavior.DROPDOWN && !this.commonRelation) {
|
|
163
|
+
// Defer to next tick so parent event listeners are registered after DOM patching
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
this.mrdFetchAll.emit({
|
|
166
|
+
name: this.name,
|
|
167
|
+
relatedClass: this.relatedClass,
|
|
168
|
+
mostSignificantClass: this.mostSignificantClass || undefined,
|
|
169
|
+
});
|
|
170
|
+
}, 0);
|
|
154
171
|
}
|
|
155
172
|
}
|
|
156
173
|
render() {
|
|
157
|
-
var _a, _b;
|
|
174
|
+
var _a, _b, _c, _d;
|
|
158
175
|
const hasError = !!this.error;
|
|
159
|
-
if (this.
|
|
176
|
+
if (this.editBehavior === ClientLayoutItemRelationEditBehavior.DROPDOWN) {
|
|
160
177
|
const currentValue = Array.isArray(this.value) ? ((_a = this.value[0]) !== null && _a !== void 0 ? _a : '') : ((_b = this.value) !== null && _b !== void 0 ? _b : '');
|
|
178
|
+
return (h(Host, null, h("div", { class: "mrd-relation-field" }, this.label && (h("label", { class: `mrd-relation-field__label${this.required ? ' mrd-relation-field__label--required' : ''}` }, this.label)), h("select", { class: `mrd-relation-field__select${hasError ? ' mrd-relation-field__select--error' : ''}`, name: this.name, required: this.required, disabled: this.disabled, onChange: this.handleDropdownChange }, h("option", { value: "" }, t('select_placeholder', this.locale)), this.allRecords.map(record => (h("option", { key: record.id, value: record.id, selected: record.id === currentValue }, record.label)))), hasError && h("span", { class: "mrd-relation-field__error" }, this.error))));
|
|
179
|
+
}
|
|
180
|
+
if (this.displayType === ClientLayoutItemRelationDisplayType.DROPDOWN) {
|
|
181
|
+
const currentValue = Array.isArray(this.value) ? ((_c = this.value[0]) !== null && _c !== void 0 ? _c : '') : ((_d = this.value) !== null && _d !== void 0 ? _d : '');
|
|
161
182
|
return (h(Host, null, h("div", { class: "mrd-relation-field" }, this.label && (h("label", { class: `mrd-relation-field__label${this.required ? ' mrd-relation-field__label--required' : ''}` }, this.label)), h("select", { class: `mrd-relation-field__select${hasError ? ' mrd-relation-field__select--error' : ''}`, name: this.name, required: this.required, disabled: this.disabled, onChange: this.handleDropdownChange }, h("option", { value: "" }, t('select_placeholder', this.locale)), this.dropdownValues.map(dv => (h("option", { key: dv.key, value: dv.key, selected: dv.key === currentValue }, dv.label)))), hasError && h("span", { class: "mrd-relation-field__error" }, this.error))));
|
|
162
183
|
}
|
|
163
184
|
// SEARCH mode
|
|
@@ -349,6 +370,51 @@ export class MrdRelationField {
|
|
|
349
370
|
"attribute": "display-type",
|
|
350
371
|
"defaultValue": "ClientLayoutItemRelationDisplayType.SEARCH"
|
|
351
372
|
},
|
|
373
|
+
"editBehavior": {
|
|
374
|
+
"type": "string",
|
|
375
|
+
"mutable": false,
|
|
376
|
+
"complexType": {
|
|
377
|
+
"original": "ClientLayoutItemRelationEditBehavior | null",
|
|
378
|
+
"resolved": "ClientLayoutItemRelationEditBehavior.CHECKBOX | ClientLayoutItemRelationEditBehavior.DROPDOWN | ClientLayoutItemRelationEditBehavior.SEARCH | null | undefined",
|
|
379
|
+
"references": {
|
|
380
|
+
"ClientLayoutItemRelationEditBehavior": {
|
|
381
|
+
"location": "import",
|
|
382
|
+
"path": "../../types",
|
|
383
|
+
"id": "src/types/index.ts::ClientLayoutItemRelationEditBehavior",
|
|
384
|
+
"referenceLocation": "ClientLayoutItemRelationEditBehavior"
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
"required": false,
|
|
389
|
+
"optional": true,
|
|
390
|
+
"docs": {
|
|
391
|
+
"tags": [],
|
|
392
|
+
"text": ""
|
|
393
|
+
},
|
|
394
|
+
"getter": false,
|
|
395
|
+
"setter": false,
|
|
396
|
+
"reflect": false,
|
|
397
|
+
"attribute": "edit-behavior"
|
|
398
|
+
},
|
|
399
|
+
"commonRelation": {
|
|
400
|
+
"type": "string",
|
|
401
|
+
"mutable": false,
|
|
402
|
+
"complexType": {
|
|
403
|
+
"original": "string",
|
|
404
|
+
"resolved": "string | undefined",
|
|
405
|
+
"references": {}
|
|
406
|
+
},
|
|
407
|
+
"required": false,
|
|
408
|
+
"optional": true,
|
|
409
|
+
"docs": {
|
|
410
|
+
"tags": [],
|
|
411
|
+
"text": ""
|
|
412
|
+
},
|
|
413
|
+
"getter": false,
|
|
414
|
+
"setter": false,
|
|
415
|
+
"reflect": false,
|
|
416
|
+
"attribute": "common-relation"
|
|
417
|
+
},
|
|
352
418
|
"multiple": {
|
|
353
419
|
"type": "boolean",
|
|
354
420
|
"mutable": false,
|
|
@@ -427,6 +493,7 @@ export class MrdRelationField {
|
|
|
427
493
|
return {
|
|
428
494
|
"searchQuery": {},
|
|
429
495
|
"searchResults": {},
|
|
496
|
+
"allRecords": {},
|
|
430
497
|
"isLoading": {},
|
|
431
498
|
"selectedItems": {},
|
|
432
499
|
"showResults": {},
|
|
@@ -480,10 +547,52 @@ export class MrdRelationField {
|
|
|
480
547
|
"resolved": "{ query: string; relatedClass: string; }",
|
|
481
548
|
"references": {}
|
|
482
549
|
}
|
|
550
|
+
}, {
|
|
551
|
+
"method": "mrdFetchAll",
|
|
552
|
+
"name": "mrdFetchAll",
|
|
553
|
+
"bubbles": true,
|
|
554
|
+
"cancelable": true,
|
|
555
|
+
"composed": true,
|
|
556
|
+
"docs": {
|
|
557
|
+
"tags": [],
|
|
558
|
+
"text": ""
|
|
559
|
+
},
|
|
560
|
+
"complexType": {
|
|
561
|
+
"original": "{ name: string; relatedClass: string; mostSignificantClass?: string; commonRelation?: string; filter?: string; filterValue?: string }",
|
|
562
|
+
"resolved": "{ name: string; relatedClass: string; mostSignificantClass?: string | undefined; commonRelation?: string | undefined; filter?: string | undefined; filterValue?: string | undefined; }",
|
|
563
|
+
"references": {}
|
|
564
|
+
}
|
|
483
565
|
}];
|
|
484
566
|
}
|
|
485
567
|
static get methods() {
|
|
486
568
|
return {
|
|
569
|
+
"setAllRecords": {
|
|
570
|
+
"complexType": {
|
|
571
|
+
"signature": "(records: RelationSearchResult[]) => Promise<void>",
|
|
572
|
+
"parameters": [{
|
|
573
|
+
"name": "records",
|
|
574
|
+
"type": "RelationSearchResult[]",
|
|
575
|
+
"docs": ""
|
|
576
|
+
}],
|
|
577
|
+
"references": {
|
|
578
|
+
"Promise": {
|
|
579
|
+
"location": "global",
|
|
580
|
+
"id": "global::Promise"
|
|
581
|
+
},
|
|
582
|
+
"RelationSearchResult": {
|
|
583
|
+
"location": "import",
|
|
584
|
+
"path": "../../types",
|
|
585
|
+
"id": "src/types/index.ts::RelationSearchResult",
|
|
586
|
+
"referenceLocation": "RelationSearchResult"
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
"return": "Promise<void>"
|
|
590
|
+
},
|
|
591
|
+
"docs": {
|
|
592
|
+
"text": "",
|
|
593
|
+
"tags": []
|
|
594
|
+
}
|
|
595
|
+
},
|
|
487
596
|
"setSearchResults": {
|
|
488
597
|
"complexType": {
|
|
489
598
|
"signature": "(results: RelationSearchResult[]) => Promise<void>",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/* =====================================================================
|
|
2
|
+
API HELPERS
|
|
3
|
+
===================================================================== */
|
|
4
|
+
|
|
5
|
+
const API_BASE = 'http://localhost:8080';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generic fetch wrapper. Accepts both absolute URLs (from _links.self.href)
|
|
9
|
+
* and relative paths (prefixed with API_BASE).
|
|
10
|
+
*/
|
|
11
|
+
async function apiRequest(method, path, token, body) {
|
|
12
|
+
const opts = {
|
|
13
|
+
method,
|
|
14
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
15
|
+
};
|
|
16
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
17
|
+
const url = path.startsWith('http') ? path : API_BASE + path;
|
|
18
|
+
const resp = await fetch(url, opts);
|
|
19
|
+
const text = await resp.text();
|
|
20
|
+
let json;
|
|
21
|
+
try { json = JSON.parse(text); } catch (_) { json = text; }
|
|
22
|
+
return { status: resp.status, ok: resp.ok, body: json };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function apiFetchTenants(token) {
|
|
26
|
+
const { ok, status, body } = await apiRequest('GET', '/tenants', token);
|
|
27
|
+
if (!ok) throw new Error(`${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
28
|
+
return body; // array of { tenantCode, name, description, logo }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function apiFetchTypes(token, tenantCode) {
|
|
32
|
+
const { ok, status, body } = await apiRequest('GET', `/metadata/${tenantCode}/types?type=BASIC`, token);
|
|
33
|
+
if (!ok) throw new Error(`${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
34
|
+
return body; // array of { name, pluralName, type, ... }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function apiFetchForm(token, tenantCode, pluralName, formHref = null) {
|
|
38
|
+
const path = formHref || `/metadata/${tenantCode}/form?types=${pluralName}`;
|
|
39
|
+
const { ok, status, body } = await apiRequest('GET', path, token);
|
|
40
|
+
if (!ok) throw new Error(`${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
41
|
+
const raw = (body && body.layouts && body.layouts.length > 0) ? body.layouts[0]
|
|
42
|
+
: (Array.isArray(body) && body.length > 0) ? body[0]
|
|
43
|
+
: body;
|
|
44
|
+
return mapApiLayoutToMrdForm(raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function apiFetchDashboard(token, tenantCode, pluralName) {
|
|
48
|
+
const language = navigator.language;
|
|
49
|
+
const { ok, status, body } = await apiRequest('GET', `/metadata/${tenantCode}/dashboard/${pluralName}?language=${language}`, token);
|
|
50
|
+
if (!ok) throw new Error(`${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
51
|
+
return body; // { layouts, views, _links }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function apiFetchPage(token, baseHref, pageNumber, sort = '') {
|
|
55
|
+
const sep = baseHref.includes('?') ? '&' : '?';
|
|
56
|
+
let url = `${baseHref}${sep}page=${pageNumber}`;
|
|
57
|
+
if (sort) url += `&sort=${encodeURIComponent(sort)}`;
|
|
58
|
+
const { ok, status, body } = await apiRequest('GET', url, token);
|
|
59
|
+
if (!ok) throw new Error(`${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
60
|
+
return body; // { _embedded, _links, page }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function apiSubmitForm(token, tenantCode, pluralName, values) {
|
|
64
|
+
const { status, ok, body } = await apiRequest('POST', `/data/${tenantCode}/${pluralName}`, token, values);
|
|
65
|
+
return { status, ok, body };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function apiUploadFile(token, tenantCode, file) {
|
|
69
|
+
// Step 1: obtain a one-time upload URL
|
|
70
|
+
const { ok, status, body } = await apiRequest('GET', `/data/${tenantCode}/upload`, token);
|
|
71
|
+
if (!ok) throw new Error(`Upload-URL ophalen mislukt (${status})`);
|
|
72
|
+
const uploadUrl = typeof body === 'string' ? body : String(body);
|
|
73
|
+
|
|
74
|
+
// Step 2: upload the file as multipart (no auth required)
|
|
75
|
+
const formData = new FormData();
|
|
76
|
+
formData.append('file', file);
|
|
77
|
+
const resp = await fetch(uploadUrl, { method: 'POST', body: formData });
|
|
78
|
+
if (!resp.ok) throw new Error(`Bestand uploaden mislukt (${resp.status})`);
|
|
79
|
+
const uris = await resp.json();
|
|
80
|
+
if (!Array.isArray(uris) || uris.length === 0) throw new Error('Geen binary URI ontvangen na upload');
|
|
81
|
+
return uris[0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function apiSearchRelation(token, tenantCode, mostSignificantClass, query) {
|
|
85
|
+
const q = encodeURIComponent(query);
|
|
86
|
+
const { ok, status, body } = await apiRequest('GET', `/data/${tenantCode}/${mostSignificantClass}?q=${q}`, token);
|
|
87
|
+
if (!ok) throw new Error(`${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
88
|
+
const items = (body._embedded && body._embedded[mostSignificantClass]) || [];
|
|
89
|
+
return items.map(item => ({
|
|
90
|
+
id: item._links.self.href,
|
|
91
|
+
label: item.name || item.label || '?',
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* =====================================================================
|
|
96
|
+
LAYOUT MAPPERS
|
|
97
|
+
Translate flat API layout responses to nested ClientLayoutItem format
|
|
98
|
+
expected by mrd-form.
|
|
99
|
+
===================================================================== */
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Map a single flat API item to the nested ClientLayoutItem structure.
|
|
103
|
+
* Used by both mapApiLayoutToMrdForm (form) and mapApiColumns (table).
|
|
104
|
+
*/
|
|
105
|
+
function mapApiItem(item) {
|
|
106
|
+
if (item.type === 'FIELD') {
|
|
107
|
+
if (item.field) return item; // already nested — pass through
|
|
108
|
+
return {
|
|
109
|
+
type: 'FIELD',
|
|
110
|
+
field: {
|
|
111
|
+
name: item.name,
|
|
112
|
+
label: item.label,
|
|
113
|
+
dataType: item.dataType,
|
|
114
|
+
required: !!item.required,
|
|
115
|
+
multiple: !!item.multiple,
|
|
116
|
+
header: !!item.header,
|
|
117
|
+
defaultValue: item.defaultValue ?? null,
|
|
118
|
+
listItems: item.listItems ?? null,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (item.type === 'RELATION') {
|
|
123
|
+
if (item.relation) return item; // already nested — pass through
|
|
124
|
+
return {
|
|
125
|
+
type: 'RELATION',
|
|
126
|
+
relation: {
|
|
127
|
+
name: item.name,
|
|
128
|
+
label: item.label,
|
|
129
|
+
relatedClass: item.relatedClass,
|
|
130
|
+
mostSignificantClass: item.mostSignificantClass ?? null,
|
|
131
|
+
displayType: item.editBehavior ?? 'SEARCH',
|
|
132
|
+
editBehavior: item.editBehavior ?? null,
|
|
133
|
+
commonRelation: item.commonRelation ?? null,
|
|
134
|
+
required: !!item.required,
|
|
135
|
+
multiple: !!item.multiple,
|
|
136
|
+
defaultValue: item.defaultValue ?? null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(item.items)) {
|
|
141
|
+
return { ...item, items: item.items.map(mapApiItem) };
|
|
142
|
+
}
|
|
143
|
+
return item;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Map API layout (OBJECT_FORM_DASHBOARD shape) → ClientLayout for mrd-form.
|
|
148
|
+
*
|
|
149
|
+
* IMPORTANT: always map editBehavior and commonRelation from the API response —
|
|
150
|
+
* omitting them causes relation fields to fall back to SEARCH mode.
|
|
151
|
+
* _relationMeta must be keyed by BOTH relatedClass AND mostSignificantClass
|
|
152
|
+
* because mrdSearch sends mostSignificantClass as the lookup key.
|
|
153
|
+
*/
|
|
154
|
+
function mapApiLayoutToMrdForm(raw) {
|
|
155
|
+
if (!raw || !Array.isArray(raw.items)) return raw;
|
|
156
|
+
|
|
157
|
+
function mapItem(item) {
|
|
158
|
+
if (item.type === 'FIELD') {
|
|
159
|
+
return {
|
|
160
|
+
type: 'FIELD',
|
|
161
|
+
field: {
|
|
162
|
+
name: item.name,
|
|
163
|
+
label: item.label,
|
|
164
|
+
dataType: item.dataType,
|
|
165
|
+
required: !!item.required,
|
|
166
|
+
multiple: !!item.multiple,
|
|
167
|
+
header: !!item.header,
|
|
168
|
+
defaultValue: item.defaultValue ?? null,
|
|
169
|
+
listItems: item.listItems ?? null,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (item.type === 'RELATION') {
|
|
174
|
+
return {
|
|
175
|
+
type: 'RELATION',
|
|
176
|
+
relation: {
|
|
177
|
+
name: item.name,
|
|
178
|
+
label: item.label,
|
|
179
|
+
relatedClass: item.relatedClass,
|
|
180
|
+
mostSignificantClass: item.mostSignificantClass ?? null,
|
|
181
|
+
displayType: item.editBehavior ?? 'SEARCH',
|
|
182
|
+
editBehavior: item.editBehavior ?? null,
|
|
183
|
+
commonRelation: item.commonRelation ?? null,
|
|
184
|
+
required: !!item.required,
|
|
185
|
+
multiple: !!item.multiple,
|
|
186
|
+
defaultValue: item.defaultValue ?? null,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// SECTION / GROUP — recurse into children
|
|
191
|
+
if (Array.isArray(item.items)) {
|
|
192
|
+
return { ...item, items: item.items.map(mapItem) };
|
|
193
|
+
}
|
|
194
|
+
return item;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { items: raw.items.map(mapItem) };
|
|
198
|
+
}
|