@mmlogic/components 0.5.1 → 0.5.3

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 (113) hide show
  1. package/dist/cjs/cell-renderer-7KX9Qbml.js +983 -0
  2. package/dist/cjs/{client-layout-RZ3Ix-4a.js → client-layout-CqVuJumR.js} +9 -0
  3. package/dist/cjs/{index-DzRfoAPz.js → index-DVbdkYO3.js} +468 -6
  4. package/dist/cjs/index.cjs.js +4 -1
  5. package/dist/cjs/loader.cjs.js +2 -2
  6. package/dist/cjs/mosterdcomponents.cjs.js +2 -2
  7. package/dist/cjs/{mrd-boolean-field_23.cjs.entry.js → mrd-boolean-field_25.cjs.entry.js} +3017 -951
  8. package/dist/cjs/mrd-merge-view.cjs.entry.js +273 -0
  9. package/dist/collection/collection-manifest.json +3 -0
  10. package/dist/collection/components/documents/mrd-document-view/mrd-document-view.js +7 -8
  11. package/dist/collection/components/email/mrd-email-view/mrd-email-view.css +126 -0
  12. package/dist/collection/components/email/mrd-email-view/mrd-email-view.js +247 -0
  13. package/dist/collection/components/fields/file-upload-common.js +23 -7
  14. package/dist/collection/components/fields/mrd-boolean-field/mrd-boolean-field.js +1 -1
  15. package/dist/collection/components/fields/mrd-currency-field/mrd-currency-field.js +1 -1
  16. package/dist/collection/components/fields/mrd-date-field/mrd-date-field.js +1 -1
  17. package/dist/collection/components/fields/mrd-datetime-field/mrd-datetime-field.js +1 -1
  18. package/dist/collection/components/fields/mrd-email-field/mrd-email-field.js +1 -1
  19. package/dist/collection/components/fields/mrd-file-field/mrd-file-field.css +20 -1
  20. package/dist/collection/components/fields/mrd-file-field/mrd-file-field.js +108 -47
  21. package/dist/collection/components/fields/mrd-image-field/mrd-image-field.css +44 -0
  22. package/dist/collection/components/fields/mrd-image-field/mrd-image-field.js +126 -58
  23. package/dist/collection/components/fields/mrd-longtext-field/mrd-longtext-field.js +1 -1
  24. package/dist/collection/components/fields/mrd-number-field/mrd-number-field.js +1 -1
  25. package/dist/collection/components/fields/mrd-secret-field/mrd-secret-field.js +2 -2
  26. package/dist/collection/components/fields/mrd-text-field/mrd-text-field.js +1 -1
  27. package/dist/collection/components/fields/mrd-textarea-field/mrd-textarea-field.js +9 -5
  28. package/dist/collection/components/fields/mrd-time-field/mrd-time-field.js +1 -1
  29. package/dist/collection/components/form/mrd-field/mrd-field.js +5 -5
  30. package/dist/collection/components/form/mrd-form/mrd-form.js +55 -6
  31. package/dist/collection/components/invoice/mrd-invoice-view/mrd-invoice-view.css +145 -0
  32. package/dist/collection/components/invoice/mrd-invoice-view/mrd-invoice-view.js +281 -0
  33. package/dist/collection/components/layout/mrd-layout-section/field-value.js +5 -1
  34. package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.css +10 -0
  35. package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.js +38 -8
  36. package/dist/collection/components/merge/mrd-merge-view/merge-diff.js +99 -0
  37. package/dist/collection/components/merge/mrd-merge-view/merge-parts.js +44 -0
  38. package/dist/collection/components/merge/mrd-merge-view/mrd-merge-view.css +244 -0
  39. package/dist/collection/components/merge/mrd-merge-view/mrd-merge-view.js +397 -0
  40. package/dist/collection/dev/api.js +41 -2
  41. package/dist/collection/dev/app.js +135 -17
  42. package/dist/collection/dev/example-data.js +126 -2
  43. package/dist/collection/types/client-layout.js +6 -0
  44. package/dist/collection/utils/cell-renderer.js +6 -1
  45. package/dist/collection/utils/document-attachments.js +27 -14
  46. package/dist/collection/utils/i18n.js +224 -0
  47. package/dist/components/cell-renderer.js +1 -0
  48. package/dist/components/client-layout.js +1 -1
  49. package/dist/components/document-attachments.js +1 -1
  50. package/dist/components/file-upload-common.js +1 -1
  51. package/dist/components/format.js +1 -1
  52. package/dist/components/i18n.js +1 -1
  53. package/dist/components/index.js +1 -1
  54. package/dist/components/mrd-boolean-field2.js +1 -1
  55. package/dist/components/mrd-currency-field2.js +1 -1
  56. package/dist/components/mrd-date-field2.js +1 -1
  57. package/dist/components/mrd-datetime-field2.js +1 -1
  58. package/dist/components/mrd-document-view2.js +1 -1
  59. package/dist/components/mrd-email-field2.js +1 -1
  60. package/dist/components/mrd-email-view.d.ts +11 -0
  61. package/dist/components/mrd-email-view.js +1 -0
  62. package/dist/components/mrd-email-view2.js +1 -0
  63. package/dist/components/mrd-field2.js +1 -1
  64. package/dist/components/mrd-file-field2.js +1 -1
  65. package/dist/components/mrd-form.js +1 -1
  66. package/dist/components/mrd-image-field2.js +1 -1
  67. package/dist/components/mrd-invoice-view.d.ts +11 -0
  68. package/dist/components/mrd-invoice-view.js +1 -0
  69. package/dist/components/mrd-invoice-view2.js +1 -0
  70. package/dist/components/mrd-layout-section.js +1 -1
  71. package/dist/components/mrd-longtext-field2.js +1 -1
  72. package/dist/components/mrd-merge-view.d.ts +11 -0
  73. package/dist/components/mrd-merge-view.js +1 -0
  74. package/dist/components/mrd-number-field2.js +1 -1
  75. package/dist/components/mrd-secret-field2.js +1 -1
  76. package/dist/components/mrd-table2.js +1 -1
  77. package/dist/components/mrd-text-field2.js +1 -1
  78. package/dist/components/mrd-textarea-field2.js +1 -1
  79. package/dist/components/mrd-time-field2.js +1 -1
  80. package/dist/components/purify.es.js +2 -0
  81. package/dist/esm/cell-renderer-DIsNX9D7.js +974 -0
  82. package/dist/esm/{client-layout-DCOAAH92.js → client-layout-Bc42CfJB.js} +7 -1
  83. package/dist/esm/{index-DiSe6vjv.js → index-CDiBvvJU.js} +468 -6
  84. package/dist/esm/index.js +1 -1
  85. package/dist/esm/loader.js +3 -3
  86. package/dist/esm/mosterdcomponents.js +3 -3
  87. package/dist/esm/{mrd-boolean-field_23.entry.js → mrd-boolean-field_25.entry.js} +2949 -885
  88. package/dist/esm/mrd-merge-view.entry.js +271 -0
  89. package/dist/mosterdcomponents/index.esm.js +1 -1
  90. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  91. package/dist/mosterdcomponents/p-4e061416.entry.js +1 -0
  92. package/dist/mosterdcomponents/p-910ebfee.entry.js +3 -0
  93. package/dist/mosterdcomponents/{p-DCOAAH92.js → p-Bc42CfJB.js} +1 -1
  94. package/dist/mosterdcomponents/p-CDiBvvJU.js +2 -0
  95. package/dist/mosterdcomponents/p-Ce2QDq0t.js +1 -0
  96. package/dist/types/components/documents/mrd-document-view/mrd-document-view.d.ts +2 -2
  97. package/dist/types/components/email/mrd-email-view/mrd-email-view.d.ts +52 -0
  98. package/dist/types/components/fields/file-upload-common.d.ts +19 -1
  99. package/dist/types/components/fields/mrd-file-field/mrd-file-field.d.ts +24 -10
  100. package/dist/types/components/fields/mrd-image-field/mrd-image-field.d.ts +27 -11
  101. package/dist/types/components/fields/mrd-textarea-field/mrd-textarea-field.d.ts +1 -0
  102. package/dist/types/components/form/mrd-form/mrd-form.d.ts +16 -1
  103. package/dist/types/components/invoice/mrd-invoice-view/mrd-invoice-view.d.ts +60 -0
  104. package/dist/types/components/layout/mrd-layout-section/mrd-layout-section.d.ts +13 -0
  105. package/dist/types/components/merge/mrd-merge-view/merge-diff.d.ts +43 -0
  106. package/dist/types/components/merge/mrd-merge-view/merge-parts.d.ts +37 -0
  107. package/dist/types/components/merge/mrd-merge-view/mrd-merge-view.d.ts +68 -0
  108. package/dist/types/components.d.ts +445 -19
  109. package/dist/types/types/client-layout.d.ts +6 -0
  110. package/dist/types/utils/document-attachments.d.ts +12 -5
  111. package/package.json +2 -1
  112. package/dist/mosterdcomponents/p-DiSe6vjv.js +0 -2
  113. package/dist/mosterdcomponents/p-cdecc698.entry.js +0 -1
@@ -10,7 +10,7 @@ let _locale = navigator.language || 'nl-NL';
10
10
  let _dashboardData = null; // full dashboard response
11
11
  let _dashboardRecord = null; // data-object voor object/form dashboards
12
12
  let _activeLayoutIndex = 0;
13
- let _dashboardType = 'class'; // 'class' | 'general' | 'navigation' | 'object' | 'form'
13
+ let _dashboardType = 'class'; // 'class' | 'general' | 'navigation' | 'object' | 'form' | 'merge'
14
14
  let _sectionGeneration = 0; // incremented on each renderSection(); prevents stale fetches
15
15
  let _navHistory = []; // stack van { dashboardData, dashboardRecord, activeLayoutIndex }
16
16
  let _meUser = null; // { id: href, label: name } — resolved once after login via /accounts/me
@@ -266,6 +266,41 @@ function initFilePreviewTab() {
266
266
  section.addEventListener('mrdDownload', (e) => logEvent('mrdDownload', e.detail));
267
267
  section.addEventListener('mrdNavigate', (e) => logEvent('mrdNavigate', e.detail));
268
268
  });
269
+
270
+ // Invoice View demo (commons.invoice archetype, TASK-0199) — the `lines`
271
+ // RELATED_VIEW is rendered inside mrd-invoice-view via slot="lines", but the
272
+ // page/sort data-loading stays owned by mrd-layout-section, exactly like any
273
+ // other RELATED_VIEW: answer mrdLoadViewPage the same way the Detail View tab
274
+ // does for appointments/share classes.
275
+ const invoiceSection = document.getElementById('invoice-view');
276
+ const invoicePreviewEl = document.getElementById('invoice-view-preview');
277
+ invoiceSection.items = window.EXAMPLE_INVOICE_ITEMS;
278
+ invoiceSection.data = window.EXAMPLE_INVOICE_DATA;
279
+ invoiceSection.archetypes = [window.EXAMPLE_INVOICE_ARCHETYPE];
280
+ invoiceSection.addEventListener('mrdLoadViewPage', async (e) => {
281
+ const { name, page } = e.detail;
282
+ logEvent('mrdLoadViewPage', e.detail);
283
+ await invoiceSection.setViewPage(name, page, window.EXAMPLE_INVOICE_LINES, window.EXAMPLE_INVOICE_LINES.length);
284
+ });
285
+ invoiceSection.addEventListener('mrdAttachments', (e) => {
286
+ logEvent('mrdAttachments', e.detail);
287
+ renderAttachmentPreview(invoicePreviewEl, e.detail.attachments);
288
+ });
289
+ invoiceSection.addEventListener('mrdDownload', (e) => logEvent('mrdDownload', e.detail));
290
+ invoiceSection.addEventListener('mrdNavigate', (e) => logEvent('mrdNavigate', e.detail));
291
+
292
+ // Email View demo (commons.email archetype, TASK-0199) — fully read-only, no
293
+ // RELATED_VIEW/table involved.
294
+ const emailSection = document.getElementById('email-view');
295
+ const emailPreviewEl = document.getElementById('email-view-preview');
296
+ emailSection.data = window.EXAMPLE_EMAIL_DATA;
297
+ emailSection.archetypes = [window.EXAMPLE_EMAIL_ARCHETYPE];
298
+ emailSection.addEventListener('mrdAttachments', (e) => {
299
+ logEvent('mrdAttachments', e.detail);
300
+ renderAttachmentPreview(emailPreviewEl, e.detail.attachments);
301
+ });
302
+ emailSection.addEventListener('mrdDownload', (e) => logEvent('mrdDownload', e.detail));
303
+ emailSection.addEventListener('mrdNavigate', (e) => logEvent('mrdNavigate', e.detail));
269
304
  }
270
305
 
271
306
  /* =====================================================================
@@ -337,7 +372,7 @@ async function loadCompactTypes(tenantCode) {
337
372
  function onDashboardTypeChange(type) {
338
373
  _dashboardType = type;
339
374
  const needsClass = type === 'class' || type === 'create';
340
- const needsObject = type === 'object' || type === 'form';
375
+ const needsObject = type === 'object' || type === 'form' || type === 'merge';
341
376
  document.getElementById('controls-class-group').classList.toggle('hidden', !needsClass);
342
377
  document.getElementById('controls-object-group').classList.toggle('hidden', !needsObject);
343
378
  if (needsClass && _selectedTenant) loadCompactTypes(_selectedTenant);
@@ -384,14 +419,17 @@ async function loadDashboard() {
384
419
  dashboard = await apiFetchNavigationPane(token, _selectedTenant, name);
385
420
 
386
421
  } else {
387
- // 'object' of 'form': fetch record → gebruik _links.metadata.href of _links.form.href
422
+ // 'object', 'form' of 'merge': fetch record → gebruik _links.metadata.href of _links.form.href
388
423
  const objectHref = document.getElementById('object-href-input').value.trim();
389
424
  if (!objectHref) throw new Error('Vul een Object URL in');
390
425
  const recResp = await apiRequest('GET', objectHref, token);
391
426
  if (!recResp.ok) throw new Error(`${recResp.status}: kon object niet ophalen`);
392
427
  recordData = recResp.body;
393
428
 
394
- const linkKey = _dashboardType === 'form' ? 'form' : 'metadata';
429
+ // 'merge' reuses the form layout too: it lists real editable fields
430
+ // (firstName, lastName, ...) and already excludes hard formula fields
431
+ // like `name` — see LayoutHelper.buildDefaultObjectFormDashboard().
432
+ const linkKey = (_dashboardType === 'form' || _dashboardType === 'merge') ? 'form' : 'metadata';
395
433
  const dashHref = recordData._links?.[linkKey]?.href;
396
434
  if (!dashHref) throw new Error(`Geen _links.${linkKey} gevonden in object`);
397
435
 
@@ -447,6 +485,11 @@ async function renderSection(index) {
447
485
  return;
448
486
  }
449
487
 
488
+ if (_dashboardType === 'merge') {
489
+ await renderMergeView(layout, _dashboardRecord, container);
490
+ return;
491
+ }
492
+
450
493
  const section = document.createElement('mrd-layout-section');
451
494
  section.items = layout.items;
452
495
  section.data = _dashboardRecord ?? { _links: _dashboardData._links ?? {} };
@@ -612,7 +655,7 @@ async function renderSection(index) {
612
655
  try {
613
656
  const result = await apiRequest('POST', `${_baseHref}${path}`, authGetToken(), values);
614
657
  if (result.ok) {
615
- const href = result.body?._links?.self?.href;
658
+ const href = result.location;
616
659
  if (href) await section.setViewCreatedObject(name, href);
617
660
  } else {
618
661
  console.error('[mrdCreateObject] mislukt', result.status, result.body);
@@ -865,17 +908,20 @@ async function renderForm(layout, record = null, selfHref = null, containerEl =
865
908
  if (_meUser) form.me = _meUser;
866
909
  form.values = record ? initialValues : {};
867
910
 
868
- // Upload files immediately on selection; write binary URI back via setFieldValue
869
- form.addEventListener('mrdChange', async (e) => {
870
- const { name, value } = e.detail;
871
- if (!(value instanceof File)) return;
872
- try {
873
- const uri = await apiUploadFile(authGetToken(), _selectedTenant, value);
874
- await form.setFieldValue(name, uri);
875
- } catch (err) {
876
- console.error(`[upload] mislukt voor veld "${name}":`, err);
877
- }
878
- });
911
+ // Upload each selected file immediately (fires once per file for `multiple` fields);
912
+ // pass `file` back into setFieldValue so mrd-form can resolve the right array entry.
913
+ // The resolved value is { href, fileName } same shape mrd-table/mrd-layout-section
914
+ // already expect when reading FILE/IMAGE data back — so the filename displays correctly
915
+ // instead of falling back to the upload URI's UUID.
916
+ form.addEventListener('mrdUpload', async (e) => {
917
+ const { name, file } = e.detail;
918
+ try {
919
+ const uri = await apiUploadFile(authGetToken(), _selectedTenant, file);
920
+ await form.setFieldValue(name, { href: uri, fileName: file.name }, file);
921
+ } catch (err) {
922
+ console.error(`[upload] mislukt voor veld "${name}":`, err);
923
+ }
924
+ });
879
925
 
880
926
  form.addEventListener('mrdSubmit', async (e) => {
881
927
  document.getElementById('panel-response').innerHTML =
@@ -914,7 +960,7 @@ async function renderForm(layout, record = null, selfHref = null, containerEl =
914
960
  renderResponse(200, 'Geen wijzigingen.');
915
961
  return;
916
962
  }
917
- const result = await apiRequest('PATCH', selfHref, authGetToken(), patch);
963
+ const result = await apiWriteAndFetch('PATCH', selfHref, authGetToken(), patch);
918
964
  renderResponse(result.status, result.body);
919
965
  } else {
920
966
  // POST mode: new record
@@ -973,6 +1019,78 @@ async function renderForm(layout, record = null, selfHref = null, containerEl =
973
1019
  });
974
1020
  }
975
1021
 
1022
+ /* =====================================================================
1023
+ MERGE — LOAD & RENDER (TASK-0177)
1024
+ ===================================================================== */
1025
+
1026
+ /** Flatten SECTION/GROUP → FIELD/RELATION, same tree shape mrd-form flattens internally. */
1027
+ function flattenLayoutItems(items) {
1028
+ const result = [];
1029
+ (items ?? []).forEach(item => {
1030
+ if (item.type === 'SECTION' || item.type === 'GROUP') {
1031
+ result.push(...flattenLayoutItems(item.items));
1032
+ } else if (item.type === 'FIELD' || item.type === 'RELATION') {
1033
+ result.push(item);
1034
+ }
1035
+ });
1036
+ return result;
1037
+ }
1038
+
1039
+ /**
1040
+ * Mount mrd-merge-view for the record loaded via the 'merge' dashboard type.
1041
+ * The record itself is object A (winner); the user searches for B inside
1042
+ * the component. mrd-merge-view never does network I/O itself — this wires
1043
+ * mrdSearch/mrdMerge to the real merge endpoint. Has no exit affordance of
1044
+ * its own — a host embedding this in an overlay page provides its own close
1045
+ * button, same as elsewhere in this dev preview.
1046
+ */
1047
+ async function renderMergeView(layout, record, containerEl) {
1048
+ const fields = flattenLayoutItems(layout.items);
1049
+ const selfHref = record?._links?.self?.href ?? '';
1050
+ // /data/{tenant}/{mostSignificantClass}/{id} → segment right before the id.
1051
+ const segments = selfHref.split('/').filter(Boolean);
1052
+ const mostSignificantClass = segments.length >= 2 ? segments[segments.length - 2] : '';
1053
+
1054
+ containerEl.innerHTML = `<mrd-merge-view id="live-merge" locale="${escHtml(_locale)}"></mrd-merge-view>`;
1055
+ const mergeEl = containerEl.querySelector('#live-merge');
1056
+ if (typeof mergeEl.componentOnReady === 'function') {
1057
+ await mergeEl.componentOnReady();
1058
+ } else {
1059
+ await customElements.whenDefined('mrd-merge-view');
1060
+ }
1061
+
1062
+ mergeEl.fields = fields;
1063
+ mergeEl.currentHref = selfHref;
1064
+ mergeEl.currentRecord = record ?? {};
1065
+ mergeEl.currentLabel = record?.name ?? '';
1066
+ mergeEl.basicType = mostSignificantClass;
1067
+ mergeEl.mostSignificantClass = mostSignificantClass;
1068
+
1069
+ mergeEl.addEventListener('mrdSearch', async (e) => {
1070
+ logEvent('mrdSearch (merge)', e.detail);
1071
+ const { query } = e.detail;
1072
+ try {
1073
+ const results = await apiSearchMergeCandidates(authGetToken(), _selectedTenant, mostSignificantClass, query);
1074
+ await mergeEl.setSearchResults(results);
1075
+ } catch (err) {
1076
+ console.error('[mrdSearch/merge] mislukt:', err);
1077
+ await mergeEl.setSearchResults([]);
1078
+ }
1079
+ });
1080
+
1081
+ mergeEl.addEventListener('mrdMerge', async (e) => {
1082
+ logEvent('mrdMerge', e.detail);
1083
+ const { winnerHref, loserHref, patch } = e.detail;
1084
+ try {
1085
+ const result = await apiMerge(authGetToken(), winnerHref, loserHref, patch);
1086
+ renderResponse(200, result);
1087
+ _dashboardRecord = result; // reflect the patched winner if the same layout is reloaded
1088
+ } catch (err) {
1089
+ renderResponse(500, err.message);
1090
+ }
1091
+ });
1092
+ }
1093
+
976
1094
  /* =====================================================================
977
1095
  RESPONSE RENDERING
978
1096
  ===================================================================== */
@@ -69,7 +69,7 @@ window.EXAMPLE_LAYOUT = {
69
69
  label: "Assets",
70
70
  items: [
71
71
  { type: "FIELD", name: "projectLogo", label: "Project Logo", dataType: "IMAGE", required: false, accept: "image/*", maxSize: 5242880 },
72
- { type: "FIELD", name: "attachments", label: "Attachments", dataType: "FILE", required: false, accept: ".pdf,.doc,.docx,.zip", maxSize: 10485760 }
72
+ { type: "FIELD", name: "attachments", label: "Attachments", dataType: "FILE", required: false, accept: ".pdf,.doc,.docx,.zip", maxSize: 10485760, multiple: true }
73
73
  ]
74
74
  },
75
75
  { type: "RELATION", name: "projectManager", label: "Project Manager", relatedClass: "Employee", displayType: "SEARCH", required: true, multiple: false },
@@ -91,7 +91,15 @@ window.EXAMPLE_VALUES = {
91
91
  budget: { amount: 150000, currency: "EUR" },
92
92
  isPublic: true,
93
93
  startDate: "2026-01-15",
94
- contactEmail: "team@mosterd.nl"
94
+ contactEmail: "team@mosterd.nl",
95
+ // FILE/IMAGE values from the API are { href, fileName } — same shape as everywhere
96
+ // else in this library (mrd-table, mrd-document-view) — so the field shows the real
97
+ // filename instead of the href's UUID.
98
+ projectLogo: { href: "/uploads/proj-9f2c/logo.png", fileName: "mosterd-logo.png" },
99
+ attachments: [
100
+ { href: "/uploads/proj-9f2c/9f2c-contract.pdf", fileName: "contract-2026.pdf" },
101
+ { href: "/uploads/proj-9f2c/1a3b-invoice.pdf", fileName: "invoice-042.pdf" },
102
+ ],
95
103
  };
96
104
 
97
105
  // ─── Companies detail view demo ────────────────────────────────────────────────
@@ -441,3 +449,119 @@ window.EXAMPLE_DOCUMENT_DATA = {
441
449
  dossier: { href: '/data/demo/dossiers/42', name: 'Acme Corp — Dossier 2026' },
442
450
  },
443
451
  };
452
+
453
+ // ─── Invoice View demo (commons.invoice archetype, TASK-0199) ────────────────
454
+ // Exercises mrd-invoice-view via mrd-layout-section's archetype path. `lines`
455
+ // is a real RELATED_VIEW item bound to the commons.invoiceLine archetype — it
456
+ // is rendered *inside* mrd-invoice-view via slot="lines" (mrd-layout-section
457
+ // still owns the table's own page/sort wiring, see renderInvoiceObject()).
458
+
459
+ window.EXAMPLE_INVOICE_ARCHETYPE = {
460
+ archetype: 'commons.invoice',
461
+ slots: {
462
+ invoiceNumber: 'invoiceNumber', invoiceDate: 'invoiceDate', dueDate: 'dueDate',
463
+ netAmount: 'netAmount', taxAmount: 'taxAmount', amount: 'amount', status: 'status',
464
+ customer: 'customer', supplier: 'supplier', lines: 'lines', attachments: 'attachments', container: 'dossier',
465
+ },
466
+ };
467
+
468
+ window.EXAMPLE_INVOICE_ITEMS = [
469
+ {
470
+ type: 'FIELD', name: 'status', label: 'Status', dataType: 'LIST', required: false,
471
+ listItems: [
472
+ { key: 'draft', label: 'Concept', color: '#6b7280', backgroundColor: '#f3f4f6' },
473
+ { key: 'sent', label: 'Verzonden', color: '#1d4ed8', backgroundColor: '#dbeafe' },
474
+ { key: 'paid', label: 'Betaald', color: '#15803d', backgroundColor: '#dcfce7' },
475
+ ],
476
+ },
477
+ {
478
+ type: 'RELATED_VIEW', label: 'Factuurregels', name: 'lines',
479
+ relatedClass: 'demo.invoiceLine', fromClass: 'demo.invoice', showTitle: true,
480
+ archetypes: [{ archetype: 'commons.invoiceLine', slots: { description: 'description', quantity: 'quantity', unitPrice: 'unitPrice', amount: 'amount' } }],
481
+ view: {
482
+ name: null,
483
+ defaultView: true,
484
+ values: [
485
+ { type: 'FIELD', label: 'Omschrijving', name: 'description', dataType: 'TEXT', required: true },
486
+ { type: 'FIELD', label: 'Aantal', name: 'quantity', dataType: 'DECIMAL', required: true },
487
+ { type: 'FIELD', label: 'Prijs', name: 'unitPrice', dataType: 'CURRENCY', required: true },
488
+ { type: 'FIELD', label: 'Bedrag', name: 'amount', dataType: 'CURRENCY', required: true, aggregate: 'sum' },
489
+ ],
490
+ type: 'demo.invoiceLine',
491
+ singularLabel: 'Factuurregel',
492
+ pluralLabel: 'Factuurregels',
493
+ },
494
+ },
495
+ ];
496
+
497
+ window.EXAMPLE_INVOICE_DATA = {
498
+ invoiceNumber: 'INV-2026-0142',
499
+ invoiceDate: '2026-06-01',
500
+ dueDate: '2026-06-30',
501
+ status: 'sent',
502
+ netAmount: { amount: 1000, currency: 'EUR' },
503
+ taxAmount: { amount: 210, currency: 'EUR' },
504
+ amount: { amount: 1210, currency: 'EUR' },
505
+ attachments: [
506
+ { href: '/data/demo/invoices/142/attachments/0', fileName: 'invoice-142.pdf', mimeType: 'application/pdf' },
507
+ ],
508
+ _links: {
509
+ self: { href: '/data/demo/invoices/142' },
510
+ customer: { href: '/data/demo/entities/acme', name: 'Acme Corp.' },
511
+ supplier: { href: '/data/demo/entities/mosterd', name: 'Mosterd B.V.' },
512
+ dossier: { href: '/data/demo/dossiers/42', name: 'Acme Corp — Dossier 2026' },
513
+ },
514
+ };
515
+
516
+ window.EXAMPLE_INVOICE_LINES = [
517
+ {
518
+ description: 'Consultancy — juni 2026',
519
+ quantity: 10,
520
+ unitPrice: { amount: 80, currency: 'EUR' },
521
+ amount: { amount: 800, currency: 'EUR' },
522
+ _links: { self: { href: '/data/demo/invoiceLines/1' } },
523
+ },
524
+ {
525
+ description: 'Projectmanagement',
526
+ quantity: 4,
527
+ unitPrice: { amount: 50, currency: 'EUR' },
528
+ amount: { amount: 200, currency: 'EUR' },
529
+ _links: { self: { href: '/data/demo/invoiceLines/2' } },
530
+ },
531
+ ];
532
+
533
+ // ─── Email View demo (commons.email archetype, TASK-0199) ────────────────────
534
+ // Exercises mrd-email-view via mrd-layout-section's archetype path. Fully
535
+ // read-only — no RELATED_VIEW/table involved.
536
+
537
+ window.EXAMPLE_EMAIL_ARCHETYPE = {
538
+ archetype: 'commons.email',
539
+ slots: {
540
+ subject: 'subject', date: 'date', from: 'from', to: 'to', cc: 'cc',
541
+ bodyHtml: 'bodyHtml', bodyText: 'bodyText', headers: 'headers',
542
+ attachments: 'attachments', container: 'dossier',
543
+ },
544
+ };
545
+
546
+ window.EXAMPLE_EMAIL_DATA = {
547
+ subject: 'RE: Contract 2026 — laatste versie bijgevoegd',
548
+ date: '2026-06-02T14:32:00',
549
+ bodyHtml: '<p>Beste Jan,</p><p>Bijgaand de laatste versie van het contract, inclusief de besproken wijzigingen in artikel 4.</p><p>Met vriendelijke groet,<br>Anne</p>',
550
+ headers: 'Message-ID: <abc123@mail.example.com>\nReceived: from mail.example.com by mx.mosterd.com',
551
+ attachments: [
552
+ { href: '/data/demo/emails/9/attachments/0', fileName: 'contract-2026-v3.pdf', mimeType: 'application/pdf' },
553
+ { href: '/data/demo/emails/9/attachments/1', fileName: 'wijzigingen.docx' },
554
+ ],
555
+ _links: {
556
+ from: { href: '/data/demo/entities/anne', name: 'Anne de Vries <anne@mosterd.com>' },
557
+ to: {
558
+ href: '/data/demo/entities/jan', name: 'Jan Bakker <jan@acme.com>',
559
+ values: [{ href: '/data/demo/entities/jan', name: 'Jan Bakker <jan@acme.com>' }],
560
+ },
561
+ cc: {
562
+ href: '/data/demo/entities/pietcc', name: 'Piet Claassen <piet@acme.com>',
563
+ values: [{ href: '/data/demo/entities/pietcc', name: 'Piet Claassen <piet@acme.com>' }],
564
+ },
565
+ dossier: { href: '/data/demo/dossiers/42', name: 'Acme Corp — Dossier 2026' },
566
+ },
567
+ };
@@ -72,6 +72,12 @@ export var ClientLayoutNavigateBehaviour;
72
72
  })(ClientLayoutNavigateBehaviour || (ClientLayoutNavigateBehaviour = {}));
73
73
  /** Well-known archetype ids the client renders specially. */
74
74
  export const ARCHETYPE_DOCUMENT = 'commons.document';
75
+ export const ARCHETYPE_INVOICE = 'commons.invoice';
76
+ export const ARCHETYPE_EMAIL = 'commons.email';
77
+ /** Bound by a `commons.invoice`'s `lines` relation target class. Used to find the
78
+ * RELATED_VIEW item for the line items generically, by archetype id rather than
79
+ * by name/class string. */
80
+ export const ARCHETYPE_INVOICE_LINE = 'commons.invoiceLine';
75
81
  /**
76
82
  * Find a binding for `archetypeId` in a published `archetypes` list.
77
83
  * Returns `undefined` when the archetype is absent — callers fall back to the
@@ -17,7 +17,12 @@ export class CellRenderer {
17
17
  const name = (_d = column.name) !== null && _d !== void 0 ? _d : '';
18
18
  const dataType = column.dataType;
19
19
  const listItems = column.listItems;
20
- const raw = row === null || row === void 0 ? void 0 : row[name];
20
+ let raw = row === null || row === void 0 ? void 0 : row[name];
21
+ // History-enabled fields are stored as { current, history: [...] } — render
22
+ // the current value, same as mrd-layout-section's field rendering.
23
+ if (column.historyEnabled && raw != null && typeof raw === 'object' && !Array.isArray(raw) && 'current' in raw) {
24
+ raw = raw.current;
25
+ }
21
26
  if (raw == null || raw === '')
22
27
  return '';
23
28
  const values = Array.isArray(raw) ? raw : [raw];
@@ -1,20 +1,33 @@
1
1
  /**
2
- * Extract the `commons.document` archetype's file slot from a data record as a
3
- * `DocumentAttachment[]` — empty when the slot is unmapped or has no value.
4
- * Shared by mrd-document-view (on load/data-change) and mrd-document-container
5
- * (row-click prefetch on mrdNavigate), so both report the exact same shape.
2
+ * Extract an archetype's file slot from a data record as a `DocumentAttachment[]` —
3
+ * empty when the slot is unmapped or has no value. `slot` defaults to `'file'` (the
4
+ * commons.document single-file slot); pass `'attachments'` for the multi-file slots
5
+ * on commons.email / commons.invoice. A single-object slot value yields a one-entry
6
+ * array, an array-valued slot yields one entry per item.
7
+ * Shared by mrd-document-view / mrd-invoice-view / mrd-email-view (on load/data-change)
8
+ * and mrd-document-container (row-click prefetch on mrdNavigate), so all report the
9
+ * exact same shape.
6
10
  */
7
- export function attachmentsFromRecord(archetype, record) {
8
- var _a, _b, _c, _d;
9
- const field = (_a = archetype === null || archetype === void 0 ? void 0 : archetype.slots) === null || _a === void 0 ? void 0 : _a['file'];
11
+ export function attachmentsFromRecord(archetype, record, slot = 'file') {
12
+ var _a;
13
+ const field = (_a = archetype === null || archetype === void 0 ? void 0 : archetype.slots) === null || _a === void 0 ? void 0 : _a[slot];
10
14
  if (!field)
11
15
  return [];
12
- const fileRaw = record === null || record === void 0 ? void 0 : record[field];
13
- if (!fileRaw)
16
+ const raw = record === null || record === void 0 ? void 0 : record[field];
17
+ if (!raw)
14
18
  return [];
15
- return [{
16
- href: fileRaw.href,
17
- fileName: (_b = fileRaw.fileName) !== null && _b !== void 0 ? _b : String(fileRaw),
18
- mimeType: (_d = (_c = fileRaw.mimeType) !== null && _c !== void 0 ? _c : fileRaw.contentType) !== null && _d !== void 0 ? _d : undefined,
19
- }];
19
+ const items = Array.isArray(raw) ? raw : [raw];
20
+ return items.filter(Boolean).map(item => {
21
+ var _a, _b, _c;
22
+ return ({
23
+ href: item.href,
24
+ fileName: (_a = item.fileName) !== null && _a !== void 0 ? _a : String(item),
25
+ mimeType: (_c = (_b = item.mimeType) !== null && _b !== void 0 ? _b : item.contentType) !== null && _c !== void 0 ? _c : undefined,
26
+ });
27
+ });
28
+ }
29
+ /** Dedup key for the componentDidLoad/@Watch('data') "emit mrdAttachments only on
30
+ * change" guard shared by mrd-document-view / mrd-invoice-view / mrd-email-view. */
31
+ export function attachmentsKey(attachments) {
32
+ return attachments.map(a => a.href).join('|');
20
33
  }