@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.
@@ -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 (!this.value)
144
- return;
145
- if (Array.isArray(this.value)) {
146
- if (this.value.length > 0 && typeof this.value[0] === 'object') {
147
- this.selectedItems = this.value;
148
- this.searchQuery = '';
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
- else if (typeof this.value === 'object') {
152
- this.selectedItems = [this.value];
153
- this.searchQuery = (_a = this.value.label) !== null && _a !== void 0 ? _a : '';
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.displayType === ClientLayoutItemRelationDisplayType.DROPDOWN) {
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
+ }