@mmlogic/components 0.5.12 → 0.5.14
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/{cell-renderer-pwUGEDql.js → cell-renderer-mjS630f1.js} +16 -0
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/mosterdcomponents.cjs.js +1 -1
- package/dist/cjs/mrd-boolean-field_25.cjs.entry.js +212 -45
- package/dist/cjs/mrd-merge-view.cjs.entry.js +1 -1
- package/dist/collection/components/documents/mrd-document-container/mrd-document-container.js +30 -4
- package/dist/collection/components/documents/mrd-document-list/mrd-document-list.js +55 -6
- package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.js +27 -3
- package/dist/collection/components/table/mrd-table/mrd-table.css +36 -0
- package/dist/collection/components/table/mrd-table/mrd-table.js +162 -26
- package/dist/collection/components/table/mrd-table/table-parts.js +14 -6
- package/dist/collection/components/table/mrd-table/table-query.js +6 -0
- package/dist/collection/dev/app.js +24 -0
- package/dist/collection/utils/i18n.js +16 -0
- package/dist/components/i18n.js +1 -1
- package/dist/components/mrd-document-container2.js +1 -1
- package/dist/components/mrd-document-list2.js +1 -1
- package/dist/components/mrd-layout-section.js +1 -1
- package/dist/components/mrd-table2.js +1 -1
- package/dist/esm/{cell-renderer-B8rJpnq8.js → cell-renderer-BDY8UXkH.js} +16 -0
- package/dist/esm/loader.js +1 -1
- package/dist/esm/mosterdcomponents.js +1 -1
- package/dist/esm/mrd-boolean-field_25.entry.js +212 -45
- package/dist/esm/mrd-merge-view.entry.js +1 -1
- package/dist/mosterdcomponents/cell-renderer-B8u5BCK-.js.map +1 -0
- package/dist/mosterdcomponents/client-layout-Bc42CfJB.js.map +1 -0
- package/dist/mosterdcomponents/document-attachments-BufPmMRv.js.map +1 -0
- package/dist/mosterdcomponents/field-helpers-CNQcI3Hf.js.map +1 -0
- package/dist/mosterdcomponents/file-upload-common-C9wp47CB.js.map +1 -0
- package/dist/mosterdcomponents/format-gcecItcD.js.map +1 -0
- package/dist/mosterdcomponents/i18n-ydNM87-Q.js.map +1 -0
- package/dist/mosterdcomponents/index-1ayBojyN.js.map +1 -0
- package/dist/mosterdcomponents/index-DhffneUF.js.map +1 -0
- package/dist/mosterdcomponents/index.esm.js.map +1 -0
- package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
- package/dist/mosterdcomponents/mosterdcomponents.esm.js.map +1 -0
- package/dist/mosterdcomponents/p-Dkm4WxMy.js +1 -0
- package/dist/mosterdcomponents/p-c834951a.entry.js +3 -0
- package/dist/mosterdcomponents/{p-e540de6a.entry.js → p-e8b0880f.entry.js} +1 -1
- package/dist/mosterdcomponents/purify.es-_duHfPgC.js.map +1 -0
- package/dist/mosterdcomponents/query-params-BZbQoeYF.js.map +1 -0
- package/dist/mosterdcomponents/quill-C9pgw_k-.js.map +1 -0
- package/dist/mosterdcomponents/validation-CjjTt66G.js.map +1 -0
- package/dist/types/components/documents/mrd-document-container/mrd-document-container.d.ts +4 -0
- package/dist/types/components/documents/mrd-document-list/mrd-document-list.d.ts +6 -1
- package/dist/types/components/layout/mrd-layout-section/mrd-layout-section.d.ts +4 -0
- package/dist/types/components/table/mrd-table/mrd-table.d.ts +52 -1
- package/dist/types/components/table/mrd-table/table-parts.d.ts +18 -0
- package/dist/types/components/table/mrd-table/table-query.d.ts +6 -0
- package/dist/types/components.d.ts +45 -1
- package/package.json +1 -1
- package/dist/mosterdcomponents/p-eb322d5c.entry.js +0 -3
- package/dist/mosterdcomponents/p-rTM1tU_M.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { h, Host, writeTask } from "@stencil/core";
|
|
2
2
|
import { t } from "../../../utils/i18n";
|
|
3
3
|
import { buildListPath } from "../../../utils/query-params";
|
|
4
|
-
import { TEXT_TYPES, NON_INTERACTIVE_TYPES, parseDefaultSort, buildSortParam, buildTableQueryParams, buildAggregationQs, filterHasValue, normalizeDatetimeFilter, datetimeFilterToDisplay, } from "./table-query";
|
|
4
|
+
import { TEXT_TYPES, NON_INTERACTIVE_TYPES, SEARCH_DEBOUNCE_MS, parseDefaultSort, buildSortParam, buildTableQueryParams, buildAggregationQs, filterHasValue, normalizeDatetimeFilter, datetimeFilterToDisplay, } from "./table-query";
|
|
5
5
|
import { FilterIcon, TableCell, TotalsRow, TextblockModal, JsonModal, Toolbar } from "./table-parts";
|
|
6
6
|
import { FilterPopup } from "./table-filter-popup";
|
|
7
7
|
import { REQUEST_DEBOUNCE_MS, REQUEST_TIMEOUT_MS, MAX_RETRY_ATTEMPTS, visibleCount, computeWindow, pageRange, getRowFromPages, computeEffectiveTotal, isTotalKnownFromPages, getStalePages, } from "./virtual-scroll";
|
|
@@ -27,6 +27,17 @@ export class MrdTable {
|
|
|
27
27
|
this.keydownHandler = null;
|
|
28
28
|
this.createPickerClickHandler = null;
|
|
29
29
|
this.viewPopoverClickHandler = null;
|
|
30
|
+
/** Debounce timer for the free-text search box — separate from debounceTimer
|
|
31
|
+
* (pending-page requests), since typing and scrolling debounce independently. */
|
|
32
|
+
this.searchDebounceTimer = null;
|
|
33
|
+
/** The rendered search <input>, captured so it can be focused the instant it mounts. */
|
|
34
|
+
this.searchInputEl = null;
|
|
35
|
+
/** Set by toggleSearch() when the box just expanded; consumed and cleared in componentDidRender(). */
|
|
36
|
+
this.searchNeedsFocus = false;
|
|
37
|
+
/** Column filters stashed by stashFiltersForSearch() while the search box is open — the
|
|
38
|
+
* backend can't combine `q` with column filters. Restored by restoreFiltersAfterSearch()
|
|
39
|
+
* on close, unless the user applied a fresh filter of their own in the meantime. */
|
|
40
|
+
this.stashedFilters = null;
|
|
30
41
|
/** Watches the host for gaining a box (e.g. its tab pane being shown) — see componentDidLoad. */
|
|
31
42
|
this.resizeObserver = null;
|
|
32
43
|
/** Guards against scheduling more than one column-width measurement per freeze. */
|
|
@@ -61,6 +72,10 @@ export class MrdTable {
|
|
|
61
72
|
/** Time (ms) to wait for setPage() before retrying a page request; after
|
|
62
73
|
* MAX_RETRY_ATTEMPTS retries the page is marked failed (see failedPages). */
|
|
63
74
|
this.requestTimeoutMs = REQUEST_TIMEOUT_MS;
|
|
75
|
+
/** When true, the toolbar's create ("+") action is disabled (with a tooltip
|
|
76
|
+
* explaining why) so a viewer-role user can see at a glance that this dashboard
|
|
77
|
+
* is read-only. Sorting, filtering and export stay available — those don't mutate data. */
|
|
78
|
+
this.readOnly = false;
|
|
64
79
|
// ── Internal state ─────────────────────────────────────────────────────────
|
|
65
80
|
/** Index into allViews[] for the currently displayed view. 0 = primary, 1+ = alternatives. */
|
|
66
81
|
this.activeViewIdx = 0;
|
|
@@ -77,6 +92,10 @@ export class MrdTable {
|
|
|
77
92
|
this.sortDir = 'asc';
|
|
78
93
|
/** Active filters keyed by field name. */
|
|
79
94
|
this.activeFilters = new Map();
|
|
95
|
+
/** True while the toolbar's magnifying glass is expanded into the search input. */
|
|
96
|
+
this.searchOpen = false;
|
|
97
|
+
/** Current free-text search query — sent as `q` on every mrdLoadPage/aggregations request. */
|
|
98
|
+
this.searchQuery = '';
|
|
80
99
|
/** Field name of the currently open filter popup (null = closed). */
|
|
81
100
|
this.openFilterCol = null;
|
|
82
101
|
/** Filter state being edited in the open popup. */
|
|
@@ -130,6 +149,11 @@ export class MrdTable {
|
|
|
130
149
|
this.colWidthMeasureScheduled = false;
|
|
131
150
|
this.colWidthsUserSet = false;
|
|
132
151
|
this.fixedContentMeasured = false;
|
|
152
|
+
// A new item is a different table (possibly not even a VIEW any more) — a leftover
|
|
153
|
+
// query from the old one should not silently keep filtering it.
|
|
154
|
+
this.searchOpen = false;
|
|
155
|
+
this.searchQuery = '';
|
|
156
|
+
this.stashedFilters = null;
|
|
133
157
|
}
|
|
134
158
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
135
159
|
componentWillLoad() {
|
|
@@ -237,7 +261,12 @@ export class MrdTable {
|
|
|
237
261
|
if (pageNumber === 0 && this.aggregationsPending) {
|
|
238
262
|
this.aggregationsPending = false;
|
|
239
263
|
const hasAggColumns = this.columns.some(c => c.type === 'FIELD' && c.aggregate);
|
|
240
|
-
|
|
264
|
+
// The aggregations endpoint doesn't support the `q` free-text param, so a request
|
|
265
|
+
// sent while searching would silently return unfiltered totals. Leave aggregations
|
|
266
|
+
// (and aggregationsTotal) at null instead — TotalsRow already renders nothing when
|
|
267
|
+
// aggregations is null, so the totals row just disappears for the duration of the
|
|
268
|
+
// search rather than showing numbers that don't match the filtered rows.
|
|
269
|
+
if (!this.searchQuery && (!isLastPage || hasAggColumns)) {
|
|
241
270
|
this.emitLoadAggregations();
|
|
242
271
|
}
|
|
243
272
|
}
|
|
@@ -276,10 +305,18 @@ export class MrdTable {
|
|
|
276
305
|
document.removeEventListener('click', this.viewPopoverClickHandler);
|
|
277
306
|
this.viewPopoverClickHandler = null;
|
|
278
307
|
}
|
|
308
|
+
if (this.searchDebounceTimer !== null) {
|
|
309
|
+
clearTimeout(this.searchDebounceTimer);
|
|
310
|
+
this.searchDebounceTimer = null;
|
|
311
|
+
}
|
|
279
312
|
}
|
|
280
313
|
componentDidRender() {
|
|
281
314
|
this.freezeColWidths();
|
|
282
315
|
this.growToFixedContent();
|
|
316
|
+
if (this.searchNeedsFocus && this.searchInputEl) {
|
|
317
|
+
this.searchInputEl.focus();
|
|
318
|
+
this.searchNeedsFocus = false;
|
|
319
|
+
}
|
|
283
320
|
}
|
|
284
321
|
/** Freeze the column widths once the first data page has rendered.
|
|
285
322
|
*
|
|
@@ -598,7 +635,13 @@ export class MrdTable {
|
|
|
598
635
|
dataClass: v.dataClass,
|
|
599
636
|
});
|
|
600
637
|
}
|
|
601
|
-
/** Build query params for a page request from current sort, view filters, filterClass and active column filters.
|
|
638
|
+
/** Build query params for a page request from current sort, view filters, filterClass and active column filters.
|
|
639
|
+
*
|
|
640
|
+
* `q` and column filters are mutually exclusive on the backend — combining them breaks
|
|
641
|
+
* the search. stashFiltersForSearch() already empties activeFilters for the duration of
|
|
642
|
+
* a search, but this guard is the actual guarantee: even if a filter is applied through
|
|
643
|
+
* a column header while the search box is still open (before the query is cleared),
|
|
644
|
+
* activeFilters is dropped from the request rather than sent alongside `q`. */
|
|
602
645
|
buildQueryParams(page) {
|
|
603
646
|
var _a;
|
|
604
647
|
const v = this.allViews[this.activeViewIdx];
|
|
@@ -607,7 +650,8 @@ export class MrdTable {
|
|
|
607
650
|
sort: this.sortParam(),
|
|
608
651
|
filterClass: v === null || v === void 0 ? void 0 : v.filterClass,
|
|
609
652
|
viewFilters: (_a = v === null || v === void 0 ? void 0 : v.view) === null || _a === void 0 ? void 0 : _a.filter,
|
|
610
|
-
activeFilters: this.activeFilters.values(),
|
|
653
|
+
activeFilters: this.searchQuery ? [] : this.activeFilters.values(),
|
|
654
|
+
query: this.searchQuery,
|
|
611
655
|
});
|
|
612
656
|
}
|
|
613
657
|
/** True when the table is in paginated / virtual-scroll mode rather than driven by the
|
|
@@ -630,7 +674,7 @@ export class MrdTable {
|
|
|
630
674
|
const raw = (_b = (_a = this.item) === null || _a === void 0 ? void 0 : _a.actions) !== null && _b !== void 0 ? _b : [];
|
|
631
675
|
return (raw !== null && raw !== void 0 ? raw : []).reduce((acc, a) => {
|
|
632
676
|
if (a === 'NEW')
|
|
633
|
-
acc.push({ action: 'create', label: t('table_new_record', this.locale), icon: 'assets/sprites.svg#icon-plus', variant: 'primary' });
|
|
677
|
+
acc.push({ action: 'create', label: t('table_new_record', this.locale), icon: 'assets/sprites.svg#icon-plus', variant: 'primary', disabled: this.readOnly });
|
|
634
678
|
if (a === 'EXPORT')
|
|
635
679
|
acc.push({ action: 'export', label: t('table_export_excel', this.locale), icon: 'assets/sprites.svg#icon-file-excel' });
|
|
636
680
|
return acc;
|
|
@@ -913,6 +957,18 @@ export class MrdTable {
|
|
|
913
957
|
const current = (_b = (_a = this.pendingFilter) === null || _a === void 0 ? void 0 : _a.values) !== null && _b !== void 0 ? _b : [];
|
|
914
958
|
this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { values: checked ? [...current, key] : current.filter(k => k !== key) });
|
|
915
959
|
}
|
|
960
|
+
/** Common reload after activeFilters or searchQuery changed: drop stale aggregations
|
|
961
|
+
* (the next setPage(0) re-requests them, guarded against `q` in setPage() itself) and
|
|
962
|
+
* re-fetch page 0. Shared by the filter popup, clear-all, and free-text search. */
|
|
963
|
+
reloadAfterQueryChange() {
|
|
964
|
+
this.aggregations = null;
|
|
965
|
+
this.aggregationsTotal = null;
|
|
966
|
+
this.aggregationsPending = true;
|
|
967
|
+
if (this.isPaginated) {
|
|
968
|
+
this.resetPages();
|
|
969
|
+
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
916
972
|
applyFilter() {
|
|
917
973
|
const f = this.pendingFilter;
|
|
918
974
|
if (!(f === null || f === void 0 ? void 0 : f.field)) {
|
|
@@ -929,13 +985,7 @@ export class MrdTable {
|
|
|
929
985
|
}
|
|
930
986
|
this.activeFilters = next;
|
|
931
987
|
this.closeFilterPopup();
|
|
932
|
-
this.
|
|
933
|
-
this.aggregationsTotal = null;
|
|
934
|
-
this.aggregationsPending = true;
|
|
935
|
-
if (this.isPaginated) {
|
|
936
|
-
this.resetPages();
|
|
937
|
-
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
938
|
-
}
|
|
988
|
+
this.reloadAfterQueryChange();
|
|
939
989
|
}
|
|
940
990
|
clearFilter() {
|
|
941
991
|
const name = this.openFilterCol;
|
|
@@ -944,23 +994,80 @@ export class MrdTable {
|
|
|
944
994
|
next.delete(name);
|
|
945
995
|
this.activeFilters = next;
|
|
946
996
|
this.closeFilterPopup();
|
|
947
|
-
this.
|
|
948
|
-
this.aggregationsTotal = null;
|
|
949
|
-
this.aggregationsPending = true;
|
|
950
|
-
if (this.isPaginated) {
|
|
951
|
-
this.resetPages();
|
|
952
|
-
this.emitPagesForWindow(this.renderStart, this.renderEnd);
|
|
953
|
-
}
|
|
997
|
+
this.reloadAfterQueryChange();
|
|
954
998
|
}
|
|
955
999
|
clearAllFilters() {
|
|
956
1000
|
this.activeFilters = new Map();
|
|
957
|
-
this.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1001
|
+
this.reloadAfterQueryChange();
|
|
1002
|
+
}
|
|
1003
|
+
// ── Free-text search ───────────────────────────────────────────────────────
|
|
1004
|
+
/** Search is a VIEW-only affordance — a RELATED_VIEW table is already scoped to
|
|
1005
|
+
* its parent record, so a free-text box there would invite confusion about what
|
|
1006
|
+
* it searches across. */
|
|
1007
|
+
get searchEnabled() {
|
|
1008
|
+
var _a;
|
|
1009
|
+
return ((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'VIEW';
|
|
1010
|
+
}
|
|
1011
|
+
toggleSearch() {
|
|
1012
|
+
if (this.searchOpen) {
|
|
1013
|
+
this.closeSearch();
|
|
963
1014
|
}
|
|
1015
|
+
else {
|
|
1016
|
+
this.searchOpen = true;
|
|
1017
|
+
this.searchNeedsFocus = true;
|
|
1018
|
+
this.stashFiltersForSearch();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
/** Collapses the box back to the icon. Clearing a non-empty query and restoring any
|
|
1022
|
+
* stashed filters can each independently require a reload — reloadAfterQueryChange()
|
|
1023
|
+
* is called at most once here regardless of how many of the two actually changed. */
|
|
1024
|
+
closeSearch() {
|
|
1025
|
+
this.searchOpen = false;
|
|
1026
|
+
if (this.searchDebounceTimer !== null) {
|
|
1027
|
+
clearTimeout(this.searchDebounceTimer);
|
|
1028
|
+
this.searchDebounceTimer = null;
|
|
1029
|
+
}
|
|
1030
|
+
const hadQuery = this.searchQuery !== '';
|
|
1031
|
+
this.searchQuery = '';
|
|
1032
|
+
const filtersRestored = this.restoreFiltersAfterSearch();
|
|
1033
|
+
if (hadQuery || filtersRestored)
|
|
1034
|
+
this.reloadAfterQueryChange();
|
|
1035
|
+
}
|
|
1036
|
+
handleSearchInput(value) {
|
|
1037
|
+
this.searchQuery = value;
|
|
1038
|
+
if (this.searchDebounceTimer !== null)
|
|
1039
|
+
clearTimeout(this.searchDebounceTimer);
|
|
1040
|
+
this.searchDebounceTimer = setTimeout(() => this.applySearch(), SEARCH_DEBOUNCE_MS);
|
|
1041
|
+
}
|
|
1042
|
+
/** Reloads from page 0 with the current searchQuery, same reset as applyFilter(). */
|
|
1043
|
+
applySearch() {
|
|
1044
|
+
this.searchDebounceTimer = null;
|
|
1045
|
+
this.reloadAfterQueryChange();
|
|
1046
|
+
}
|
|
1047
|
+
/** Column filters and the free-text `q` param are mutually exclusive on the backend —
|
|
1048
|
+
* sending both breaks the search. Opening the search box stashes any active filters
|
|
1049
|
+
* and clears them (the header filter icons and the clear-all button disappear along
|
|
1050
|
+
* with activeFilters, so there is nothing extra to hide) and reloads unfiltered. */
|
|
1051
|
+
stashFiltersForSearch() {
|
|
1052
|
+
if (this.activeFilters.size === 0)
|
|
1053
|
+
return;
|
|
1054
|
+
this.stashedFilters = this.activeFilters;
|
|
1055
|
+
this.activeFilters = new Map();
|
|
1056
|
+
this.reloadAfterQueryChange();
|
|
1057
|
+
}
|
|
1058
|
+
/** Restores filters stashed by stashFiltersForSearch(), unless the user applied a fresh
|
|
1059
|
+
* filter of their own while the search box was open — that explicit choice wins over
|
|
1060
|
+
* reinstating the old one. Returns whether activeFilters actually changed, so the
|
|
1061
|
+
* caller can decide whether a reload is needed. */
|
|
1062
|
+
restoreFiltersAfterSearch() {
|
|
1063
|
+
if (!this.stashedFilters)
|
|
1064
|
+
return false;
|
|
1065
|
+
const stashed = this.stashedFilters;
|
|
1066
|
+
this.stashedFilters = null;
|
|
1067
|
+
if (this.activeFilters.size > 0)
|
|
1068
|
+
return false;
|
|
1069
|
+
this.activeFilters = stashed;
|
|
1070
|
+
return true;
|
|
964
1071
|
}
|
|
965
1072
|
// ── View switcher ──────────────────────────────────────────────────────────
|
|
966
1073
|
handleViewSwitch(targetIdx) {
|
|
@@ -971,6 +1078,9 @@ export class MrdTable {
|
|
|
971
1078
|
this.activeViewIdx = targetIdx;
|
|
972
1079
|
this.applyDefaultSort((_b = (_a = target.view) === null || _a === void 0 ? void 0 : _a.defaultSort) !== null && _b !== void 0 ? _b : '');
|
|
973
1080
|
this.activeFilters = new Map();
|
|
1081
|
+
this.searchOpen = false;
|
|
1082
|
+
this.searchQuery = '';
|
|
1083
|
+
this.stashedFilters = null;
|
|
974
1084
|
this.init();
|
|
975
1085
|
}
|
|
976
1086
|
toggleViewPopover(e) {
|
|
@@ -993,7 +1103,7 @@ export class MrdTable {
|
|
|
993
1103
|
// ── Render: toolbar ────────────────────────────────────────────────────────
|
|
994
1104
|
renderToolbar() {
|
|
995
1105
|
var _a;
|
|
996
|
-
return (h(Toolbar, { locale: this.locale, filterCount: this.activeFilters.size, actions: this.tableActions, views: this.allViews, activeViewIdx: this.activeViewIdx, viewPopoverOpen: this.viewPopoverOpen, createPickerOpen: this.createPickerOpen, createTypes: (_a = this.item) === null || _a === void 0 ? void 0 : _a.createTypes, cb: {
|
|
1106
|
+
return (h(Toolbar, { locale: this.locale, filterCount: this.activeFilters.size, actions: this.tableActions, views: this.allViews, activeViewIdx: this.activeViewIdx, viewPopoverOpen: this.viewPopoverOpen, createPickerOpen: this.createPickerOpen, createTypes: (_a = this.item) === null || _a === void 0 ? void 0 : _a.createTypes, readOnly: this.readOnly, searchEnabled: this.searchEnabled, searchOpen: this.searchOpen, searchQuery: this.searchQuery, cb: {
|
|
997
1107
|
onClearAllFilters: () => this.clearAllFilters(),
|
|
998
1108
|
onViewSwitch: idx => this.handleViewSwitch(idx),
|
|
999
1109
|
onToggleViewPopover: e => this.toggleViewPopover(e),
|
|
@@ -1004,6 +1114,10 @@ export class MrdTable {
|
|
|
1004
1114
|
this.closeCreatePicker();
|
|
1005
1115
|
},
|
|
1006
1116
|
onAction: action => this.mrdAction.emit(this.buildActionDetail(action)),
|
|
1117
|
+
onToggleSearch: () => this.toggleSearch(),
|
|
1118
|
+
onSearchInput: value => this.handleSearchInput(value),
|
|
1119
|
+
onCloseSearch: () => this.closeSearch(),
|
|
1120
|
+
onSearchInputRef: el => (this.searchInputEl = el !== null && el !== void 0 ? el : null),
|
|
1007
1121
|
} }));
|
|
1008
1122
|
}
|
|
1009
1123
|
// ── Render: filter popup ───────────────────────────────────────────────────
|
|
@@ -1338,6 +1452,26 @@ export class MrdTable {
|
|
|
1338
1452
|
"reflect": false,
|
|
1339
1453
|
"attribute": "request-timeout-ms",
|
|
1340
1454
|
"defaultValue": "15000"
|
|
1455
|
+
},
|
|
1456
|
+
"readOnly": {
|
|
1457
|
+
"type": "boolean",
|
|
1458
|
+
"mutable": false,
|
|
1459
|
+
"complexType": {
|
|
1460
|
+
"original": "boolean",
|
|
1461
|
+
"resolved": "boolean",
|
|
1462
|
+
"references": {}
|
|
1463
|
+
},
|
|
1464
|
+
"required": false,
|
|
1465
|
+
"optional": false,
|
|
1466
|
+
"docs": {
|
|
1467
|
+
"tags": [],
|
|
1468
|
+
"text": "When true, the toolbar's create (\"+\") action is disabled (with a tooltip\nexplaining why) so a viewer-role user can see at a glance that this dashboard\nis read-only. Sorting, filtering and export stay available \u2014 those don't mutate data."
|
|
1469
|
+
},
|
|
1470
|
+
"getter": false,
|
|
1471
|
+
"setter": false,
|
|
1472
|
+
"reflect": false,
|
|
1473
|
+
"attribute": "read-only",
|
|
1474
|
+
"defaultValue": "false"
|
|
1341
1475
|
}
|
|
1342
1476
|
};
|
|
1343
1477
|
}
|
|
@@ -1354,6 +1488,8 @@ export class MrdTable {
|
|
|
1354
1488
|
"sortField": {},
|
|
1355
1489
|
"sortDir": {},
|
|
1356
1490
|
"activeFilters": {},
|
|
1491
|
+
"searchOpen": {},
|
|
1492
|
+
"searchQuery": {},
|
|
1357
1493
|
"openFilterCol": {},
|
|
1358
1494
|
"pendingFilter": {},
|
|
1359
1495
|
"popupPos": {},
|
|
@@ -8,6 +8,7 @@ import { NUMERIC_TYPES } from "./table-query";
|
|
|
8
8
|
* component; these components receive plain values and callbacks.
|
|
9
9
|
*/
|
|
10
10
|
export const FilterIcon = () => (h("span", { class: "mrd-table__filter-icon", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "currentColor" }, h("path", { d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" }))));
|
|
11
|
+
export const SearchIcon = ({ class: cls }) => (h("svg", { class: cls, viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l5 4.99L20.49 19zM9.5 14A4.5 4.5 0 1 1 14 9.5 4.5 4.5 0 0 1 9.5 14" })));
|
|
11
12
|
const TEXTBLOCK_MAX = 200;
|
|
12
13
|
export const TableCell = ({ col, row, locale, onDownload, onOpenTextblock, onOpenJson }) => {
|
|
13
14
|
var _a, _b, _c;
|
|
@@ -87,24 +88,31 @@ export const JsonModal = ({ html, locale, onClose }) => {
|
|
|
87
88
|
return null;
|
|
88
89
|
return (h("div", { class: "mrd-table__modal-backdrop", onClick: onClose, role: "dialog", "aria-modal": "true" }, h("div", { class: "mrd-table__modal", onClick: (e) => e.stopPropagation() }, h("button", { class: "mrd-table__modal-close", onClick: onClose, "aria-label": t('close', locale) }, "\u2715"), h("pre", { class: "mrd-table__modal-json", innerHTML: html }))));
|
|
89
90
|
};
|
|
90
|
-
export const Toolbar = ({ locale, filterCount, actions, views, activeViewIdx, viewPopoverOpen, createPickerOpen, createTypes, cb }) => {
|
|
91
|
+
export const Toolbar = ({ locale, filterCount, actions, views, activeViewIdx, viewPopoverOpen, createPickerOpen, createTypes, readOnly, searchEnabled, searchOpen, searchQuery, cb }) => {
|
|
91
92
|
var _a;
|
|
92
93
|
const hasActions = actions.length > 0;
|
|
93
94
|
const hasViewSwitcher = views.length > 1;
|
|
94
|
-
return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" },
|
|
95
|
+
return (h("div", { class: "mrd-table__toolbar" }, h("div", { class: "mrd-table__toolbar-left" }, searchEnabled && (searchOpen ? (h("span", { class: "mrd-table__search-box" }, h(SearchIcon, null), h("input", { type: "text", class: "mrd-table__search-input", value: searchQuery !== null && searchQuery !== void 0 ? searchQuery : '', placeholder: t('search_placeholder', locale), ref: cb.onSearchInputRef, onInput: (e) => cb.onSearchInput(e.target.value), onKeyDown: (e) => {
|
|
96
|
+
if (e.key === 'Escape')
|
|
97
|
+
cb.onCloseSearch();
|
|
98
|
+
} }), h("button", { type: "button", class: "mrd-table__search-clear", "aria-label": t('clear', locale), onClick: cb.onCloseSearch }, "\u2715"))) : (h("button", { type: "button", class: "mrd-table__action mrd-table__action--secondary mrd-table__search-toggle", onClick: cb.onToggleSearch }, h(SearchIcon, { class: "mrd-table__action-icon" }), h("span", { class: "mrd-table__action-tooltip" }, t('table_search', locale))))), filterCount > 0 && (h("button", { class: "mrd-table__action mrd-table__action--secondary mrd-table__filter-clear", onClick: cb.onClearAllFilters }, 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__filter-clear-label" }, t('table_filter_clear_all', locale), " (", filterCount, ")")))), hasViewSwitcher && (h("div", { class: "mrd-table__toolbar-center" }, h("div", { class: "mrd-table__view-switcher" }, h("button", { class: "mrd-table__view-arrow", "aria-label": "Previous view", onClick: () => cb.onViewSwitch((activeViewIdx - 1 + views.length) % views.length) }, "\u25C0"), h("div", { class: "mrd-table__view-title-wrap" }, h("button", { class: "mrd-table__view-title", onClick: (e) => cb.onToggleViewPopover(e) }, (_a = views[activeViewIdx]) === null || _a === void 0 ? void 0 : _a.label), viewPopoverOpen && (h("div", { class: "mrd-table__view-popover", onClick: (e) => e.stopPropagation() }, views.map((v, i) => (h("button", { key: String(i), class: `mrd-table__view-popover-item${i === activeViewIdx ? ' mrd-table__view-popover-item--active' : ''}`, onClick: () => {
|
|
95
99
|
if (i !== activeViewIdx)
|
|
96
100
|
cb.onViewSwitch(i);
|
|
97
101
|
cb.onCloseViewPopover();
|
|
98
102
|
} }, v.label)))))), h("button", { class: "mrd-table__view-arrow", "aria-label": "Next view", onClick: () => cb.onViewSwitch((activeViewIdx + 1) % views.length) }, "\u25B6")))), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, actions.map(a => {
|
|
99
103
|
var _a, _b;
|
|
100
104
|
const pickerTypes = a.action === 'create' ? createTypes : null;
|
|
105
|
+
// readOnly only ever disables 'create' (see mrd-table.tsx tableActions) — export,
|
|
106
|
+
// sorting and filtering don't mutate data and stay available.
|
|
107
|
+
const disabled = a.disabled || (readOnly && a.action === 'create');
|
|
108
|
+
const tooltip = disabled && readOnly ? t('readonly_access', locale) : a.label;
|
|
101
109
|
if (pickerTypes === null || pickerTypes === void 0 ? void 0 : pickerTypes.length) {
|
|
102
|
-
return (h("div", { class: "mrd-table__create-picker-wrap", key: `action-${a.action}` }, h("button", { class: `mrd-table__action mrd-table__action--${(_a = a.variant) !== null && _a !== void 0 ? _a : 'secondary'}`, onClick: cb.onToggleCreatePicker }, a.icon
|
|
110
|
+
return (h("div", { class: "mrd-table__create-picker-wrap", key: `action-${a.action}` }, h("button", { class: `mrd-table__action mrd-table__action--${(_a = a.variant) !== null && _a !== void 0 ? _a : 'secondary'}`, disabled: disabled, onClick: cb.onToggleCreatePicker }, a.icon
|
|
103
111
|
? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
|
|
104
|
-
: a.label, h("span", { class: "mrd-table__action-tooltip" },
|
|
112
|
+
: a.label, h("span", { class: "mrd-table__action-tooltip" }, tooltip)), createPickerOpen && (h("div", { class: "mrd-table__create-picker" }, pickerTypes.map(ct => (h("button", { key: ct.type, class: "mrd-table__create-picker-item", onClick: () => cb.onCreate(ct.type) }, ct.label)))))));
|
|
105
113
|
}
|
|
106
|
-
return (h("button", { key: `action-${a.action}`, class: `mrd-table__action mrd-table__action--${(_b = a.variant) !== null && _b !== void 0 ? _b : 'secondary'}`, disabled:
|
|
114
|
+
return (h("button", { key: `action-${a.action}`, class: `mrd-table__action mrd-table__action--${(_b = a.variant) !== null && _b !== void 0 ? _b : 'secondary'}`, disabled: disabled, onClick: () => cb.onAction(a.action) }, a.icon
|
|
107
115
|
? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
|
|
108
|
-
: a.label, h("span", { class: "mrd-table__action-tooltip" },
|
|
116
|
+
: a.label, h("span", { class: "mrd-table__action-tooltip" }, tooltip)));
|
|
109
117
|
})))));
|
|
110
118
|
};
|
|
@@ -9,6 +9,10 @@ export const DATE_TYPES = new Set(['DATE', 'DATETIME', 'TIME']);
|
|
|
9
9
|
export const NO_FILTER_TYPES = new Set(['FILE', 'IMAGE']);
|
|
10
10
|
/** Column types that cannot be sorted or filtered (not stored in PostgreSQL). */
|
|
11
11
|
export const NON_INTERACTIVE_TYPES = new Set(['LONGTEXT', 'JSON', 'FILE', 'IMAGE']);
|
|
12
|
+
/** Wait this long (ms) after the user stops typing in the free-text search box
|
|
13
|
+
* before firing the search request — mirrors REQUEST_DEBOUNCE_MS's purpose
|
|
14
|
+
* (skip intermediate keystrokes) but is tuned for typing, not scrolling. */
|
|
15
|
+
export const SEARCH_DEBOUNCE_MS = 400;
|
|
12
16
|
export function parseDefaultSort(defaultSort) {
|
|
13
17
|
var _a;
|
|
14
18
|
if (!defaultSort)
|
|
@@ -33,6 +37,8 @@ export function buildTableQueryParams(opts) {
|
|
|
33
37
|
if (opts.filterClass)
|
|
34
38
|
p.set('type', opts.filterClass);
|
|
35
39
|
applyViewFilters(p, opts.viewFilters);
|
|
40
|
+
if (opts.query)
|
|
41
|
+
p.set('q', opts.query);
|
|
36
42
|
for (const f of opts.activeFilters) {
|
|
37
43
|
if (f.operator === 'isEmpty') {
|
|
38
44
|
p.set(f.field, '');
|
|
@@ -14,6 +14,8 @@ let _dashboardType = 'class'; // 'class' | 'general' | 'navigation' | 'objec
|
|
|
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
|
|
17
|
+
let _detailReadOnly = false; // Detail View tab: readOnly toggle (TASK-0248 demo)
|
|
18
|
+
let _liveApiReadOnly = false; // Live API tab: readOnly toggle (TASK-0248 demo)
|
|
17
19
|
|
|
18
20
|
/* =====================================================================
|
|
19
21
|
UTILITIES
|
|
@@ -135,6 +137,7 @@ async function initDetailViewTab() {
|
|
|
135
137
|
table.totalElements = window.EXAMPLE_COMPANIES.length;
|
|
136
138
|
table.pageSize = 20;
|
|
137
139
|
table.locale = _locale;
|
|
140
|
+
table.readOnly = _detailReadOnly;
|
|
138
141
|
|
|
139
142
|
// Register before init(): init() emits mrdLoadPage for page 0 itself.
|
|
140
143
|
table.addEventListener('mrdLoadPage', async (e) => {
|
|
@@ -175,6 +178,7 @@ function showCompanyDetail(row) {
|
|
|
175
178
|
section.items = layout.items;
|
|
176
179
|
section.data = company;
|
|
177
180
|
section.locale = _locale;
|
|
181
|
+
section.readOnly = _detailReadOnly;
|
|
178
182
|
|
|
179
183
|
section.addEventListener('mrdNavigate', (e) => {
|
|
180
184
|
logEvent('mrdNavigate', e.detail);
|
|
@@ -199,6 +203,17 @@ function showCompanyList() {
|
|
|
199
203
|
document.getElementById('detail-table-panel').style.display = 'block';
|
|
200
204
|
}
|
|
201
205
|
|
|
206
|
+
/** readOnly demo (TASK-0248): flips the companies table and any rendered
|
|
207
|
+
* detail sections without re-fetching — readOnly is a pure render-time prop. */
|
|
208
|
+
function onDetailViewReadOnlyToggle(checked) {
|
|
209
|
+
_detailReadOnly = checked;
|
|
210
|
+
const table = document.getElementById('companies-table');
|
|
211
|
+
if (table) table.readOnly = checked;
|
|
212
|
+
document.querySelectorAll('#detail-sections mrd-layout-section').forEach((section) => {
|
|
213
|
+
section.readOnly = checked;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
/* =====================================================================
|
|
203
218
|
DOCUMENT PREVIEW TAB
|
|
204
219
|
===================================================================== */
|
|
@@ -494,6 +509,7 @@ async function renderSection(index) {
|
|
|
494
509
|
section.items = layout.items;
|
|
495
510
|
section.data = _dashboardRecord ?? { _links: _dashboardData._links ?? {} };
|
|
496
511
|
section.locale = _locale;
|
|
512
|
+
section.readOnly = _liveApiReadOnly;
|
|
497
513
|
// Dashboard-level archetypes describe the object itself → single-document view.
|
|
498
514
|
// Only apply to the main object layout, not tab-page (related) layouts.
|
|
499
515
|
section.archetypes = (!layout.tabPage && _dashboardData.archetypes) ? _dashboardData.archetypes : [];
|
|
@@ -670,6 +686,14 @@ async function renderSection(index) {
|
|
|
670
686
|
}
|
|
671
687
|
|
|
672
688
|
|
|
689
|
+
/** readOnly demo (TASK-0248): flips the currently rendered section without
|
|
690
|
+
* re-fetching — readOnly is a pure render-time prop, no reload needed. */
|
|
691
|
+
function onLiveApiReadOnlyToggle(checked) {
|
|
692
|
+
_liveApiReadOnly = checked;
|
|
693
|
+
const section = document.querySelector('#sections-container mrd-layout-section');
|
|
694
|
+
if (section) section.readOnly = checked;
|
|
695
|
+
}
|
|
696
|
+
|
|
673
697
|
function toggleJsonViewer() {
|
|
674
698
|
const viewer = document.getElementById('json-viewer');
|
|
675
699
|
const btn = document.getElementById('btn-json-toggle');
|
|
@@ -31,8 +31,10 @@ const translations = {
|
|
|
31
31
|
value_empty: '—',
|
|
32
32
|
// mrd-table toolbar
|
|
33
33
|
table_filter_clear_all: 'Alle filters wissen',
|
|
34
|
+
table_search: 'Zoeken',
|
|
34
35
|
table_new_record: 'Nieuw record',
|
|
35
36
|
table_export_excel: 'Exporteer naar Excel',
|
|
37
|
+
readonly_access: 'Je hebt alleen-leestoegang',
|
|
36
38
|
doc_view_switch: 'Weergave wisselen',
|
|
37
39
|
doc_view_tree: 'Boomweergave',
|
|
38
40
|
doc_upload: 'Uploaden',
|
|
@@ -134,8 +136,10 @@ const translations = {
|
|
|
134
136
|
value_empty: '—',
|
|
135
137
|
// mrd-table toolbar
|
|
136
138
|
table_filter_clear_all: 'Clear all filters',
|
|
139
|
+
table_search: 'Search',
|
|
137
140
|
table_new_record: 'New record',
|
|
138
141
|
table_export_excel: 'Export to Excel',
|
|
142
|
+
readonly_access: 'You have read-only access',
|
|
139
143
|
doc_view_switch: 'Switch view',
|
|
140
144
|
doc_view_tree: 'Tree view',
|
|
141
145
|
doc_upload: 'Upload',
|
|
@@ -237,8 +241,10 @@ const translations = {
|
|
|
237
241
|
value_empty: '—',
|
|
238
242
|
// mrd-table toolbar
|
|
239
243
|
table_filter_clear_all: 'مسح جميع الفلاتر',
|
|
244
|
+
table_search: 'بحث',
|
|
240
245
|
table_new_record: 'سجل جديد',
|
|
241
246
|
table_export_excel: 'تصدير إلى Excel',
|
|
247
|
+
readonly_access: 'لديك حق الوصول للقراءة فقط',
|
|
242
248
|
doc_view_switch: 'تبديل العرض',
|
|
243
249
|
doc_view_tree: 'عرض شجري',
|
|
244
250
|
doc_upload: 'رفع',
|
|
@@ -334,8 +340,10 @@ const translations = {
|
|
|
334
340
|
value_empty: '—',
|
|
335
341
|
// mrd-table toolbar
|
|
336
342
|
table_filter_clear_all: 'Effacer tous les filtres',
|
|
343
|
+
table_search: 'Rechercher',
|
|
337
344
|
table_new_record: 'Nouvel enregistrement',
|
|
338
345
|
table_export_excel: 'Exporter vers Excel',
|
|
346
|
+
readonly_access: "Vous disposez d'un accès en lecture seule",
|
|
339
347
|
doc_view_switch: 'Changer de vue',
|
|
340
348
|
doc_view_tree: 'Vue arborescente',
|
|
341
349
|
doc_upload: 'Téléverser',
|
|
@@ -431,8 +439,10 @@ const translations = {
|
|
|
431
439
|
value_empty: '—',
|
|
432
440
|
// mrd-table toolbar
|
|
433
441
|
table_filter_clear_all: 'Alle Filter löschen',
|
|
442
|
+
table_search: 'Suchen',
|
|
434
443
|
table_new_record: 'Neuer Eintrag',
|
|
435
444
|
table_export_excel: 'Als Excel exportieren',
|
|
445
|
+
readonly_access: 'Sie haben nur Lesezugriff',
|
|
436
446
|
doc_view_switch: 'Ansicht wechseln',
|
|
437
447
|
doc_view_tree: 'Baumansicht',
|
|
438
448
|
doc_upload: 'Hochladen',
|
|
@@ -528,8 +538,10 @@ const translations = {
|
|
|
528
538
|
value_empty: '—',
|
|
529
539
|
// mrd-table toolbar
|
|
530
540
|
table_filter_clear_all: 'Borrar todos los filtros',
|
|
541
|
+
table_search: 'Buscar',
|
|
531
542
|
table_new_record: 'Nuevo registro',
|
|
532
543
|
table_export_excel: 'Exportar a Excel',
|
|
544
|
+
readonly_access: 'Tienes acceso de solo lectura',
|
|
533
545
|
doc_view_switch: 'Cambiar vista',
|
|
534
546
|
doc_view_tree: 'Vista de árbol',
|
|
535
547
|
doc_upload: 'Subir',
|
|
@@ -625,8 +637,10 @@ const translations = {
|
|
|
625
637
|
value_empty: '—',
|
|
626
638
|
// mrd-table toolbar
|
|
627
639
|
table_filter_clear_all: 'Cancella tutti i filtri',
|
|
640
|
+
table_search: 'Cerca',
|
|
628
641
|
table_new_record: 'Nuovo record',
|
|
629
642
|
table_export_excel: 'Esporta in Excel',
|
|
643
|
+
readonly_access: 'Hai accesso in sola lettura',
|
|
630
644
|
doc_view_switch: 'Cambia vista',
|
|
631
645
|
doc_view_tree: 'Vista ad albero',
|
|
632
646
|
doc_upload: 'Carica',
|
|
@@ -722,8 +736,10 @@ const translations = {
|
|
|
722
736
|
value_empty: '—',
|
|
723
737
|
// mrd-table toolbar
|
|
724
738
|
table_filter_clear_all: 'Очистити всі фільтри',
|
|
739
|
+
table_search: 'Пошук',
|
|
725
740
|
table_new_record: 'Новий запис',
|
|
726
741
|
table_export_excel: 'Експортувати до Excel',
|
|
742
|
+
readonly_access: 'У вас доступ лише для перегляду',
|
|
727
743
|
doc_view_switch: 'Змінити вигляд',
|
|
728
744
|
doc_view_tree: 'Деревоподібний вигляд',
|
|
729
745
|
doc_upload: 'Завантажити',
|