@mmlogic/components 0.1.18 → 0.1.20

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 (30) hide show
  1. package/dist/cjs/{format-IFzg0q-6.js → format-DExY8_nu.js} +4 -0
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/mosterdcomponents.cjs.js +1 -1
  4. package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +3 -3
  5. package/dist/cjs/mrd-table.cjs.entry.js +90 -13
  6. package/dist/collection/components/mrd-form/mrd-form.js +2 -2
  7. package/dist/collection/components/mrd-table/mrd-table.js +149 -12
  8. package/dist/collection/components/mrd-table/mrd-table.scss +80 -0
  9. package/dist/collection/dev/api.js +8 -0
  10. package/dist/collection/dev/app.js +100 -42
  11. package/dist/collection/utils/i18n.js +4 -0
  12. package/dist/components/i18n.js +1 -1
  13. package/dist/components/mrd-form.js +1 -1
  14. package/dist/components/mrd-table.js +1 -1
  15. package/dist/esm/{format-Cc9kQ1j-.js → format-CcRjWvcb.js} +4 -0
  16. package/dist/esm/loader.js +1 -1
  17. package/dist/esm/mosterdcomponents.js +1 -1
  18. package/dist/esm/mrd-boolean-field_16.entry.js +3 -3
  19. package/dist/esm/mrd-table.entry.js +90 -13
  20. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  21. package/dist/mosterdcomponents/p-CcRjWvcb.js +1 -0
  22. package/dist/mosterdcomponents/p-c5b058e7.entry.js +1 -0
  23. package/dist/mosterdcomponents/p-fab1cac2.entry.js +1 -0
  24. package/dist/types/components/mrd-table/mrd-table.d.ts +17 -1
  25. package/dist/types/components.d.ts +28 -2
  26. package/dist/types/utils/cell-renderer.d.ts +9 -0
  27. package/package.json +1 -1
  28. package/dist/mosterdcomponents/p-05b585bb.entry.js +0 -1
  29. package/dist/mosterdcomponents/p-Cc9kQ1j-.js +0 -1
  30. package/dist/mosterdcomponents/p-a3d8feb8.entry.js +0 -1
@@ -18,6 +18,7 @@ export class MrdTable {
18
18
  this.pendingPages = new Set();
19
19
  this.debounceTimer = null;
20
20
  this.outsideClickHandler = null;
21
+ this.viewSwitcherClickHandler = null;
21
22
  this.keydownHandler = null;
22
23
  // ── Props ──────────────────────────────────────────────────────────────────
23
24
  this.columns = [];
@@ -37,6 +38,10 @@ export class MrdTable {
37
38
  this.defaultSort = '';
38
39
  /** Toolbar action buttons rendered above the table. */
39
40
  this.actions = [];
41
+ /** Display label of the current view — shown in the toolbar center as a view picker trigger. */
42
+ this.viewLabel = '';
43
+ /** Alternative views available for this table; renders a dropdown when non-empty. */
44
+ this.alternativeViews = [];
40
45
  // ── Internal state ─────────────────────────────────────────────────────────
41
46
  this.loadedPages = new Map();
42
47
  this.requestedPages = new Set();
@@ -61,6 +66,8 @@ export class MrdTable {
61
66
  this.textblockModal = null;
62
67
  /** Aggregation totals received from the host via setAggregations(). Null = not yet loaded. */
63
68
  this.aggregations = null;
69
+ /** Whether the view switcher dropdown is open. */
70
+ this.viewSwitcherOpen = false;
64
71
  this.handleScroll = (e) => {
65
72
  const scroller = e.currentTarget;
66
73
  const scrollTop = scroller.scrollTop;
@@ -143,6 +150,10 @@ export class MrdTable {
143
150
  document.removeEventListener('click', this.outsideClickHandler);
144
151
  this.outsideClickHandler = null;
145
152
  }
153
+ if (this.viewSwitcherClickHandler) {
154
+ document.removeEventListener('click', this.viewSwitcherClickHandler);
155
+ this.viewSwitcherClickHandler = null;
156
+ }
146
157
  if (this.keydownHandler) {
147
158
  document.removeEventListener('keydown', this.keydownHandler);
148
159
  this.keydownHandler = null;
@@ -338,7 +349,7 @@ export class MrdTable {
338
349
  // "YYYY-MM-DD" dates so the date inputs show what the user originally entered.
339
350
  // If from and to cover the same local day it was an exact-date filter — restore
340
351
  // to exact mode so the user sees the single-date input again.
341
- if (dataType === 'DATETIME' && existing) {
352
+ if (dataType === 'DATETIME' && existing && existing.operator !== 'isEmpty' && existing.operator !== 'isNotEmpty') {
342
353
  const display = Object.assign({}, existing);
343
354
  if (typeof display.from === 'string' && display.from)
344
355
  display.from = this.utcISOToLocalDate(display.from);
@@ -458,7 +469,7 @@ export class MrdTable {
458
469
  // Exact date → range covering the full local day (from = midnight, to = next midnight).
459
470
  // "to" is always the exclusive end (midnight of the next local day).
460
471
  let normalized = Object.assign({}, f);
461
- if (f.dataType === 'DATETIME') {
472
+ if (f.dataType === 'DATETIME' && f.operator !== 'isEmpty' && f.operator !== 'isNotEmpty') {
462
473
  if (typeof normalized.value === 'string' && normalized.value) {
463
474
  normalized.from = this.dateLocalToUTCStart(normalized.value);
464
475
  normalized.to = this.dateLocalToUTCEndExclusive(normalized.value);
@@ -513,12 +524,45 @@ export class MrdTable {
513
524
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
514
525
  }
515
526
  }
527
+ // ── View switcher ──────────────────────────────────────────────────────────
528
+ openViewSwitcher() {
529
+ this.viewSwitcherOpen = true;
530
+ if (this.viewSwitcherClickHandler)
531
+ document.removeEventListener('click', this.viewSwitcherClickHandler);
532
+ this.viewSwitcherClickHandler = (ev) => {
533
+ const wrapper = this.el.querySelector('.mrd-table__view-switcher');
534
+ if (wrapper && !wrapper.contains(ev.target))
535
+ this.closeViewSwitcher();
536
+ };
537
+ document.addEventListener('click', this.viewSwitcherClickHandler);
538
+ }
539
+ closeViewSwitcher() {
540
+ this.viewSwitcherOpen = false;
541
+ if (this.viewSwitcherClickHandler) {
542
+ document.removeEventListener('click', this.viewSwitcherClickHandler);
543
+ this.viewSwitcherClickHandler = null;
544
+ }
545
+ }
546
+ handleViewSwitch(view) {
547
+ this.closeViewSwitcher();
548
+ this.mrdSwitchView.emit({ name: view.name, class: view.class });
549
+ }
550
+ renderViewSwitcher() {
551
+ return (h("div", { class: "mrd-table__view-switcher" }, h("button", { class: `mrd-table__view-switcher-btn${this.viewSwitcherOpen ? ' mrd-table__view-switcher-btn--open' : ''}`, onClick: (e) => {
552
+ e.stopPropagation();
553
+ this.viewSwitcherOpen ? this.closeViewSwitcher() : this.openViewSwitcher();
554
+ } }, h("span", { class: "mrd-table__view-switcher-label" }, this.viewLabel), h("svg", { class: "mrd-table__view-switcher-chevron", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M7 10l5 5 5-5z" }))), this.viewSwitcherOpen && (h("div", { class: "mrd-table__view-switcher-dropdown", onClick: (e) => e.stopPropagation() }, this.alternativeViews.map(view => {
555
+ var _a;
556
+ return (h("button", { class: "mrd-table__view-switcher-item", onClick: () => this.handleViewSwitch(view) }, (_a = view.label) !== null && _a !== void 0 ? _a : view.name));
557
+ })))));
558
+ }
516
559
  // ── Render: toolbar ────────────────────────────────────────────────────────
517
560
  renderToolbar() {
518
- var _a;
561
+ var _a, _b;
519
562
  const filterCount = this.activeFilters.size;
520
563
  const hasActions = ((_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) > 0;
521
- return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" }, h("button", { class: `mrd-table__action mrd-table__action--secondary mrd-table__filter-toggle${this.filterMode ? ' mrd-table__filter-toggle--active' : ''}`, onClick: () => this.handleFilterToggle() }, h("svg", { class: "mrd-table__action-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" })), filterCount > 0 && h("span", { class: "mrd-table__filter-badge" }, filterCount), h("span", { class: "mrd-table__action-tooltip" }, this.filterMode ? t('table_filter_hide', this.locale) : t('table_filter', this.locale), filterCount > 0 ? ` (${filterCount} ${t('table_filter_active', this.locale)})` : '')), filterCount > 0 && (h("button", { class: "mrd-table__action mrd-table__action--secondary", onClick: () => this.clearAllFilters() }, h("svg", { class: "mrd-table__action-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" })), h("span", { class: "mrd-table__action-tooltip" }, t('table_filter_clear_all', this.locale))))), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, this.actions.map(a => {
564
+ const hasViewSwitcher = !!this.viewLabel && ((_b = this.alternativeViews) === null || _b === void 0 ? void 0 : _b.length) > 0;
565
+ return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" }, h("button", { class: `mrd-table__action mrd-table__action--secondary mrd-table__filter-toggle${this.filterMode ? ' mrd-table__filter-toggle--active' : ''}`, onClick: () => this.handleFilterToggle() }, h("svg", { class: "mrd-table__action-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" })), filterCount > 0 && h("span", { class: "mrd-table__filter-badge" }, filterCount), h("span", { class: "mrd-table__action-tooltip" }, this.filterMode ? t('table_filter_hide', this.locale) : t('table_filter', this.locale), filterCount > 0 ? ` (${filterCount} ${t('table_filter_active', this.locale)})` : '')), filterCount > 0 && (h("button", { class: "mrd-table__action mrd-table__action--secondary", onClick: () => this.clearAllFilters() }, h("svg", { class: "mrd-table__action-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" })), h("span", { class: "mrd-table__action-tooltip" }, t('table_filter_clear_all', this.locale))))), hasViewSwitcher && (h("div", { class: "mrd-table__toolbar-center" }, this.renderViewSwitcher())), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, this.actions.map(a => {
522
566
  var _a;
523
567
  return (h("button", { class: `mrd-table__action mrd-table__action--${(_a = a.variant) !== null && _a !== void 0 ? _a : 'secondary'}`, disabled: a.disabled, onClick: () => this.mrdAction.emit({ action: a.action }) }, a.icon
524
568
  ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
@@ -534,11 +578,13 @@ export class MrdTable {
534
578
  return h("p", { class: "mrd-table__filter-no-support" }, t('filter_no_support', this.locale));
535
579
  }
536
580
  if (dataType === 'BOOLEAN') {
581
+ const boolOp = pf.operator;
582
+ const noValueOp = boolOp === 'isEmpty' || boolOp === 'isNotEmpty';
537
583
  return (h("div", { class: "mrd-table__filter-radio-group" }, [
538
584
  { labelKey: 'filter_all', value: null },
539
585
  { labelKey: 'yes', value: true },
540
586
  { labelKey: 'no', value: false },
541
- ].map(opt => (h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: pf.value === opt.value, onChange: () => this.setPending('value', opt.value) }), t(opt.labelKey, this.locale))))));
587
+ ].map(opt => (h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: !noValueOp && pf.value === opt.value, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined, value: opt.value }); } }), t(opt.labelKey, this.locale)))), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: boolOp === 'isEmpty', onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: 'isEmpty', value: undefined }); } }), t('filter_is_empty', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: boolOp === 'isNotEmpty', onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: 'isNotEmpty', value: undefined }); } }), t('filter_is_not_empty', this.locale))));
542
588
  }
543
589
  if (dataType === 'LIST') {
544
590
  const items = (_c = (_b = col.field) === null || _b === void 0 ? void 0 : _b.listItems) !== null && _c !== void 0 ? _c : [];
@@ -556,17 +602,47 @@ export class MrdTable {
556
602
  ].map(o => h("option", { value: o.val, selected: op === o.val }, t(o.labelKey, this.locale)))), !noInput && (h("input", { type: "text", class: "mrd-table__filter-input", value: String((_f = pf.value) !== null && _f !== void 0 ? _f : ''), placeholder: t('filter_search_value', this.locale), onInput: (e) => this.setPending('value', e.target.value) }))));
557
603
  }
558
604
  if (NUMERIC_TYPES.has(dataType)) {
559
- const rangeMode = pf.from !== undefined || pf.to !== undefined;
560
- return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "number", class: "mrd-table__filter-input", value: pf.value != null ? String(pf.value) : '', onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("span", { class: "mrd-table__filter-range-sep" }, "\u2013"), h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
605
+ const numOp = pf.operator;
606
+ const noInput = numOp === 'isEmpty' || numOp === 'isNotEmpty';
607
+ const rangeMode = !noInput && (pf.from !== undefined || pf.to !== undefined);
608
+ return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => {
609
+ const val = e.target.value;
610
+ if (val === 'isEmpty' || val === 'isNotEmpty') {
611
+ this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: val, value: undefined, from: undefined, to: undefined });
612
+ }
613
+ else {
614
+ this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
615
+ }
616
+ } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: numOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: numOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "number", class: "mrd-table__filter-input", value: pf.value != null ? String(pf.value) : '', onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("span", { class: "mrd-table__filter-range-sep" }, "\u2013"), h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))))));
561
617
  }
562
618
  if (dataType === 'DATETIME') {
563
- const rangeMode = pf.from !== undefined || pf.to !== undefined;
564
- return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "date", class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range mrd-table__filter-range--stacked" }, h("label", { class: "mrd-table__filter-range-label" }, t('filter_from', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("label", { class: "mrd-table__filter-range-label" }, t('filter_to', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
619
+ const dtOp = pf.operator;
620
+ const noInput = dtOp === 'isEmpty' || dtOp === 'isNotEmpty';
621
+ const rangeMode = !noInput && (pf.from !== undefined || pf.to !== undefined);
622
+ return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => {
623
+ const val = e.target.value;
624
+ if (val === 'isEmpty' || val === 'isNotEmpty') {
625
+ this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: val, value: undefined, from: undefined, to: undefined });
626
+ }
627
+ else {
628
+ this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
629
+ }
630
+ } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: dtOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: dtOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "date", class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range mrd-table__filter-range--stacked" }, h("label", { class: "mrd-table__filter-range-label" }, t('filter_from', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("label", { class: "mrd-table__filter-range-label" }, t('filter_to', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))))));
565
631
  }
566
632
  if (DATE_TYPES.has(dataType)) {
567
633
  const inputType = dataType === 'DATE' ? 'date' : 'time';
568
- const rangeMode = pf.from !== undefined || pf.to !== undefined;
569
- return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_h = pf.value) !== null && _h !== void 0 ? _h : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
634
+ const dtdOp = pf.operator;
635
+ const noInput = dtdOp === 'isEmpty' || dtdOp === 'isNotEmpty';
636
+ const rangeMode = !noInput && (pf.from !== undefined || pf.to !== undefined);
637
+ return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => {
638
+ const val = e.target.value;
639
+ if (val === 'isEmpty' || val === 'isNotEmpty') {
640
+ this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: val, value: undefined, from: undefined, to: undefined });
641
+ }
642
+ else {
643
+ this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
644
+ }
645
+ } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: dtdOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: dtdOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_h = pf.value) !== null && _h !== void 0 ? _h : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))))));
570
646
  }
571
647
  return null;
572
648
  }
@@ -920,6 +996,51 @@ export class MrdTable {
920
996
  "getter": false,
921
997
  "setter": false,
922
998
  "defaultValue": "[]"
999
+ },
1000
+ "viewLabel": {
1001
+ "type": "string",
1002
+ "mutable": false,
1003
+ "complexType": {
1004
+ "original": "string",
1005
+ "resolved": "string",
1006
+ "references": {}
1007
+ },
1008
+ "required": false,
1009
+ "optional": false,
1010
+ "docs": {
1011
+ "tags": [],
1012
+ "text": "Display label of the current view \u2014 shown in the toolbar center as a view picker trigger."
1013
+ },
1014
+ "getter": false,
1015
+ "setter": false,
1016
+ "reflect": false,
1017
+ "attribute": "view-label",
1018
+ "defaultValue": "''"
1019
+ },
1020
+ "alternativeViews": {
1021
+ "type": "unknown",
1022
+ "mutable": false,
1023
+ "complexType": {
1024
+ "original": "AlternativeView[]",
1025
+ "resolved": "AlternativeView[]",
1026
+ "references": {
1027
+ "AlternativeView": {
1028
+ "location": "import",
1029
+ "path": "../../utils/cell-renderer",
1030
+ "id": "src/utils/cell-renderer.ts::AlternativeView",
1031
+ "referenceLocation": "AlternativeView"
1032
+ }
1033
+ }
1034
+ },
1035
+ "required": false,
1036
+ "optional": false,
1037
+ "docs": {
1038
+ "tags": [],
1039
+ "text": "Alternative views available for this table; renders a dropdown when non-empty."
1040
+ },
1041
+ "getter": false,
1042
+ "setter": false,
1043
+ "defaultValue": "[]"
923
1044
  }
924
1045
  };
925
1046
  }
@@ -939,7 +1060,8 @@ export class MrdTable {
939
1060
  "popupPos": {},
940
1061
  "scrollTop": {},
941
1062
  "textblockModal": {},
942
- "aggregations": {}
1063
+ "aggregations": {},
1064
+ "viewSwitcherOpen": {}
943
1065
  };
944
1066
  }
945
1067
  static get events() {
@@ -1030,6 +1152,21 @@ export class MrdTable {
1030
1152
  "resolved": "{ href: string; fileName: string; }",
1031
1153
  "references": {}
1032
1154
  }
1155
+ }, {
1156
+ "method": "mrdSwitchView",
1157
+ "name": "mrdSwitchView",
1158
+ "bubbles": true,
1159
+ "cancelable": true,
1160
+ "composed": true,
1161
+ "docs": {
1162
+ "tags": [],
1163
+ "text": "Fired when the user selects an alternative view from the view switcher dropdown."
1164
+ },
1165
+ "complexType": {
1166
+ "original": "{ name: string; class?: string }",
1167
+ "resolved": "{ name: string; class?: string | undefined; }",
1168
+ "references": {}
1169
+ }
1033
1170
  }, {
1034
1171
  "method": "mrdLoadAggregations",
1035
1172
  "name": "mrdLoadAggregations",
@@ -175,6 +175,86 @@
175
175
  align-items: center;
176
176
  }
177
177
 
178
+ .mrd-table__toolbar-center {
179
+ flex: 1;
180
+ display: flex;
181
+ justify-content: center;
182
+ align-items: center;
183
+ }
184
+
185
+ /* ── View switcher ───────────────────────────────────────────────────────── */
186
+ .mrd-table__view-switcher {
187
+ position: relative;
188
+ }
189
+
190
+ .mrd-table__view-switcher-btn {
191
+ display: inline-flex;
192
+ align-items: center;
193
+ gap: var(--mrd-space-1);
194
+ background: none;
195
+ border: none;
196
+ cursor: pointer;
197
+ padding: var(--mrd-space-1) var(--mrd-space-2);
198
+ color: var(--mrd-color-neutral-400);
199
+ font-size: var(--mrd-font-size-sm);
200
+ border-radius: var(--mrd-border-radius);
201
+ transition: color 0.15s, background-color 0.15s;
202
+ line-height: 1.4;
203
+ }
204
+
205
+ .mrd-table__view-switcher-btn:hover {
206
+ color: var(--mrd-color-neutral-700);
207
+ background-color: var(--mrd-color-neutral-50);
208
+ }
209
+
210
+ .mrd-table__view-switcher-label {
211
+ font-weight: var(--mrd-font-weight-medium);
212
+ }
213
+
214
+ .mrd-table__view-switcher-chevron {
215
+ width: 1.1rem;
216
+ height: 1.1rem;
217
+ flex-shrink: 0;
218
+ transition: transform 0.15s;
219
+ }
220
+
221
+ .mrd-table__view-switcher-btn--open .mrd-table__view-switcher-chevron {
222
+ transform: rotate(180deg);
223
+ }
224
+
225
+ .mrd-table__view-switcher-dropdown {
226
+ position: absolute;
227
+ top: calc(100% + 4px);
228
+ left: 50%;
229
+ transform: translateX(-50%);
230
+ min-width: 160px;
231
+ background: var(--mrd-color-white);
232
+ border: 1px solid var(--mrd-border-color);
233
+ border-radius: var(--mrd-border-radius);
234
+ box-shadow: var(--mrd-shadow-md, 0 4px 12px rgba(0,0,0,.12));
235
+ z-index: var(--mrd-z-dropdown, 200);
236
+ overflow: hidden;
237
+ }
238
+
239
+ .mrd-table__view-switcher-item {
240
+ display: block;
241
+ width: 100%;
242
+ padding: var(--mrd-space-2) var(--mrd-space-3);
243
+ background: none;
244
+ border: none;
245
+ text-align: left;
246
+ cursor: pointer;
247
+ font-size: var(--mrd-font-size-sm);
248
+ color: var(--mrd-color-neutral-700);
249
+ white-space: nowrap;
250
+ transition: background-color 0.1s, color 0.1s;
251
+ }
252
+
253
+ .mrd-table__view-switcher-item:hover {
254
+ background-color: var(--mrd-color-neutral-50);
255
+ color: var(--mrd-color-neutral-900);
256
+ }
257
+
178
258
  .mrd-table__action {
179
259
  position: relative;
180
260
  display: inline-flex;
@@ -51,6 +51,14 @@ async function apiFetchDashboard(token, tenantCode, pluralName) {
51
51
  return body; // { layouts, views, _links }
52
52
  }
53
53
 
54
+ function applyViewFilter(href, viewFilter) {
55
+ if (!viewFilter?.length) return href;
56
+ const params = new URLSearchParams();
57
+ viewFilter.forEach(f => { if (f.name && f.value != null) params.set(f.name, String(f.value)); });
58
+ const qs = params.toString();
59
+ return qs ? `${href}?${qs}` : href;
60
+ }
61
+
54
62
  async function apiFetchPage(token, baseHref, pageNumber, sort = '') {
55
63
  const sep = baseHref.includes('?') ? '&' : '?';
56
64
  let url = `${baseHref}${sep}page=${pageNumber}`;
@@ -7,6 +7,8 @@ let _selectedType = null; // { name, pluralName }
7
7
  let _relationMeta = {}; // relatedClass / mostSignificantClass → { name, mostSignificantClass }
8
8
  let _tableDataHref = null; // base href for the current table view (without ?page=)
9
9
  let _locale = navigator.language || 'nl-NL';
10
+ let _dashboard = null; // full dashboard response — used for view switching
11
+ let _currentViewKey = null; // key of the view currently displayed in the table
10
12
 
11
13
  /* =====================================================================
12
14
  UTILITIES
@@ -160,71 +162,91 @@ async function loadTable() {
160
162
 
161
163
  try {
162
164
  const dashboard = await apiFetchDashboard(authGetToken(), _selectedTenant, pluralName);
165
+ _dashboard = dashboard;
163
166
 
164
- const viewKey = dashboard.layouts?.[0]?.items?.[0]?.name;
165
- const view = viewKey && dashboard.views?.[viewKey];
166
- const dataHref = viewKey && dashboard._links?.[viewKey]?.href;
167
+ const layoutItem = dashboard.layouts?.[0]?.items?.[0];
168
+ const viewKey = layoutItem?.name;
169
+ const view = viewKey && dashboard.views?.[viewKey];
170
+ const dataHref = viewKey && dashboard._links?.[viewKey]?.href;
167
171
  if (!view || !dataHref) throw new Error('Geen geldige view gevonden in dashboard response.');
168
172
 
173
+ _currentViewKey = viewKey;
174
+
169
175
  // Strip any ?page= from href so we control pagination ourselves
170
176
  _tableDataHref = dataHref.replace(/([?&])page=\d+/, '').replace(/\?$/, '');
171
177
 
172
- const columns = (view.values ?? []).map(mapApiItem);
173
- const defaultSort = view.defaultSort ?? '';
178
+ const columns = (view.values ?? []).map(mapApiItem);
179
+ const defaultSort = view.defaultSort ?? '';
180
+ const viewLabel = layoutItem?.label ?? viewKey ?? '';
181
+ const alternativeViews = layoutItem?.alternativeViews ?? [];
182
+ const viewFilter = view.filter ?? [];
174
183
 
175
- // Fetch page 0 for totalElements + first rows (including defaultSort)
176
- const page0 = await apiFetchPage(authGetToken(), _tableDataHref, 0, defaultSort);
184
+ // Fetch page 0 for totalElements + first rows (including defaultSort + view filters)
185
+ const page0 = await apiFetchPage(authGetToken(), applyViewFilter(_tableDataHref, viewFilter), 0, defaultSort);
177
186
  const meta = page0.page ?? {};
178
187
  const total = meta.totalElements ?? 0;
179
188
  const pageSize = meta.size ?? 20;
180
189
  const embedded = page0._embedded ?? {};
181
190
  const rows0 = Object.values(embedded)[0] ?? [];
182
191
 
183
- renderTable(columns, total, pageSize, rows0, _tableDataHref, defaultSort);
192
+ renderTable(columns, total, pageSize, rows0, _tableDataHref, defaultSort, viewLabel, alternativeViews, viewFilter);
184
193
  } catch (err) {
185
194
  tablePanel.innerHTML = `<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
186
195
  }
187
196
  }
188
197
 
189
- function renderTable(columns, totalElements, pageSize, page0Rows, dataHref, defaultSort = '') {
198
+ function renderTable(columns, totalElements, pageSize, page0Rows, dataHref, defaultSort = '', initialViewLabel = '', initialAlternativeViews = [], initialViewFilter = []) {
190
199
  const tablePanel = document.getElementById('panel-table');
191
200
  tablePanel.innerHTML = '<mrd-table id="live-table"></mrd-table>';
192
201
 
193
202
  customElements.whenDefined('mrd-table').then(async () => {
194
203
  const table = document.getElementById('live-table');
195
204
 
196
- // Active filters stored here; combined with sort in every mrdLoadPage call
197
- let _activeFilters = [];
198
-
199
- table.columns = columns;
200
- table.locale = _locale;
201
- table.pageSize = pageSize;
202
- table.tableHeight = 500;
203
- table.totalElements = totalElements;
204
- table.defaultSort = defaultSort;
205
- table.actions = [
205
+ // Mutable closure state reassigned on view switch so subsequent handlers see new values
206
+ let _activeFilters = [];
207
+ let viewLabel = initialViewLabel;
208
+ let alternativeViews = initialAlternativeViews;
209
+ let viewFilter = initialViewFilter; // static filters from view definition
210
+
211
+ _tableDataHref = dataHref;
212
+
213
+ table.columns = columns;
214
+ table.locale = _locale;
215
+ table.pageSize = pageSize;
216
+ table.tableHeight = 500;
217
+ table.totalElements = totalElements;
218
+ table.defaultSort = defaultSort;
219
+ table.viewLabel = viewLabel;
220
+ table.alternativeViews = alternativeViews;
221
+ table.actions = [
206
222
  { action: 'create', label: 'Nieuw record', icon: 'dev/sprites.svg#icon-plus', variant: 'primary' },
207
223
  { action: 'export', label: 'Exporteer naar Excel', icon: 'dev/sprites.svg#icon-file-excel' },
208
224
  ];
209
225
 
226
+ function buildParams(sort) {
227
+ const params = new URLSearchParams();
228
+ if (sort) params.set('sort', sort);
229
+ // Static view-level filters (from view.filter in dashboard response)
230
+ viewFilter.forEach(f => { if (f.name && f.value != null) params.set(f.name, String(f.value)); });
231
+ // User-applied column filters
232
+ _activeFilters.forEach(f => {
233
+ if (f.operator === 'isEmpty') { params.set(f.field, ''); return; }
234
+ if (f.operator === 'isNotEmpty') { params.set(f.field + '_notempty', 'true'); return; }
235
+ if (f.values?.length) { params.set(f.field, f.values.join(',')); return; }
236
+ if (f.value != null && f.value !== '') params.set(f.field, String(f.value));
237
+ if (f.from != null && f.from !== '') params.set(f.field + '_from', String(f.from));
238
+ if (f.to != null && f.to !== '') params.set(f.field + '_to', String(f.to));
239
+ });
240
+ return params;
241
+ }
242
+
210
243
  // Register mrdLoadPage before init() so scroll events are caught immediately
211
244
  table.addEventListener('mrdLoadPage', async (e) => {
212
245
  const { page, sort } = e.detail;
213
246
  try {
214
- const params = new URLSearchParams();
215
- if (sort) params.set('sort', sort);
216
- // Apply active filters as simple ?field=value params (range = future API)
217
- _activeFilters.forEach(f => {
218
- if (f.operator === 'isEmpty') { params.set(f.field, ''); return; }
219
- if (f.operator === 'isNotEmpty') { params.set(f.field + '_notempty', 'true'); return; }
220
- if (f.values?.length) { params.set(f.field, f.values.join(',')); return; }
221
- if (f.value != null && f.value !== '') params.set(f.field, String(f.value));
222
- // Range: _from/_to suffix (future API support)
223
- if (f.from != null && f.from !== '') params.set(f.field + '_from', String(f.from));
224
- if (f.to != null && f.to !== '') params.set(f.field + '_to', String(f.to));
225
- });
247
+ const params = buildParams(sort);
226
248
  const qs = params.toString();
227
- const url = qs ? `${dataHref}?${qs}` : dataHref;
249
+ const url = qs ? `${_tableDataHref}?${qs}` : _tableDataHref;
228
250
 
229
251
  const result = await apiFetchPage(authGetToken(), url, page);
230
252
  const embedded = result._embedded ?? {};
@@ -256,19 +278,11 @@ function renderTable(columns, totalElements, pageSize, page0Rows, dataHref, defa
256
278
  table.addEventListener('mrdLoadAggregations', async (e) => {
257
279
  const { sum, avg, count } = e.detail;
258
280
  try {
259
- const aggUrl = dataHref.split('?')[0] + '/aggregations';
260
- const params = new URLSearchParams();
281
+ const aggUrl = _tableDataHref.split('?')[0] + '/aggregations';
282
+ const params = buildParams('');
261
283
  if (sum?.length) params.set('sum', sum.join(','));
262
284
  if (avg?.length) params.set('avg', avg.join(','));
263
285
  if (count?.length) params.set('count', count.join(','));
264
- _activeFilters.forEach(f => {
265
- if (f.operator === 'isEmpty') { params.set(f.field, ''); return; }
266
- if (f.operator === 'isNotEmpty') { params.set(f.field + '_notempty', 'true'); return; }
267
- if (f.values?.length) { params.set(f.field, f.values.join(',')); return; }
268
- if (f.value != null && f.value !== '') params.set(f.field, String(f.value));
269
- if (f.from != null && f.from !== '') params.set(f.field + '_from', String(f.from));
270
- if (f.to != null && f.to !== '') params.set(f.field + '_to', String(f.to));
271
- });
272
286
  const qs = params.toString();
273
287
  const result = await apiRequest('GET', qs ? `${aggUrl}?${qs}` : aggUrl, authGetToken());
274
288
  if (result.ok) await table.setAggregations(result.body);
@@ -277,6 +291,50 @@ function renderTable(columns, totalElements, pageSize, page0Rows, dataHref, defa
277
291
  }
278
292
  });
279
293
 
294
+ // Switch to an alternative view: swap current and clicked view
295
+ table.addEventListener('mrdSwitchView', async (e) => {
296
+ const newKey = e.detail.name;
297
+ const newView = _dashboard?.views?.[newKey];
298
+ const newHref = _dashboard?._links?.[newKey]?.href;
299
+ if (!newView || !newHref) { console.error('[mrdSwitchView] view niet gevonden:', newKey); return; }
300
+
301
+ const clickedAlt = alternativeViews.find(av => av.name === newKey);
302
+ const newViewLabel = clickedAlt?.label ?? newKey;
303
+ const layoutItem = _dashboard.layouts?.[0]?.items?.[0];
304
+
305
+ // Old current becomes an alternative; clicked alternative becomes current
306
+ const newAlts = [
307
+ { name: _currentViewKey, label: viewLabel, class: layoutItem?.class ?? '' },
308
+ ...alternativeViews.filter(av => av.name !== newKey),
309
+ ];
310
+
311
+ _currentViewKey = newKey;
312
+ _tableDataHref = newHref.replace(/([?&])page=\d+/, '').replace(/\?$/, '');
313
+ viewLabel = newViewLabel;
314
+ alternativeViews = newAlts;
315
+ viewFilter = newView.filter ?? [];
316
+ _activeFilters = [];
317
+
318
+ try {
319
+ const newDefaultSort = newView.defaultSort ?? '';
320
+ const page0 = await apiFetchPage(authGetToken(), applyViewFilter(_tableDataHref, viewFilter), 0, newDefaultSort);
321
+ const meta = page0.page ?? {};
322
+ const rows0 = Object.values(page0._embedded ?? {})[0] ?? [];
323
+
324
+ table.columns = (newView.values ?? []).map(mapApiItem);
325
+ table.defaultSort = newDefaultSort;
326
+ table.totalElements = meta.totalElements ?? 0;
327
+ table.pageSize = meta.size ?? 20;
328
+ table.viewLabel = viewLabel;
329
+ table.alternativeViews = alternativeViews;
330
+
331
+ await table.init();
332
+ await table.setPage(0, rows0);
333
+ } catch (err) {
334
+ console.error('[mrdSwitchView] laden mislukt', err);
335
+ }
336
+ });
337
+
280
338
  await table.init();
281
339
  await table.setPage(0, page0Rows); // inject pre-fetched page 0 — no extra request
282
340
  });
@@ -42,6 +42,7 @@ const translations = {
42
42
  filter_contains: 'Bevat',
43
43
  filter_starts_with: 'Begint met',
44
44
  filter_equals: 'Gelijk aan',
45
+ filter_has_value: 'Heeft waarde',
45
46
  filter_is_empty: 'Is leeg',
46
47
  filter_is_not_empty: 'Is niet leeg',
47
48
  filter_exact: 'Exact',
@@ -100,6 +101,7 @@ const translations = {
100
101
  filter_contains: 'Contains',
101
102
  filter_starts_with: 'Starts with',
102
103
  filter_equals: 'Equals',
104
+ filter_has_value: 'Has value',
103
105
  filter_is_empty: 'Is empty',
104
106
  filter_is_not_empty: 'Is not empty',
105
107
  filter_exact: 'Exact',
@@ -158,6 +160,7 @@ const translations = {
158
160
  filter_contains: 'يحتوي على',
159
161
  filter_starts_with: 'يبدأ بـ',
160
162
  filter_equals: 'يساوي',
163
+ filter_has_value: 'له قيمة',
161
164
  filter_is_empty: 'فارغ',
162
165
  filter_is_not_empty: 'ليس فارغاً',
163
166
  filter_exact: 'دقيق',
@@ -216,6 +219,7 @@ const translations = {
216
219
  filter_contains: 'Contient',
217
220
  filter_starts_with: 'Commence par',
218
221
  filter_equals: 'Égal à',
222
+ filter_has_value: 'A une valeur',
219
223
  filter_is_empty: 'Est vide',
220
224
  filter_is_not_empty: "N'est pas vide",
221
225
  filter_exact: 'Exact',