@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.
- package/dist/cjs/{format-IFzg0q-6.js → format-DExY8_nu.js} +4 -0
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/mosterdcomponents.cjs.js +1 -1
- package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +3 -3
- package/dist/cjs/mrd-table.cjs.entry.js +90 -13
- package/dist/collection/components/mrd-form/mrd-form.js +2 -2
- package/dist/collection/components/mrd-table/mrd-table.js +149 -12
- package/dist/collection/components/mrd-table/mrd-table.scss +80 -0
- package/dist/collection/dev/api.js +8 -0
- package/dist/collection/dev/app.js +100 -42
- package/dist/collection/utils/i18n.js +4 -0
- package/dist/components/i18n.js +1 -1
- package/dist/components/mrd-form.js +1 -1
- package/dist/components/mrd-table.js +1 -1
- package/dist/esm/{format-Cc9kQ1j-.js → format-CcRjWvcb.js} +4 -0
- package/dist/esm/loader.js +1 -1
- package/dist/esm/mosterdcomponents.js +1 -1
- package/dist/esm/mrd-boolean-field_16.entry.js +3 -3
- package/dist/esm/mrd-table.entry.js +90 -13
- package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
- package/dist/mosterdcomponents/p-CcRjWvcb.js +1 -0
- package/dist/mosterdcomponents/p-c5b058e7.entry.js +1 -0
- package/dist/mosterdcomponents/p-fab1cac2.entry.js +1 -0
- package/dist/types/components/mrd-table/mrd-table.d.ts +17 -1
- package/dist/types/components.d.ts +28 -2
- package/dist/types/utils/cell-renderer.d.ts +9 -0
- package/package.json +1 -1
- package/dist/mosterdcomponents/p-05b585bb.entry.js +0 -1
- package/dist/mosterdcomponents/p-Cc9kQ1j-.js +0 -1
- 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
|
-
|
|
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.
|
|
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
|
|
560
|
-
|
|
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
|
|
564
|
-
|
|
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
|
|
569
|
-
|
|
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
|
|
165
|
-
const
|
|
166
|
-
const
|
|
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
|
|
173
|
-
const 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
|
-
//
|
|
197
|
-
let _activeFilters
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
table.
|
|
205
|
-
table.
|
|
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 =
|
|
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 ? `${
|
|
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 =
|
|
260
|
-
const params =
|
|
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',
|