@mmlogic/components 0.1.30 → 0.3.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/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/mosterdcomponents.cjs.js +1 -1
- package/dist/cjs/mrd-boolean-field_19.cjs.entry.js +349 -297
- package/dist/collection/components/mrd-field/mrd-field.js +18 -20
- package/dist/collection/components/mrd-form/mrd-form.js +38 -46
- package/dist/collection/components/mrd-layout-section/mrd-layout-section.js +82 -212
- package/dist/collection/components/mrd-table/mrd-table.js +255 -219
- package/dist/collection/dev/api.js +32 -103
- package/dist/collection/dev/app.js +386 -283
- package/dist/collection/dev/example-data.js +111 -299
- package/dist/collection/utils/cell-renderer.js +7 -5
- package/dist/components/mrd-field2.js +1 -1
- package/dist/components/mrd-form.js +1 -1
- package/dist/components/mrd-layout-section.js +1 -1
- package/dist/components/mrd-table2.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/mosterdcomponents.js +1 -1
- package/dist/esm/mrd-boolean-field_19.entry.js +349 -297
- package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
- package/dist/mosterdcomponents/p-f6d0f02b.entry.js +1 -0
- package/dist/types/components/mrd-layout-section/mrd-layout-section.d.ts +16 -29
- package/dist/types/components/mrd-table/mrd-table.d.ts +36 -24
- package/dist/types/components.d.ts +39 -85
- package/dist/types/types/client-layout.d.ts +39 -15
- package/package.json +1 -1
- package/dist/mosterdcomponents/p-9ecefa81.entry.js +0 -1
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
APP STATE
|
|
3
3
|
===================================================================== */
|
|
4
4
|
|
|
5
|
-
let _selectedTenant
|
|
6
|
-
let _selectedType
|
|
7
|
-
let _relationMeta
|
|
8
|
-
let
|
|
9
|
-
let _locale
|
|
10
|
-
let
|
|
11
|
-
let
|
|
5
|
+
let _selectedTenant = null;
|
|
6
|
+
let _selectedType = null; // { name, pluralName }
|
|
7
|
+
let _relationMeta = {}; // relatedClass / mostSignificantClass → { name, mostSignificantClass }
|
|
8
|
+
let _baseHref = null; // tenant base href: /data/{tenant}
|
|
9
|
+
let _locale = navigator.language || 'nl-NL';
|
|
10
|
+
let _dashboardData = null; // full dashboard response
|
|
11
|
+
let _dashboardRecord = null; // data-object voor object/form dashboards
|
|
12
|
+
let _activeLayoutIndex = 0;
|
|
13
|
+
let _dashboardType = 'class'; // 'class' | 'general' | 'navigation' | 'object' | 'form'
|
|
14
|
+
let _sectionGeneration = 0; // incremented on each renderSection(); prevents stale fetches
|
|
15
|
+
let _navHistory = []; // stack van { dashboardData, dashboardRecord, activeLayoutIndex }
|
|
12
16
|
|
|
13
17
|
/* =====================================================================
|
|
14
18
|
UTILITIES
|
|
@@ -21,6 +25,29 @@ function escHtml(str) {
|
|
|
21
25
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function formatJson(value) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
31
|
+
const json = JSON.stringify(parsed, null, 2);
|
|
32
|
+
return json.replace(
|
|
33
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
34
|
+
match => {
|
|
35
|
+
let style = 'color:#2aa198';
|
|
36
|
+
if (/^"/.test(match)) {
|
|
37
|
+
style = /:$/.test(match) ? 'color:#81a1c1' : 'color:#a3be8c';
|
|
38
|
+
} else if (/true|false/.test(match)) {
|
|
39
|
+
style = 'color:#ebcb8b';
|
|
40
|
+
} else if (/null/.test(match)) {
|
|
41
|
+
style = 'color:#bf616a';
|
|
42
|
+
}
|
|
43
|
+
return `<span style="${style}">${match}</span>`;
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
} catch {
|
|
47
|
+
return escHtml(String(value));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
function httpStatusText(code) {
|
|
25
52
|
const map = {
|
|
26
53
|
200: 'OK', 201: 'Created', 204: 'No Content',
|
|
@@ -31,9 +58,43 @@ function httpStatusText(code) {
|
|
|
31
58
|
return map[code] || '';
|
|
32
59
|
}
|
|
33
60
|
|
|
61
|
+
const EVENT_LOG_MAX = 200;
|
|
62
|
+
|
|
34
63
|
function logEvent(type, data) {
|
|
35
|
-
document.getElementById('event-log')
|
|
36
|
-
|
|
64
|
+
const log = document.getElementById('event-log');
|
|
65
|
+
|
|
66
|
+
const empty = log.querySelector('.event-log-empty');
|
|
67
|
+
if (empty) empty.remove();
|
|
68
|
+
|
|
69
|
+
// Trim oldest entries when the log grows too large
|
|
70
|
+
while (log.children.length >= EVENT_LOG_MAX) log.removeChild(log.firstChild);
|
|
71
|
+
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const time = now.toTimeString().slice(0, 8) + '.' + String(now.getMilliseconds()).padStart(3, '0');
|
|
74
|
+
const json = data !== undefined ? JSON.stringify(data, null, 2) : null;
|
|
75
|
+
|
|
76
|
+
const entry = document.createElement('div');
|
|
77
|
+
entry.className = 'event-log-entry';
|
|
78
|
+
entry.dataset.type = type;
|
|
79
|
+
entry.innerHTML = `
|
|
80
|
+
<div class="event-log-entry-head">
|
|
81
|
+
<span class="event-log-time">${escHtml(time)}</span>
|
|
82
|
+
<span class="event-log-type">${escHtml(type)}</span>
|
|
83
|
+
${json ? '<span class="event-log-arrow">▶</span>' : ''}
|
|
84
|
+
</div>
|
|
85
|
+
${json ? `<pre class="event-log-detail">${escHtml(json)}</pre>` : ''}`;
|
|
86
|
+
|
|
87
|
+
entry.querySelector('.event-log-entry-head')?.addEventListener('click', () => {
|
|
88
|
+
if (json) entry.classList.toggle('open');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
log.appendChild(entry);
|
|
92
|
+
log.scrollTop = log.scrollHeight;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clearEventLog() {
|
|
96
|
+
const log = document.getElementById('event-log');
|
|
97
|
+
log.innerHTML = '<span class="event-log-empty">Wacht op events…</span>';
|
|
37
98
|
}
|
|
38
99
|
|
|
39
100
|
/* =====================================================================
|
|
@@ -139,293 +200,339 @@ function showCompanyList() {
|
|
|
139
200
|
===================================================================== */
|
|
140
201
|
|
|
141
202
|
async function loadTenants() {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
grid.innerHTML = '<span class="spinner"></span>';
|
|
203
|
+
document.getElementById('panel-controls').classList.remove('hidden');
|
|
204
|
+
const sel = document.getElementById('tenant-select-compact');
|
|
205
|
+
sel.innerHTML = '<option value="">⏳ laden...</option>';
|
|
146
206
|
|
|
147
207
|
try {
|
|
148
208
|
const tenants = await apiFetchTenants(authGetToken());
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const code = t.tenantCode || t.code || t.id || String(i);
|
|
164
|
-
const name = t.name || code;
|
|
165
|
-
const desc = t.description || '';
|
|
166
|
-
return `
|
|
167
|
-
<div class="tenant-card">
|
|
168
|
-
<input type="radio" name="tenant" id="tenant-${escHtml(code)}" value="${escHtml(code)}"
|
|
169
|
-
onchange="onTenantChange('${escHtml(code)}', '${escHtml(name)}')">
|
|
170
|
-
<label for="tenant-${escHtml(code)}">
|
|
171
|
-
${escHtml(name)}
|
|
172
|
-
${desc ? `<small>${escHtml(desc)}</small>` : ''}
|
|
173
|
-
</label>
|
|
174
|
-
</div>`;
|
|
175
|
-
}).join('');
|
|
176
|
-
|
|
177
|
-
const savedTenant = localStorage.getItem('last_tenant');
|
|
178
|
-
if (savedTenant) {
|
|
179
|
-
const radio = document.getElementById(`tenant-${savedTenant}`);
|
|
180
|
-
if (radio) {
|
|
181
|
-
radio.checked = true;
|
|
182
|
-
const label = document.querySelector(`label[for="tenant-${savedTenant}"]`);
|
|
183
|
-
const name = label ? (label.firstChild?.textContent?.trim() || savedTenant) : savedTenant;
|
|
184
|
-
onTenantChange(savedTenant, name);
|
|
209
|
+
if (!tenants || tenants.length === 0) {
|
|
210
|
+
sel.innerHTML = '<option value="">— geen tenants gevonden —</option>';
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
sel.innerHTML = '<option value="">— selecteer tenant —</option>' +
|
|
214
|
+
tenants.map(t => {
|
|
215
|
+
const code = t.tenantCode || t.code || t.id;
|
|
216
|
+
return `<option value="${escHtml(code)}">${escHtml(t.name || code)}</option>`;
|
|
217
|
+
}).join('');
|
|
218
|
+
|
|
219
|
+
const saved = localStorage.getItem('last_tenant');
|
|
220
|
+
if (saved) {
|
|
221
|
+
sel.value = saved;
|
|
222
|
+
onCompactTenantChange(saved);
|
|
185
223
|
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
sel.innerHTML = `<option value="">❌ ${escHtml(err.message)}</option>`;
|
|
186
226
|
}
|
|
187
227
|
}
|
|
188
228
|
|
|
189
|
-
|
|
229
|
+
function onCompactTenantChange(code) {
|
|
190
230
|
_selectedTenant = code;
|
|
231
|
+
_baseHref = `/data/${code}`;
|
|
191
232
|
localStorage.setItem('last_tenant', code);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
document.getElementById('panel-form').classList.add('hidden');
|
|
195
|
-
document.getElementById('panel-response').innerHTML = '';
|
|
233
|
+
if (_dashboardType === 'class' && code) loadCompactTypes(code);
|
|
234
|
+
}
|
|
196
235
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const sel = document.getElementById('type-select');
|
|
236
|
+
async function loadCompactTypes(tenantCode) {
|
|
237
|
+
const sel = document.getElementById('type-select-compact');
|
|
200
238
|
sel.innerHTML = '<option value="">⏳ laden...</option>';
|
|
201
239
|
sel.disabled = true;
|
|
202
|
-
|
|
203
240
|
try {
|
|
204
|
-
const types = await apiFetchTypes(authGetToken(),
|
|
205
|
-
|
|
241
|
+
const types = await apiFetchTypes(authGetToken(), tenantCode);
|
|
242
|
+
if (!types || types.length === 0) {
|
|
243
|
+
sel.innerHTML = '<option value="">— geen types —</option>';
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
sel.innerHTML = '<option value="">— selecteer type —</option>' +
|
|
247
|
+
types.map(t => {
|
|
248
|
+
const plural = t.pluralName || t.name;
|
|
249
|
+
return `<option value="${escHtml(plural)}">${escHtml(t.name)}</option>`;
|
|
250
|
+
}).join('');
|
|
251
|
+
sel.disabled = false;
|
|
252
|
+
|
|
253
|
+
const saved = localStorage.getItem('last_type');
|
|
254
|
+
if (saved && sel.querySelector(`option[value="${saved}"]`)) {
|
|
255
|
+
sel.value = saved;
|
|
256
|
+
}
|
|
206
257
|
} catch (err) {
|
|
207
|
-
sel.innerHTML =
|
|
258
|
+
sel.innerHTML = `<option value="">❌ ${escHtml(err.message)}</option>`;
|
|
208
259
|
sel.disabled = false;
|
|
209
|
-
typePanel.querySelector('h3').insertAdjacentHTML('afterend',
|
|
210
|
-
`<p style="color:var(--mrd-color-danger);font-size:.875rem;margin:0 0 .75rem">❌ ${escHtml(err.message)}</p>`);
|
|
211
260
|
}
|
|
212
261
|
}
|
|
213
262
|
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
sel.innerHTML = '<option value="">— selecteer type —</option>' +
|
|
222
|
-
types.map(t => {
|
|
223
|
-
const plural = t.pluralName || t.name;
|
|
224
|
-
return `<option value="${escHtml(plural)}" data-name="${escHtml(t.name)}">${escHtml(t.name)}</option>`;
|
|
225
|
-
}).join('');
|
|
226
|
-
sel.disabled = false;
|
|
263
|
+
function onDashboardTypeChange(type) {
|
|
264
|
+
_dashboardType = type;
|
|
265
|
+
document.getElementById('controls-class-group').classList.toggle('hidden', type !== 'class');
|
|
266
|
+
document.getElementById('controls-object-group').classList.toggle('hidden', type !== 'object' && type !== 'form');
|
|
267
|
+
if (type === 'class' && _selectedTenant) loadCompactTypes(_selectedTenant);
|
|
227
268
|
}
|
|
228
269
|
|
|
229
270
|
/* =====================================================================
|
|
230
|
-
|
|
271
|
+
DASHBOARD — laad + render layout sections
|
|
231
272
|
===================================================================== */
|
|
232
273
|
|
|
233
|
-
async function
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
if (!pluralName) return;
|
|
237
|
-
|
|
238
|
-
const opt = sel.options[sel.selectedIndex];
|
|
239
|
-
_selectedType = { name: opt.dataset.name || pluralName, pluralName };
|
|
274
|
+
async function loadDashboard() {
|
|
275
|
+
const name = document.getElementById('dashboard-name-input').value.trim() || undefined;
|
|
276
|
+
const token = authGetToken();
|
|
240
277
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
document.getElementById('panel-form').classList.add('hidden');
|
|
278
|
+
document.getElementById('sections-container').innerHTML =
|
|
279
|
+
'<span class="spinner"></span> Dashboard laden...';
|
|
280
|
+
document.getElementById('panel-sections').classList.remove('hidden');
|
|
245
281
|
document.getElementById('panel-response').innerHTML = '';
|
|
246
|
-
_tableDataHref = null;
|
|
247
282
|
|
|
248
283
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
let dashboard;
|
|
285
|
+
let recordData = null;
|
|
286
|
+
|
|
287
|
+
if (_dashboardType === 'class') {
|
|
288
|
+
const pluralName = document.getElementById('type-select-compact').value;
|
|
289
|
+
if (!pluralName) throw new Error('Selecteer een type');
|
|
290
|
+
const opt = document.getElementById('type-select-compact').selectedOptions[0];
|
|
291
|
+
_selectedType = { name: opt.text, pluralName };
|
|
292
|
+
localStorage.setItem('last_type', pluralName);
|
|
293
|
+
dashboard = await apiFetchClassDashboard(token, _selectedTenant, pluralName, name);
|
|
294
|
+
|
|
295
|
+
} else if (_dashboardType === 'general') {
|
|
296
|
+
dashboard = await apiFetchGeneralDashboard(token, _selectedTenant, name);
|
|
297
|
+
|
|
298
|
+
} else if (_dashboardType === 'navigation') {
|
|
299
|
+
dashboard = await apiFetchNavigationPane(token, _selectedTenant, name);
|
|
300
|
+
|
|
301
|
+
} else {
|
|
302
|
+
// 'object' of 'form': fetch record → gebruik _links.metadata.href of _links.form.href
|
|
303
|
+
const objectHref = document.getElementById('object-href-input').value.trim();
|
|
304
|
+
if (!objectHref) throw new Error('Vul een Object URL in');
|
|
305
|
+
const recResp = await apiRequest('GET', objectHref, token);
|
|
306
|
+
if (!recResp.ok) throw new Error(`${recResp.status}: kon object niet ophalen`);
|
|
307
|
+
recordData = recResp.body;
|
|
308
|
+
|
|
309
|
+
const linkKey = _dashboardType === 'form' ? 'form' : 'metadata';
|
|
310
|
+
const dashHref = recordData._links?.[linkKey]?.href;
|
|
311
|
+
if (!dashHref) throw new Error(`Geen _links.${linkKey} gevonden in object`);
|
|
312
|
+
|
|
313
|
+
const dashResp = await apiRequest('GET', dashHref, token);
|
|
314
|
+
if (!dashResp.ok) throw new Error(`${dashResp.status}: dashboard ophalen mislukt`);
|
|
315
|
+
dashboard = dashResp.body;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_dashboardData = dashboard;
|
|
319
|
+
_dashboardRecord = recordData;
|
|
320
|
+
_activeLayoutIndex = 0;
|
|
321
|
+
_navHistory = [];
|
|
322
|
+
|
|
323
|
+
renderSectionTabs(dashboard.layouts ?? []);
|
|
324
|
+
renderSection(0);
|
|
325
|
+
updateBackButton();
|
|
278
326
|
} catch (err) {
|
|
279
|
-
|
|
327
|
+
document.getElementById('sections-container').innerHTML =
|
|
328
|
+
`<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
|
|
280
329
|
}
|
|
281
330
|
}
|
|
282
331
|
|
|
283
|
-
function
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
332
|
+
function renderSectionTabs(layouts) {
|
|
333
|
+
const bar = document.getElementById('sections-tab-bar');
|
|
334
|
+
bar.innerHTML = '';
|
|
335
|
+
if (layouts.length <= 1) return;
|
|
336
|
+
|
|
337
|
+
layouts.forEach((layout, i) => {
|
|
338
|
+
const label = layout.label ?? layout.type ?? `Sectie ${i + 1}`;
|
|
339
|
+
const btn = document.createElement('button');
|
|
340
|
+
btn.className = 'section-tab-btn' + (i === 0 ? ' active' : '');
|
|
341
|
+
btn.textContent = label;
|
|
342
|
+
btn.onclick = () => renderSection(i);
|
|
343
|
+
bar.appendChild(btn);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function renderSection(index) {
|
|
348
|
+
_activeLayoutIndex = index;
|
|
349
|
+
const generation = ++_sectionGeneration; // snapshot for stale-fetch detection
|
|
350
|
+
|
|
351
|
+
document.querySelectorAll('.section-tab-btn').forEach((btn, i) =>
|
|
352
|
+
btn.classList.toggle('active', i === index));
|
|
353
|
+
|
|
354
|
+
const layout = _dashboardData.layouts[index];
|
|
355
|
+
document.getElementById('json-viewer').innerHTML = formatJson(layout);
|
|
356
|
+
|
|
357
|
+
const container = document.getElementById('sections-container');
|
|
358
|
+
container.innerHTML = '';
|
|
359
|
+
|
|
360
|
+
const section = document.createElement('mrd-layout-section');
|
|
361
|
+
section.items = layout.items;
|
|
362
|
+
section.data = _dashboardRecord ?? { _links: _dashboardData._links ?? {} };
|
|
363
|
+
section.locale = _locale;
|
|
364
|
+
|
|
365
|
+
section.addEventListener('mrdLoadViewPage', async (e) => {
|
|
366
|
+
if (generation !== _sectionGeneration) return;
|
|
367
|
+
const { name, page, path, qs } = e.detail;
|
|
368
|
+
logEvent('mrdLoadViewPage', e.detail);
|
|
369
|
+
try {
|
|
370
|
+
const url = `${_baseHref}${path}${qs ? '?' + qs : ''}`;
|
|
371
|
+
const result = await apiFetchPage(authGetToken(), url, page);
|
|
372
|
+
const rows = Object.values(result._embedded ?? {})[0] ?? [];
|
|
373
|
+
const total = result.page?.totalElements;
|
|
374
|
+
await section.setViewPage(name, page, rows, total);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.error('[mrdLoadViewPage] mislukt', name, err);
|
|
327
377
|
}
|
|
378
|
+
});
|
|
328
379
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
380
|
+
section.addEventListener('mrdLoadViewAggregations', async (e) => {
|
|
381
|
+
if (generation !== _sectionGeneration) return;
|
|
382
|
+
const { name, path, qs: filterQs, sum, avg, count } = e.detail;
|
|
383
|
+
logEvent('mrdLoadViewAggregations', e.detail);
|
|
384
|
+
try {
|
|
385
|
+
const aggUrl = `${_baseHref}${path}/aggregations`;
|
|
386
|
+
const p = new URLSearchParams(filterQs ?? '');
|
|
387
|
+
p.delete('page');
|
|
388
|
+
p.delete('sort');
|
|
389
|
+
if (sum?.length) p.set('sum', sum.join(','));
|
|
390
|
+
if (avg?.length) p.set('avg', avg.join(','));
|
|
391
|
+
if (count?.length) p.set('count', count.join(','));
|
|
392
|
+
const qs = p.toString();
|
|
393
|
+
const result = await apiRequest('GET', qs ? `${aggUrl}?${qs}` : aggUrl, authGetToken());
|
|
394
|
+
if (result.ok) await section.setViewAggregations(name, result.body);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.error('[mrdLoadViewAggregations] mislukt', name, err);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
336
399
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
await table.setPage(page, rows);
|
|
341
|
-
} catch (err) {
|
|
342
|
-
console.error('[mrd-table] pagina laden mislukt', page, err);
|
|
343
|
-
}
|
|
344
|
-
});
|
|
400
|
+
section.addEventListener('mrdViewAction', async (e) => {
|
|
401
|
+
logEvent('mrdViewAction', e.detail);
|
|
402
|
+
});
|
|
345
403
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
404
|
+
section.addEventListener('mrdNavigate', async (e) => {
|
|
405
|
+
if (generation !== _sectionGeneration) return;
|
|
406
|
+
await handleNavigate(e.detail);
|
|
407
|
+
});
|
|
408
|
+
section.addEventListener('mrdDownload', (e) => logEvent('mrdDownload', e.detail));
|
|
409
|
+
section.addEventListener('mrdSearch', (e) => logEvent('mrdSearch', e.detail));
|
|
350
410
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
logEvent('mrdAction', e.detail);
|
|
354
|
-
if (e.detail.action === 'create') loadForm();
|
|
355
|
-
});
|
|
411
|
+
container.appendChild(section);
|
|
412
|
+
}
|
|
356
413
|
|
|
357
|
-
// Store filters; mrdLoadPage will pick them up automatically
|
|
358
|
-
table.addEventListener('mrdFilter', (e) => {
|
|
359
|
-
logEvent('mrdFilter', e.detail);
|
|
360
|
-
_activeFilters = e.detail.filters;
|
|
361
|
-
});
|
|
362
414
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (result.ok) await table.setAggregations(result.body);
|
|
375
|
-
} catch (err) {
|
|
376
|
-
console.error('[mrdLoadAggregations] mislukt', err);
|
|
377
|
-
}
|
|
378
|
-
});
|
|
415
|
+
function toggleJsonViewer() {
|
|
416
|
+
const viewer = document.getElementById('json-viewer');
|
|
417
|
+
const btn = document.getElementById('btn-json-toggle');
|
|
418
|
+
const open = !viewer.classList.toggle('hidden');
|
|
419
|
+
btn.classList.toggle('active', open);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function updateBackButton() {
|
|
423
|
+
const btn = document.getElementById('btn-nav-back');
|
|
424
|
+
if (btn) btn.classList.toggle('hidden', _navHistory.length === 0);
|
|
425
|
+
}
|
|
379
426
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// Old current becomes an alternative; clicked alternative becomes current
|
|
392
|
-
const newAlts = [
|
|
393
|
-
{ name: _currentViewKey, label: viewLabel, class: layoutItem?.class ?? '' },
|
|
394
|
-
...alternativeViews.filter(av => av.name !== newKey),
|
|
395
|
-
];
|
|
396
|
-
|
|
397
|
-
_currentViewKey = newKey;
|
|
398
|
-
_tableDataHref = newHref.replace(/([?&])page=\d+/, '').replace(/\?$/, '');
|
|
399
|
-
viewLabel = newViewLabel;
|
|
400
|
-
alternativeViews = newAlts;
|
|
401
|
-
viewFilter = newView.filter ?? [];
|
|
402
|
-
_activeFilters = [];
|
|
427
|
+
function syncDashboardTypeUI(type, objectHref) {
|
|
428
|
+
_dashboardType = type;
|
|
429
|
+
document.getElementById('dashboard-type-select').value = type;
|
|
430
|
+
const isObject = type === 'object' || type === 'form';
|
|
431
|
+
document.getElementById('controls-class-group').classList.toggle('hidden', type !== 'class');
|
|
432
|
+
document.getElementById('controls-object-group').classList.toggle('hidden', !isObject);
|
|
433
|
+
if (isObject && objectHref) {
|
|
434
|
+
document.getElementById('object-href-input').value = objectHref;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
403
437
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
table.alternativeViews = alternativeViews;
|
|
416
|
-
|
|
417
|
-
await table.init();
|
|
418
|
-
await table.setPage(0, rows0);
|
|
419
|
-
} catch (err) {
|
|
420
|
-
console.error('[mrdSwitchView] laden mislukt', err);
|
|
421
|
-
}
|
|
422
|
-
});
|
|
438
|
+
function navigateBack() {
|
|
439
|
+
if (_navHistory.length === 0) return;
|
|
440
|
+
const prev = _navHistory.pop();
|
|
441
|
+
_dashboardData = prev.dashboardData;
|
|
442
|
+
_dashboardRecord = prev.dashboardRecord;
|
|
443
|
+
_activeLayoutIndex = prev.activeLayoutIndex;
|
|
444
|
+
syncDashboardTypeUI(prev.dashboardType ?? _dashboardType, prev.objectHref ?? '');
|
|
445
|
+
renderSectionTabs(_dashboardData.layouts ?? []);
|
|
446
|
+
renderSection(_activeLayoutIndex);
|
|
447
|
+
updateBackButton();
|
|
448
|
+
}
|
|
423
449
|
|
|
424
|
-
|
|
425
|
-
|
|
450
|
+
function pushHistory() {
|
|
451
|
+
_navHistory.push({
|
|
452
|
+
dashboardData: _dashboardData,
|
|
453
|
+
dashboardRecord: _dashboardRecord,
|
|
454
|
+
activeLayoutIndex: _activeLayoutIndex,
|
|
455
|
+
dashboardType: _dashboardType,
|
|
456
|
+
objectHref: document.getElementById('object-href-input').value,
|
|
426
457
|
});
|
|
427
458
|
}
|
|
428
459
|
|
|
460
|
+
async function navigateToObjectDashboard(selfHref) {
|
|
461
|
+
if (!selfHref) return;
|
|
462
|
+
|
|
463
|
+
document.getElementById('sections-container').innerHTML =
|
|
464
|
+
'<span class="spinner"></span> Object laden...';
|
|
465
|
+
document.getElementById('panel-sections').classList.remove('hidden');
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const recResp = await apiRequest('GET', selfHref, authGetToken());
|
|
469
|
+
if (!recResp.ok) throw new Error(`${recResp.status}: object ophalen mislukt`);
|
|
470
|
+
const record = recResp.body;
|
|
471
|
+
|
|
472
|
+
const dashHref = record._links?.metadata?.href;
|
|
473
|
+
if (!dashHref) throw new Error('Geen _links.metadata gevonden in object');
|
|
474
|
+
|
|
475
|
+
const dashResp = await apiRequest('GET', dashHref, authGetToken());
|
|
476
|
+
if (!dashResp.ok) throw new Error(`${dashResp.status}: dashboard ophalen mislukt`);
|
|
477
|
+
const dashboard = dashResp.body;
|
|
478
|
+
|
|
479
|
+
pushHistory();
|
|
480
|
+
_dashboardData = dashboard;
|
|
481
|
+
_dashboardRecord = record;
|
|
482
|
+
_activeLayoutIndex = 0;
|
|
483
|
+
|
|
484
|
+
const displayHref = selfHref.startsWith('http') ? selfHref : API_BASE + selfHref;
|
|
485
|
+
syncDashboardTypeUI('object', displayHref);
|
|
486
|
+
renderSectionTabs(dashboard.layouts ?? []);
|
|
487
|
+
renderSection(0);
|
|
488
|
+
updateBackButton();
|
|
489
|
+
} catch (err) {
|
|
490
|
+
document.getElementById('sections-container').innerHTML =
|
|
491
|
+
`<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function navigateToClassDashboard(pluralName) {
|
|
496
|
+
if (!_selectedTenant || !pluralName) return;
|
|
497
|
+
|
|
498
|
+
document.getElementById('sections-container').innerHTML =
|
|
499
|
+
'<span class="spinner"></span> Dashboard laden...';
|
|
500
|
+
document.getElementById('panel-sections').classList.remove('hidden');
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const dashboard = await apiFetchClassDashboard(authGetToken(), _selectedTenant, pluralName);
|
|
504
|
+
|
|
505
|
+
pushHistory();
|
|
506
|
+
_dashboardData = dashboard;
|
|
507
|
+
_dashboardRecord = null;
|
|
508
|
+
_activeLayoutIndex = 0;
|
|
509
|
+
|
|
510
|
+
syncDashboardTypeUI('class', '');
|
|
511
|
+
const sel = document.getElementById('type-select-compact');
|
|
512
|
+
if (sel.querySelector(`option[value="${pluralName}"]`)) sel.value = pluralName;
|
|
513
|
+
|
|
514
|
+
renderSectionTabs(dashboard.layouts ?? []);
|
|
515
|
+
renderSection(0);
|
|
516
|
+
updateBackButton();
|
|
517
|
+
} catch (err) {
|
|
518
|
+
document.getElementById('sections-container').innerHTML =
|
|
519
|
+
`<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function handleNavigate(detail) {
|
|
524
|
+
const { href, label, navigate } = detail;
|
|
525
|
+
if (href) {
|
|
526
|
+
logEvent('mrdNavigate', { href, label });
|
|
527
|
+
await navigateToObjectDashboard(href);
|
|
528
|
+
} else if (navigate?.dataClass) {
|
|
529
|
+
logEvent('mrdNavigate', { dataClass: navigate.dataClass, navigationType: navigate.navigationType, label });
|
|
530
|
+
await navigateToClassDashboard(navigate.dataClass);
|
|
531
|
+
} else {
|
|
532
|
+
logEvent('mrdNavigate', detail);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
429
536
|
/* =====================================================================
|
|
430
537
|
FORM — LOAD & RENDER
|
|
431
538
|
===================================================================== */
|
|
@@ -438,37 +545,29 @@ function renderTable(columns, totalElements, pageSize, page0Rows, dataHref, defa
|
|
|
438
545
|
async function loadRecord(row) {
|
|
439
546
|
const selfHref = row?._links?.self?.href;
|
|
440
547
|
if (!selfHref) { console.warn('[loadRecord] geen _links.self.href in rij', row); return; }
|
|
441
|
-
|
|
442
|
-
const sel = document.getElementById('type-select');
|
|
443
|
-
if (!sel.value) return;
|
|
548
|
+
if (!_selectedType) return;
|
|
444
549
|
|
|
445
550
|
const formPanel = document.getElementById('panel-form');
|
|
446
551
|
formPanel.classList.remove('hidden');
|
|
447
552
|
formPanel.innerHTML = '<span class="spinner"></span> Record laden...';
|
|
448
|
-
document.getElementById('panel-table').classList.add('hidden');
|
|
449
553
|
document.getElementById('panel-response').innerHTML = '';
|
|
450
554
|
|
|
451
555
|
try {
|
|
452
|
-
// Fetch the record first so we can read _links.form.href for the exact layout URL
|
|
453
556
|
const recordResp = await apiRequest('GET', selfHref, authGetToken());
|
|
454
557
|
if (!recordResp.ok) throw new Error(`${recordResp.status}: kon record niet ophalen`);
|
|
455
558
|
|
|
456
559
|
const formHref = recordResp.body?._links?.form?.href ?? null;
|
|
457
560
|
const layout = await apiFetchForm(authGetToken(), _selectedTenant, _selectedType.pluralName, formHref);
|
|
458
|
-
renderForm(layout, recordResp.body, selfHref);
|
|
561
|
+
await renderForm(layout, recordResp.body, selfHref);
|
|
459
562
|
} catch (err) {
|
|
460
563
|
formPanel.innerHTML = `<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
|
|
461
564
|
}
|
|
462
565
|
}
|
|
463
566
|
|
|
464
567
|
async function loadForm() {
|
|
465
|
-
const
|
|
466
|
-
const pluralName = sel.value;
|
|
568
|
+
const pluralName = _selectedType?.pluralName;
|
|
467
569
|
if (!pluralName) return;
|
|
468
570
|
|
|
469
|
-
const opt = sel.options[sel.selectedIndex];
|
|
470
|
-
_selectedType = { name: opt.dataset.name || pluralName, pluralName };
|
|
471
|
-
|
|
472
571
|
const formPanel = document.getElementById('panel-form');
|
|
473
572
|
formPanel.classList.remove('hidden');
|
|
474
573
|
formPanel.innerHTML = '<span class="spinner"></span> Formulier laden...';
|
|
@@ -476,7 +575,7 @@ async function loadForm() {
|
|
|
476
575
|
|
|
477
576
|
try {
|
|
478
577
|
const layout = await apiFetchForm(authGetToken(), _selectedTenant, pluralName);
|
|
479
|
-
renderForm(layout);
|
|
578
|
+
await renderForm(layout);
|
|
480
579
|
} catch (err) {
|
|
481
580
|
formPanel.innerHTML = `<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
|
|
482
581
|
}
|
|
@@ -489,21 +588,21 @@ async function loadForm() {
|
|
|
489
588
|
* @param record - existing record for edit mode (null = new record)
|
|
490
589
|
* @param selfHref - absolute URL for PATCH; null = POST (new record)
|
|
491
590
|
*/
|
|
492
|
-
function renderForm(layout, record = null, selfHref = null) {
|
|
591
|
+
async function renderForm(layout, record = null, selfHref = null) {
|
|
493
592
|
// Build relation metadata map keyed by both relatedClass and mostSignificantClass
|
|
494
593
|
// so lookups work regardless of which value arrives in events (mrdSearch uses mostSignificantClass).
|
|
495
594
|
_relationMeta = {};
|
|
496
595
|
function collectRelations(items) {
|
|
497
596
|
if (!Array.isArray(items)) return;
|
|
498
597
|
items.forEach(item => {
|
|
499
|
-
if (item.type === 'RELATION' && item.
|
|
598
|
+
if (item.type === 'RELATION' && item.relatedClass) {
|
|
500
599
|
const meta = {
|
|
501
|
-
name: item.
|
|
502
|
-
mostSignificantClass: item.
|
|
600
|
+
name: item.name,
|
|
601
|
+
mostSignificantClass: item.mostSignificantClass,
|
|
503
602
|
};
|
|
504
|
-
_relationMeta[item.
|
|
505
|
-
if (item.
|
|
506
|
-
_relationMeta[item.
|
|
603
|
+
_relationMeta[item.relatedClass] = meta;
|
|
604
|
+
if (item.mostSignificantClass) {
|
|
605
|
+
_relationMeta[item.mostSignificantClass] = meta;
|
|
507
606
|
}
|
|
508
607
|
}
|
|
509
608
|
if (Array.isArray(item.items)) collectRelations(item.items);
|
|
@@ -532,14 +631,21 @@ function renderForm(layout, record = null, selfHref = null) {
|
|
|
532
631
|
}
|
|
533
632
|
|
|
534
633
|
const formPanel = document.getElementById('panel-form');
|
|
535
|
-
formPanel.innerHTML = `<mrd-form id="live-form" locale="${_locale}"></mrd-form>`;
|
|
634
|
+
formPanel.innerHTML = `<mrd-form id="live-form" locale="${escHtml(_locale)}"></mrd-form>`;
|
|
536
635
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
636
|
+
const form = document.getElementById('live-form');
|
|
637
|
+
// Wait for Stencil to fully initialize this specific instance before setting props.
|
|
638
|
+
// componentOnReady() resolves after componentDidLoad — more reliable than customElements.whenDefined.
|
|
639
|
+
if (typeof form.componentOnReady === 'function') {
|
|
640
|
+
await form.componentOnReady();
|
|
641
|
+
} else {
|
|
642
|
+
await customElements.whenDefined('mrd-form');
|
|
643
|
+
}
|
|
541
644
|
|
|
542
|
-
|
|
645
|
+
form.layout = layout;
|
|
646
|
+
if (record) form.values = initialValues;
|
|
647
|
+
|
|
648
|
+
// Upload files immediately on selection; write binary URI back via setFieldValue
|
|
543
649
|
form.addEventListener('mrdChange', async (e) => {
|
|
544
650
|
const { name, value } = e.detail;
|
|
545
651
|
if (!(value instanceof File)) return;
|
|
@@ -645,7 +751,6 @@ function renderForm(layout, record = null, selfHref = null) {
|
|
|
645
751
|
console.error('[mrdFetchAll] failed:', err);
|
|
646
752
|
}
|
|
647
753
|
});
|
|
648
|
-
});
|
|
649
754
|
}
|
|
650
755
|
|
|
651
756
|
/* =====================================================================
|
|
@@ -695,15 +800,13 @@ customElements.whenDefined('mrd-form').then(() => {
|
|
|
695
800
|
|
|
696
801
|
document.getElementById('locale-select').addEventListener('change', (e) => {
|
|
697
802
|
_locale = e.target.value;
|
|
698
|
-
// Update embedded demo form
|
|
699
803
|
const demoForm = document.getElementById('demo-form');
|
|
700
804
|
if (demoForm) demoForm.locale = _locale;
|
|
701
|
-
// Update live table if present
|
|
702
|
-
const liveTable = document.getElementById('live-table');
|
|
703
|
-
if (liveTable) liveTable.locale = _locale;
|
|
704
|
-
// Update live form if present
|
|
705
805
|
const liveForm = document.getElementById('live-form');
|
|
706
806
|
if (liveForm) liveForm.locale = _locale;
|
|
807
|
+
// Update actieve section als aanwezig
|
|
808
|
+
const section = document.querySelector('#sections-container mrd-layout-section');
|
|
809
|
+
if (section) section.locale = _locale;
|
|
707
810
|
});
|
|
708
811
|
|
|
709
812
|
document.getElementById('btn-inject-results').addEventListener('click', () => {
|