@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.
Files changed (53) hide show
  1. package/dist/cjs/{cell-renderer-pwUGEDql.js → cell-renderer-mjS630f1.js} +16 -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_25.cjs.entry.js +212 -45
  5. package/dist/cjs/mrd-merge-view.cjs.entry.js +1 -1
  6. package/dist/collection/components/documents/mrd-document-container/mrd-document-container.js +30 -4
  7. package/dist/collection/components/documents/mrd-document-list/mrd-document-list.js +55 -6
  8. package/dist/collection/components/layout/mrd-layout-section/mrd-layout-section.js +27 -3
  9. package/dist/collection/components/table/mrd-table/mrd-table.css +36 -0
  10. package/dist/collection/components/table/mrd-table/mrd-table.js +162 -26
  11. package/dist/collection/components/table/mrd-table/table-parts.js +14 -6
  12. package/dist/collection/components/table/mrd-table/table-query.js +6 -0
  13. package/dist/collection/dev/app.js +24 -0
  14. package/dist/collection/utils/i18n.js +16 -0
  15. package/dist/components/i18n.js +1 -1
  16. package/dist/components/mrd-document-container2.js +1 -1
  17. package/dist/components/mrd-document-list2.js +1 -1
  18. package/dist/components/mrd-layout-section.js +1 -1
  19. package/dist/components/mrd-table2.js +1 -1
  20. package/dist/esm/{cell-renderer-B8rJpnq8.js → cell-renderer-BDY8UXkH.js} +16 -0
  21. package/dist/esm/loader.js +1 -1
  22. package/dist/esm/mosterdcomponents.js +1 -1
  23. package/dist/esm/mrd-boolean-field_25.entry.js +212 -45
  24. package/dist/esm/mrd-merge-view.entry.js +1 -1
  25. package/dist/mosterdcomponents/cell-renderer-B8u5BCK-.js.map +1 -0
  26. package/dist/mosterdcomponents/client-layout-Bc42CfJB.js.map +1 -0
  27. package/dist/mosterdcomponents/document-attachments-BufPmMRv.js.map +1 -0
  28. package/dist/mosterdcomponents/field-helpers-CNQcI3Hf.js.map +1 -0
  29. package/dist/mosterdcomponents/file-upload-common-C9wp47CB.js.map +1 -0
  30. package/dist/mosterdcomponents/format-gcecItcD.js.map +1 -0
  31. package/dist/mosterdcomponents/i18n-ydNM87-Q.js.map +1 -0
  32. package/dist/mosterdcomponents/index-1ayBojyN.js.map +1 -0
  33. package/dist/mosterdcomponents/index-DhffneUF.js.map +1 -0
  34. package/dist/mosterdcomponents/index.esm.js.map +1 -0
  35. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  36. package/dist/mosterdcomponents/mosterdcomponents.esm.js.map +1 -0
  37. package/dist/mosterdcomponents/p-Dkm4WxMy.js +1 -0
  38. package/dist/mosterdcomponents/p-c834951a.entry.js +3 -0
  39. package/dist/mosterdcomponents/{p-e540de6a.entry.js → p-e8b0880f.entry.js} +1 -1
  40. package/dist/mosterdcomponents/purify.es-_duHfPgC.js.map +1 -0
  41. package/dist/mosterdcomponents/query-params-BZbQoeYF.js.map +1 -0
  42. package/dist/mosterdcomponents/quill-C9pgw_k-.js.map +1 -0
  43. package/dist/mosterdcomponents/validation-CjjTt66G.js.map +1 -0
  44. package/dist/types/components/documents/mrd-document-container/mrd-document-container.d.ts +4 -0
  45. package/dist/types/components/documents/mrd-document-list/mrd-document-list.d.ts +6 -1
  46. package/dist/types/components/layout/mrd-layout-section/mrd-layout-section.d.ts +4 -0
  47. package/dist/types/components/table/mrd-table/mrd-table.d.ts +52 -1
  48. package/dist/types/components/table/mrd-table/table-parts.d.ts +18 -0
  49. package/dist/types/components/table/mrd-table/table-query.d.ts +6 -0
  50. package/dist/types/components.d.ts +45 -1
  51. package/package.json +1 -1
  52. package/dist/mosterdcomponents/p-eb322d5c.entry.js +0 -3
  53. 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
- if (!isLastPage || hasAggColumns) {
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.aggregations = null;
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.aggregations = null;
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.aggregations = null;
958
- this.aggregationsTotal = null;
959
- this.aggregationsPending = true;
960
- if (this.isPaginated) {
961
- this.resetPages();
962
- this.emitPagesForWindow(this.renderStart, this.renderEnd);
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" }, 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
+ 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" }, a.label)), 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)))))));
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: a.disabled, onClick: () => cb.onAction(a.action) }, a.icon
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" }, a.label)));
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: 'Завантажити',