@mmlogic/components 0.1.4 → 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 = '';
@@ -10,6 +10,8 @@ export class MrdRelationField {
10
10
  this.disabled = false;
11
11
  this.locale = navigator.language;
12
12
  this.relatedClass = '';
13
+ /** When set, used instead of relatedClass for search queries (mostSignificantClass from API). */
14
+ this.mostSignificantClass = '';
13
15
  this.displayType = ClientLayoutItemRelationDisplayType.SEARCH;
14
16
  this.multiple = false;
15
17
  this.dropdownValues = [];
@@ -17,6 +19,7 @@ export class MrdRelationField {
17
19
  this.value = null;
18
20
  this.searchQuery = '';
19
21
  this.searchResults = [];
22
+ this.allRecords = [];
20
23
  this.isLoading = false;
21
24
  this.selectedItems = [];
22
25
  this.showResults = false;
@@ -67,7 +70,7 @@ export class MrdRelationField {
67
70
  this.isLoading = true;
68
71
  this.showResults = true;
69
72
  this.searchDebounce = setTimeout(() => {
70
- this.mrdSearch.emit({ query, relatedClass: this.relatedClass });
73
+ this.mrdSearch.emit({ query, relatedClass: this.mostSignificantClass });
71
74
  }, 300);
72
75
  }
73
76
  else {
@@ -125,6 +128,9 @@ export class MrdRelationField {
125
128
  this.mrdBlur.emit({ name: this.name, value: val });
126
129
  };
127
130
  }
131
+ async setAllRecords(records) {
132
+ this.allRecords = records;
133
+ }
128
134
  async setSearchResults(results) {
129
135
  this.searchResults = results;
130
136
  this.isLoading = false;
@@ -138,24 +144,41 @@ export class MrdRelationField {
138
144
  var _a;
139
145
  // Pre-fill selectedItems when value is passed as { id, label } objects
140
146
  // (e.g. when editing an existing record fetched from the API).
141
- if (!this.value)
142
- return;
143
- if (Array.isArray(this.value)) {
144
- if (this.value.length > 0 && typeof this.value[0] === 'object') {
145
- this.selectedItems = this.value;
146
- 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 : '';
147
157
  }
148
158
  }
149
- else if (typeof this.value === 'object') {
150
- this.selectedItems = [this.value];
151
- 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);
152
171
  }
153
172
  }
154
173
  render() {
155
- var _a, _b;
174
+ var _a, _b, _c, _d;
156
175
  const hasError = !!this.error;
157
- if (this.displayType === ClientLayoutItemRelationDisplayType.DROPDOWN) {
176
+ if (this.editBehavior === ClientLayoutItemRelationEditBehavior.DROPDOWN) {
158
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 : '');
159
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))));
160
183
  }
161
184
  // SEARCH mode
@@ -300,6 +323,26 @@ export class MrdRelationField {
300
323
  "attribute": "related-class",
301
324
  "defaultValue": "''"
302
325
  },
326
+ "mostSignificantClass": {
327
+ "type": "string",
328
+ "mutable": false,
329
+ "complexType": {
330
+ "original": "string",
331
+ "resolved": "string",
332
+ "references": {}
333
+ },
334
+ "required": false,
335
+ "optional": false,
336
+ "docs": {
337
+ "tags": [],
338
+ "text": "When set, used instead of relatedClass for search queries (mostSignificantClass from API)."
339
+ },
340
+ "getter": false,
341
+ "setter": false,
342
+ "reflect": false,
343
+ "attribute": "most-significant-class",
344
+ "defaultValue": "''"
345
+ },
303
346
  "displayType": {
304
347
  "type": "string",
305
348
  "mutable": false,
@@ -327,6 +370,51 @@ export class MrdRelationField {
327
370
  "attribute": "display-type",
328
371
  "defaultValue": "ClientLayoutItemRelationDisplayType.SEARCH"
329
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
+ },
330
418
  "multiple": {
331
419
  "type": "boolean",
332
420
  "mutable": false,
@@ -405,6 +493,7 @@ export class MrdRelationField {
405
493
  return {
406
494
  "searchQuery": {},
407
495
  "searchResults": {},
496
+ "allRecords": {},
408
497
  "isLoading": {},
409
498
  "selectedItems": {},
410
499
  "showResults": {},
@@ -458,10 +547,52 @@ export class MrdRelationField {
458
547
  "resolved": "{ query: string; relatedClass: string; }",
459
548
  "references": {}
460
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
+ }
461
565
  }];
462
566
  }
463
567
  static get methods() {
464
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
+ },
465
596
  "setSearchResults": {
466
597
  "complexType": {
467
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
+ }