@mmlogic/components 0.3.11 → 0.4.1

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 (138) hide show
  1. package/dist/cjs/{index-BPj2cBXs.js → index-CAzwGt9u.js} +38 -0
  2. package/dist/cjs/loader.cjs.js +2 -2
  3. package/dist/cjs/mosterdcomponents.cjs.js +2 -2
  4. package/dist/cjs/mrd-boolean-field_23.cjs.entry.js +1313 -1088
  5. package/dist/collection/collection-manifest.json +23 -23
  6. package/dist/collection/components/{mrd-document-container/mrd-document-container.scss → documents/mrd-document-container/mrd-document-container.css} +3 -3
  7. package/dist/collection/components/{mrd-document-container → documents/mrd-document-container}/mrd-document-container.js +7 -7
  8. package/dist/collection/components/documents/mrd-document-list/folder-tree.js +109 -0
  9. package/dist/collection/components/{mrd-document-list/mrd-document-list.scss → documents/mrd-document-list/mrd-document-list.css} +6 -4
  10. package/dist/collection/components/{mrd-document-list → documents/mrd-document-list}/mrd-document-list.js +83 -240
  11. package/dist/collection/components/documents/mrd-document-list/tree-view.js +75 -0
  12. package/dist/collection/components/{mrd-document-view/mrd-document-view.scss → documents/mrd-document-view/mrd-document-view.css} +1 -1
  13. package/dist/collection/components/{mrd-document-view → documents/mrd-document-view}/mrd-document-view.js +4 -4
  14. package/dist/collection/components/fields/field-helpers.js +17 -0
  15. package/dist/collection/components/fields/file-upload-common.js +38 -0
  16. package/dist/collection/components/{mrd-boolean-field/mrd-boolean-field.scss → fields/mrd-boolean-field/mrd-boolean-field.css} +3 -3
  17. package/dist/collection/components/{mrd-boolean-field → fields/mrd-boolean-field}/mrd-boolean-field.js +1 -1
  18. package/dist/collection/components/{mrd-currency-field/mrd-currency-field.scss → fields/mrd-currency-field/mrd-currency-field.css} +11 -9
  19. package/dist/collection/components/{mrd-currency-field → fields/mrd-currency-field}/mrd-currency-field.js +7 -13
  20. package/dist/collection/components/{mrd-date-field/mrd-date-field.scss → fields/mrd-date-field/mrd-date-field.css} +8 -10
  21. package/dist/collection/components/{mrd-date-field → fields/mrd-date-field}/mrd-date-field.js +3 -10
  22. package/dist/collection/components/{mrd-datetime-field/mrd-datetime-field.scss → fields/mrd-datetime-field/mrd-datetime-field.css} +8 -10
  23. package/dist/collection/components/{mrd-datetime-field → fields/mrd-datetime-field}/mrd-datetime-field.js +3 -10
  24. package/dist/collection/components/{mrd-email-field/mrd-email-field.scss → fields/mrd-email-field/mrd-email-field.css} +2 -11
  25. package/dist/collection/components/{mrd-email-field → fields/mrd-email-field}/mrd-email-field.js +6 -13
  26. package/dist/collection/components/{mrd-file-field/mrd-file-field.scss → fields/mrd-file-field/mrd-file-field.css} +19 -24
  27. package/dist/collection/components/{mrd-file-field → fields/mrd-file-field}/mrd-file-field.js +13 -24
  28. package/dist/collection/components/{mrd-hyperlink-field/mrd-hyperlink-field.scss → fields/mrd-hyperlink-field/mrd-hyperlink-field.css} +7 -16
  29. package/dist/collection/components/{mrd-hyperlink-field → fields/mrd-hyperlink-field}/mrd-hyperlink-field.js +9 -15
  30. package/dist/collection/components/{mrd-image-field/mrd-image-field.scss → fields/mrd-image-field/mrd-image-field.css} +15 -24
  31. package/dist/collection/components/{mrd-image-field → fields/mrd-image-field}/mrd-image-field.js +13 -28
  32. package/dist/collection/components/{mrd-list-field/mrd-list-field.scss → fields/mrd-list-field/mrd-list-field.css} +18 -21
  33. package/dist/collection/components/{mrd-list-field → fields/mrd-list-field}/mrd-list-field.js +10 -25
  34. package/dist/collection/components/{mrd-longtext-field/mrd-longtext-field.scss → fields/mrd-longtext-field/mrd-longtext-field.css} +21 -18
  35. package/dist/collection/components/{mrd-longtext-field → fields/mrd-longtext-field}/mrd-longtext-field.js +3 -10
  36. package/dist/collection/components/{mrd-number-field/mrd-number-field.scss → fields/mrd-number-field/mrd-number-field.css} +9 -10
  37. package/dist/collection/components/{mrd-number-field → fields/mrd-number-field}/mrd-number-field.js +7 -6
  38. package/dist/collection/components/{mrd-relation-field/mrd-relation-field.scss → fields/mrd-relation-field/mrd-relation-field.css} +61 -59
  39. package/dist/collection/components/{mrd-relation-field → fields/mrd-relation-field}/mrd-relation-field.js +16 -26
  40. package/dist/collection/components/{mrd-secret-field/mrd-secret-field.scss → fields/mrd-secret-field/mrd-secret-field.css} +7 -10
  41. package/dist/collection/components/{mrd-secret-field → fields/mrd-secret-field}/mrd-secret-field.js +4 -11
  42. package/dist/collection/components/{mrd-text-field/mrd-text-field.scss → fields/mrd-text-field/mrd-text-field.css} +2 -11
  43. package/dist/collection/components/{mrd-text-field → fields/mrd-text-field}/mrd-text-field.js +3 -10
  44. package/dist/collection/components/{mrd-textarea-field/mrd-textarea-field.scss → fields/mrd-textarea-field/mrd-textarea-field.css} +7 -10
  45. package/dist/collection/components/{mrd-textarea-field → fields/mrd-textarea-field}/mrd-textarea-field.js +3 -9
  46. package/dist/collection/components/{mrd-time-field/mrd-time-field.scss → fields/mrd-time-field/mrd-time-field.css} +8 -10
  47. package/dist/collection/components/{mrd-time-field → fields/mrd-time-field}/mrd-time-field.js +3 -10
  48. package/dist/collection/components/{mrd-field/mrd-field.scss → form/mrd-field/mrd-field.css} +1 -1
  49. package/dist/collection/components/{mrd-field → form/mrd-field}/mrd-field.js +3 -3
  50. package/dist/collection/components/{mrd-form/mrd-form.scss → form/mrd-form/mrd-form.css} +1 -1
  51. package/dist/collection/components/{mrd-form → form/mrd-form}/mrd-form.js +5 -5
  52. package/dist/collection/components/layout/mrd-layout-section/field-value.js +111 -0
  53. package/dist/collection/components/layout/mrd-layout-section/layout-helpers.js +29 -0
  54. package/dist/collection/components/{mrd-layout-section/mrd-layout-section.scss → layout/mrd-layout-section/mrd-layout-section.css} +4 -4
  55. package/dist/collection/components/{mrd-layout-section → layout/mrd-layout-section}/mrd-layout-section.js +52 -142
  56. package/dist/collection/components/{mrd-table/mrd-table.scss → table/mrd-table/mrd-table.css} +30 -12
  57. package/dist/collection/components/{mrd-table → table/mrd-table}/mrd-table.js +234 -443
  58. package/dist/collection/components/table/mrd-table/table-filter-popup.js +85 -0
  59. package/dist/collection/components/table/mrd-table/table-parts.js +110 -0
  60. package/dist/collection/components/table/mrd-table/table-query.js +178 -0
  61. package/dist/collection/components/table/mrd-table/virtual-scroll.js +63 -0
  62. package/dist/collection/dev/app.js +19 -18
  63. package/dist/collection/dev/example-data.js +58 -43
  64. package/dist/collection/utils/i18n.js +16 -0
  65. package/dist/collection/utils/query-params.js +43 -0
  66. package/dist/components/field-helpers.js +1 -0
  67. package/dist/components/file-upload-common.js +1 -0
  68. package/dist/components/i18n.js +1 -1
  69. package/dist/components/mrd-boolean-field2.js +1 -1
  70. package/dist/components/mrd-currency-field2.js +1 -1
  71. package/dist/components/mrd-date-field2.js +1 -1
  72. package/dist/components/mrd-datetime-field2.js +1 -1
  73. package/dist/components/mrd-document-container2.js +1 -1
  74. package/dist/components/mrd-document-list2.js +1 -1
  75. package/dist/components/mrd-document-view2.js +1 -1
  76. package/dist/components/mrd-email-field2.js +1 -1
  77. package/dist/components/mrd-file-field2.js +1 -1
  78. package/dist/components/mrd-form.js +1 -1
  79. package/dist/components/mrd-hyperlink-field2.js +1 -1
  80. package/dist/components/mrd-image-field2.js +1 -1
  81. package/dist/components/mrd-layout-section.js +1 -1
  82. package/dist/components/mrd-list-field2.js +1 -1
  83. package/dist/components/mrd-longtext-field2.js +1 -1
  84. package/dist/components/mrd-number-field2.js +1 -1
  85. package/dist/components/mrd-relation-field2.js +1 -1
  86. package/dist/components/mrd-secret-field2.js +1 -1
  87. package/dist/components/mrd-table2.js +1 -1
  88. package/dist/components/mrd-text-field2.js +1 -1
  89. package/dist/components/mrd-textarea-field2.js +1 -1
  90. package/dist/components/mrd-time-field2.js +1 -1
  91. package/dist/components/query-params.js +1 -0
  92. package/dist/esm/{index-_tsCCkAi.js → index-DiSEJMA8.js} +38 -1
  93. package/dist/esm/loader.js +3 -3
  94. package/dist/esm/mosterdcomponents.js +3 -3
  95. package/dist/esm/mrd-boolean-field_23.entry.js +1313 -1088
  96. package/dist/mosterdcomponents/mosterdcomponents.css +1 -1
  97. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  98. package/dist/mosterdcomponents/p-DiSEJMA8.js +2 -0
  99. package/dist/mosterdcomponents/p-b1a2e108.entry.js +1 -0
  100. package/dist/types/components/{mrd-document-container → documents/mrd-document-container}/mrd-document-container.d.ts +2 -2
  101. package/dist/types/components/documents/mrd-document-list/folder-tree.d.ts +66 -0
  102. package/dist/types/components/{mrd-document-list → documents/mrd-document-list}/mrd-document-list.d.ts +4 -37
  103. package/dist/types/components/documents/mrd-document-list/tree-view.d.ts +32 -0
  104. package/dist/types/components/{mrd-document-view → documents/mrd-document-view}/mrd-document-view.d.ts +2 -2
  105. package/dist/types/components/fields/field-helpers.d.ts +18 -0
  106. package/dist/types/components/fields/file-upload-common.d.ts +18 -0
  107. package/dist/types/components/{mrd-boolean-field → fields/mrd-boolean-field}/mrd-boolean-field.d.ts +1 -1
  108. package/dist/types/components/{mrd-currency-field → fields/mrd-currency-field}/mrd-currency-field.d.ts +2 -2
  109. package/dist/types/components/{mrd-date-field → fields/mrd-date-field}/mrd-date-field.d.ts +1 -1
  110. package/dist/types/components/{mrd-datetime-field → fields/mrd-datetime-field}/mrd-datetime-field.d.ts +1 -1
  111. package/dist/types/components/{mrd-email-field → fields/mrd-email-field}/mrd-email-field.d.ts +1 -1
  112. package/dist/types/components/{mrd-file-field → fields/mrd-file-field}/mrd-file-field.d.ts +2 -4
  113. package/dist/types/components/{mrd-hyperlink-field → fields/mrd-hyperlink-field}/mrd-hyperlink-field.d.ts +2 -2
  114. package/dist/types/components/{mrd-image-field → fields/mrd-image-field}/mrd-image-field.d.ts +2 -4
  115. package/dist/types/components/{mrd-list-field → fields/mrd-list-field}/mrd-list-field.d.ts +2 -2
  116. package/dist/types/components/{mrd-longtext-field → fields/mrd-longtext-field}/mrd-longtext-field.d.ts +1 -1
  117. package/dist/types/components/{mrd-number-field → fields/mrd-number-field}/mrd-number-field.d.ts +2 -2
  118. package/dist/types/components/{mrd-relation-field → fields/mrd-relation-field}/mrd-relation-field.d.ts +2 -2
  119. package/dist/types/components/{mrd-secret-field → fields/mrd-secret-field}/mrd-secret-field.d.ts +1 -1
  120. package/dist/types/components/{mrd-text-field → fields/mrd-text-field}/mrd-text-field.d.ts +1 -1
  121. package/dist/types/components/{mrd-textarea-field → fields/mrd-textarea-field}/mrd-textarea-field.d.ts +1 -1
  122. package/dist/types/components/{mrd-time-field → fields/mrd-time-field}/mrd-time-field.d.ts +1 -1
  123. package/dist/types/components/{mrd-field → form/mrd-field}/mrd-field.d.ts +2 -2
  124. package/dist/types/components/{mrd-form → form/mrd-form}/mrd-form.d.ts +2 -2
  125. package/dist/types/components/layout/mrd-layout-section/field-value.d.ts +22 -0
  126. package/dist/types/components/layout/mrd-layout-section/layout-helpers.d.ts +8 -0
  127. package/dist/types/components/{mrd-layout-section → layout/mrd-layout-section}/mrd-layout-section.d.ts +4 -11
  128. package/dist/types/components/{mrd-table → table/mrd-table}/mrd-table.d.ts +29 -13
  129. package/dist/types/components/table/mrd-table/table-filter-popup.d.ts +35 -0
  130. package/dist/types/components/table/mrd-table/table-parts.d.ts +61 -0
  131. package/dist/types/components/table/mrd-table/table-query.d.ts +50 -0
  132. package/dist/types/components/table/mrd-table/virtual-scroll.d.ts +33 -0
  133. package/dist/types/components.d.ts +19 -5
  134. package/dist/types/utils/query-params.d.ts +14 -0
  135. package/package.json +7 -2
  136. package/dist/components/validation.js +0 -1
  137. package/dist/mosterdcomponents/p-_tsCCkAi.js +0 -2
  138. package/dist/mosterdcomponents/p-a9618055.entry.js +0 -1
@@ -1,28 +1,30 @@
1
- import { h, Host } from "@stencil/core";
2
- import { CellRenderer } from "../../utils/cell-renderer";
3
- import { t } from "../../utils/i18n";
4
- import { formatNumber, formatPercentage, formatCurrency } from "../../utils/format";
5
- const BUFFER = 10;
6
- /** Wacht deze tijd (ms) na het laatste scroll-event voordat pagina's worden
7
- * aangevraagd. Pagina's die de gebruiker snel voorbij scrollt worden zo geskipt. */
8
- const REQUEST_DEBOUNCE_MS = 150;
1
+ import { h, Host, writeTask } from "@stencil/core";
2
+ import { t } from "../../../utils/i18n";
3
+ import { buildListPath } from "../../../utils/query-params";
4
+ import { TEXT_TYPES, NON_INTERACTIVE_TYPES, parseDefaultSort, buildSortParam, buildTableQueryParams, buildAggregationQs, filterHasValue, normalizeDatetimeFilter, datetimeFilterToDisplay, } from "./table-query";
5
+ import { FilterIcon, TableCell, TotalsRow, TextblockModal, JsonModal, Toolbar } from "./table-parts";
6
+ import { FilterPopup } from "./table-filter-popup";
7
+ import { REQUEST_DEBOUNCE_MS, REQUEST_TIMEOUT_MS, MAX_RETRY_ATTEMPTS, visibleCount, computeWindow, pageRange, getRowFromPages, computeEffectiveTotal, isTotalKnownFromPages, getStalePages, } from "./virtual-scroll";
8
+ /** How often (ms) to scan outstanding page requests for staleness. */
9
+ const STALE_CHECK_INTERVAL_MS = 2000;
9
10
  /** Breedte van de filterpopup in px — voor overflow-correctie. */
10
11
  const POPUP_WIDTH = 280;
11
- const TEXT_TYPES = new Set(['TEXT', 'TEXTBLOCK', 'EMAIL', 'HYPERLINK']);
12
- const NUMERIC_TYPES = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
13
- const DATE_TYPES = new Set(['DATE', 'DATETIME', 'TIME']);
14
- const NO_FILTER_TYPES = new Set(['FILE', 'IMAGE']);
15
- /** Column types that cannot be sorted or filtered (not stored in PostgreSQL). */
16
- const NON_INTERACTIVE_TYPES = new Set(['LONGTEXT', 'JSON', 'FILE', 'IMAGE']);
17
12
  export class MrdTable {
18
13
  constructor() {
19
14
  // ── Non-state internals ────────────────────────────────────────────────────
20
15
  this.pendingPages = new Set();
21
16
  this.debounceTimer = null;
17
+ /** Timestamp of the last mrdLoadPage emission per outstanding page — drives stale-request detection. */
18
+ this.requestedAt = new Map();
19
+ /** Retry count per page since it first became stale. */
20
+ this.retryAttempts = new Map();
21
+ this.staleCheckTimer = null;
22
22
  this.outsideClickHandler = null;
23
23
  this.keydownHandler = null;
24
24
  this.createPickerClickHandler = null;
25
25
  this.viewPopoverClickHandler = null;
26
+ /** Guards against scheduling more than one column-width measurement per freeze. */
27
+ this.colWidthMeasureScheduled = false;
26
28
  // ── Props ──────────────────────────────────────────────────────────────────
27
29
  /** The VIEW or RELATED_VIEW layout item. Contains view config, dataClass, fromClass, actions etc. */
28
30
  this.item = null;
@@ -39,11 +41,16 @@ export class MrdTable {
39
41
  this.rowHeight = 36;
40
42
  /** Height of the scroll container in px. */
41
43
  this.tableHeight = 500;
44
+ /** Time (ms) to wait for setPage() before retrying a page request; after
45
+ * MAX_RETRY_ATTEMPTS retries the page is marked failed (see failedPages). */
46
+ this.requestTimeoutMs = REQUEST_TIMEOUT_MS;
42
47
  // ── Internal state ─────────────────────────────────────────────────────────
43
48
  /** Index into allViews[] for the currently displayed view. 0 = primary, 1+ = alternatives. */
44
49
  this.activeViewIdx = 0;
45
50
  this.loadedPages = new Map();
46
51
  this.requestedPages = new Set();
52
+ /** Pages that timed out MAX_RETRY_ATTEMPTS times — rendered as a retry row instead of shimmer. */
53
+ this.failedPages = new Set();
47
54
  this.renderStart = 0;
48
55
  this.renderEnd = 0;
49
56
  this.colWidths = [];
@@ -79,13 +86,10 @@ export class MrdTable {
79
86
  this.minKnownTotal = 0;
80
87
  this.handleScroll = (e) => {
81
88
  const scroller = e.currentTarget;
82
- const scrollTop = scroller.scrollTop;
83
- const total = this.baseTotal;
84
- const visStart = Math.floor(scrollTop / this.rowHeight);
85
- const visEnd = Math.min(visStart + this.visibleCount(), total - 1);
86
- this.scrollTop = scrollTop;
87
- this.renderStart = Math.max(0, visStart - BUFFER);
88
- this.renderEnd = Math.min(total - 1, visEnd + BUFFER);
89
+ const { renderStart, renderEnd } = computeWindow(scroller.scrollTop, this.rowHeight, this.tableHeight, this.baseTotal);
90
+ this.scrollTop = scroller.scrollTop;
91
+ this.renderStart = renderStart;
92
+ this.renderEnd = renderEnd;
89
93
  this.requestPagesForWindow(this.renderStart, this.renderEnd);
90
94
  };
91
95
  }
@@ -105,18 +109,14 @@ export class MrdTable {
105
109
  var _a, _b, _c;
106
110
  this.applyDefaultSort((_c = (_b = (_a = this.item) === null || _a === void 0 ? void 0 : _a.view) === null || _b === void 0 ? void 0 : _b.defaultSort) !== null && _c !== void 0 ? _c : '');
107
111
  }
112
+ componentDidLoad() {
113
+ this.staleCheckTimer = setInterval(() => this.checkStalePages(), STALE_CHECK_INTERVAL_MS);
114
+ }
108
115
  // ── Helpers ────────────────────────────────────────────────────────────────
109
116
  applyDefaultSort(defaultSort) {
110
- var _a;
111
- if (defaultSort) {
112
- const parts = defaultSort.split(',');
113
- this.sortField = parts[0].trim();
114
- this.sortDir = ((_a = parts[1]) === null || _a === void 0 ? void 0 : _a.trim()) === 'desc' ? 'desc' : 'asc';
115
- }
116
- else {
117
- this.sortField = '';
118
- this.sortDir = 'asc';
119
- }
117
+ const { field, dir } = parseDefaultSort(defaultSort);
118
+ this.sortField = field;
119
+ this.sortDir = dir;
120
120
  }
121
121
  // ── Public API ─────────────────────────────────────────────────────────────
122
122
  /**
@@ -132,7 +132,11 @@ export class MrdTable {
132
132
  this.pendingPages.clear();
133
133
  this.loadedPages = new Map();
134
134
  this.requestedPages = new Set();
135
+ this.failedPages = new Set();
136
+ this.requestedAt.clear();
137
+ this.retryAttempts.clear();
135
138
  this.colWidths = [];
139
+ this.colWidthMeasureScheduled = false;
136
140
  this.scrollTop = 0;
137
141
  this.renderStart = 0;
138
142
  // Always fill the visible viewport on init — totalElements may be stale from a
@@ -146,7 +150,7 @@ export class MrdTable {
146
150
  this.aggregationsPending = true;
147
151
  this.minKnownTotal = 0;
148
152
  // Always request page 0 — totalElements may be unknown (0) on first load.
149
- this.mrdLoadPage.emit({ page: 0, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(0) });
153
+ this.emitLoadPage(0);
150
154
  this.requestedPages = new Set([0]);
151
155
  }
152
156
  /**
@@ -177,6 +181,13 @@ export class MrdTable {
177
181
  const next = new Map(this.loadedPages);
178
182
  next.set(pageNumber, rows);
179
183
  this.loadedPages = next;
184
+ this.requestedAt.delete(pageNumber);
185
+ this.retryAttempts.delete(pageNumber);
186
+ if (this.failedPages.has(pageNumber)) {
187
+ const nextFailed = new Set(this.failedPages);
188
+ nextFailed.delete(pageNumber);
189
+ this.failedPages = nextFailed;
190
+ }
180
191
  if (pageNumber === 0 && this.aggregationsPending) {
181
192
  this.aggregationsPending = false;
182
193
  const hasAggColumns = this.columns.some(c => c.type === 'FIELD' && c.aggregate);
@@ -195,6 +206,10 @@ export class MrdTable {
195
206
  }
196
207
  // ── Lifecycle ──────────────────────────────────────────────────────────────
197
208
  disconnectedCallback() {
209
+ if (this.staleCheckTimer !== null) {
210
+ clearInterval(this.staleCheckTimer);
211
+ this.staleCheckTimer = null;
212
+ }
198
213
  if (this.outsideClickHandler) {
199
214
  document.removeEventListener('click', this.outsideClickHandler);
200
215
  this.outsideClickHandler = null;
@@ -213,21 +228,34 @@ export class MrdTable {
213
228
  }
214
229
  }
215
230
  componentDidRender() {
216
- if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
217
- const ths = this.el.querySelectorAll('.mrd-table__header');
218
- if (ths.length > 0) {
219
- this.colWidths = Array.from(ths).map(th => th.offsetWidth);
220
- }
231
+ // Freeze header widths once the first data page has rendered. The measurement
232
+ // reads offsetWidth and writes the colWidths @State, which would trigger a
233
+ // re-render *during* the render cycle if done synchronously here. Defer it to a
234
+ // writeTask so the state mutation happens outside the render pass (avoids the
235
+ // Stencil "state/prop changed during rendering" warning and potential loops).
236
+ if (this.colWidths.length === 0 &&
237
+ !this.colWidthMeasureScheduled &&
238
+ this.loadedPages.size > 0 &&
239
+ this.totalElements > 0) {
240
+ this.colWidthMeasureScheduled = true;
241
+ writeTask(() => {
242
+ this.colWidthMeasureScheduled = false;
243
+ // Re-check: state may have changed (e.g. a reset) between scheduling and running.
244
+ if (this.colWidths.length !== 0)
245
+ return;
246
+ const ths = this.el.querySelectorAll('.mrd-table__header');
247
+ if (ths.length > 0) {
248
+ this.colWidths = Array.from(ths).map(th => th.offsetWidth);
249
+ }
250
+ });
221
251
  }
222
252
  }
223
253
  // ── Paging / scroll helpers ────────────────────────────────────────────────
224
254
  visibleCount() {
225
- return Math.ceil(this.tableHeight / this.rowHeight);
255
+ return visibleCount(this.tableHeight, this.rowHeight);
226
256
  }
227
257
  sortParam() {
228
- if (!this.sortField)
229
- return '';
230
- return this.sortDir === 'desc' ? `${this.sortField},desc` : this.sortField;
258
+ return buildSortParam(this.sortField, this.sortDir);
231
259
  }
232
260
  /** Stable ordered list: primary view first, then alternatives (from the item prop). */
233
261
  get allViews() {
@@ -253,14 +281,16 @@ export class MrdTable {
253
281
  * VIEW: /excel/{dataClass}
254
282
  * RELATED_VIEW: /excel/{fromClass}/{parentId}/{dataClass} */
255
283
  buildExcelPath() {
256
- var _a, _b, _c, _d;
284
+ var _a;
257
285
  const v = this.allViews[this.activeViewIdx];
258
286
  if (!v)
259
287
  return '';
260
- if (((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW') {
261
- return `/excel/${(_b = v.fromClass) !== null && _b !== void 0 ? _b : ''}/${this.parentId}/${(_c = v.dataClass) !== null && _c !== void 0 ? _c : ''}`;
262
- }
263
- return `/excel/${(_d = v.dataClass) !== null && _d !== void 0 ? _d : ''}`;
288
+ return '/excel' + buildListPath({
289
+ isRelatedView: ((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW',
290
+ fromClass: v.fromClass,
291
+ parentId: this.parentId,
292
+ dataClass: v.dataClass,
293
+ });
264
294
  }
265
295
  buildActionDetail(action, basicType) {
266
296
  var _a, _b, _c;
@@ -280,80 +310,28 @@ export class MrdTable {
280
310
  * VIEW: /{dataClass}
281
311
  * RELATED_VIEW: /{fromClass}/{parentId}/{dataClass} */
282
312
  buildDataPath() {
283
- var _a, _b, _c, _d;
313
+ var _a;
284
314
  const v = this.allViews[this.activeViewIdx];
285
315
  if (!v)
286
316
  return '';
287
- if (((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW') {
288
- return `/${(_b = v.fromClass) !== null && _b !== void 0 ? _b : ''}/${this.parentId}/${(_c = v.dataClass) !== null && _c !== void 0 ? _c : ''}`;
289
- }
290
- return `/${(_d = v.dataClass) !== null && _d !== void 0 ? _d : ''}`;
317
+ return buildListPath({
318
+ isRelatedView: ((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW',
319
+ fromClass: v.fromClass,
320
+ parentId: this.parentId,
321
+ dataClass: v.dataClass,
322
+ });
291
323
  }
292
324
  /** Build query params for a page request from current sort, view filters, filterClass and active column filters. */
293
325
  buildQueryParams(page) {
294
- var _a, _b, _c, _d, _e, _f, _g;
326
+ var _a;
295
327
  const v = this.allViews[this.activeViewIdx];
296
- const p = new URLSearchParams();
297
- if (page > 0)
298
- p.set('page', String(page));
299
- const sort = this.sortParam();
300
- if (sort)
301
- p.set('sort', sort);
302
- const filterClass = v === null || v === void 0 ? void 0 : v.filterClass;
303
- if (filterClass)
304
- p.set('type', filterClass);
305
- for (const f of ((_b = (_a = v === null || v === void 0 ? void 0 : v.view) === null || _a === void 0 ? void 0 : _a.filter) !== null && _b !== void 0 ? _b : [])) {
306
- if (!f.name)
307
- continue;
308
- if (f.operator === 'EMPTY') {
309
- p.set(f.name, '');
310
- continue;
311
- }
312
- if (f.operator === 'NOT_EMPTY') {
313
- p.set(f.name + '_notempty', 'true');
314
- continue;
315
- }
316
- if (f.operator === 'STARTS_WITH') {
317
- p.set(f.name + '_startswith', String((_c = f.value) !== null && _c !== void 0 ? _c : ''));
318
- continue;
319
- }
320
- if (f.operator === 'FROM') {
321
- p.set(f.name + '_from', String((_d = f.value) !== null && _d !== void 0 ? _d : ''));
322
- continue;
323
- }
324
- if (f.operator === 'TO') {
325
- p.set(f.name + '_to', String((_e = f.value) !== null && _e !== void 0 ? _e : ''));
326
- continue;
327
- }
328
- if (f.value != null) {
329
- p.set(f.name, String(f.value));
330
- }
331
- }
332
- for (const f of this.activeFilters.values()) {
333
- if (f.operator === 'isEmpty') {
334
- p.set(f.field, '');
335
- continue;
336
- }
337
- if (f.operator === 'isNotEmpty') {
338
- p.set(f.field + '_notempty', 'true');
339
- continue;
340
- }
341
- if (f.operator === 'startsWith') {
342
- p.set(f.field + '_startswith', String((_f = f.value) !== null && _f !== void 0 ? _f : ''));
343
- continue;
344
- }
345
- if ((_g = f.values) === null || _g === void 0 ? void 0 : _g.length) {
346
- p.set(f.field, f.values.join(','));
347
- continue;
348
- }
349
- if (f.value != null)
350
- p.set(f.field, String(f.value));
351
- if (f.from != null)
352
- p.set(f.field + '_from', String(f.from));
353
- if (f.to != null)
354
- p.set(f.field + '_to', String(f.to));
355
- }
356
- return p.toString();
328
+ return buildTableQueryParams({
329
+ page,
330
+ sort: this.sortParam(),
331
+ filterClass: v === null || v === void 0 ? void 0 : v.filterClass,
332
+ viewFilters: (_a = v === null || v === void 0 ? void 0 : v.view) === null || _a === void 0 ? void 0 : _a.filter,
333
+ activeFilters: this.activeFilters.values(),
334
+ });
357
335
  }
358
336
  get columns() {
359
337
  var _a, _b, _c;
@@ -408,11 +386,7 @@ export class MrdTable {
408
386
  isTotalKnown() {
409
387
  if (this.aggregationsTotal != null)
410
388
  return true;
411
- for (const rows of this.loadedPages.values()) {
412
- if (rows.length < this.pageSize)
413
- return true;
414
- }
415
- return false;
389
+ return isTotalKnownFromPages(this.loadedPages, this.pageSize);
416
390
  }
417
391
  /** Effective total: aggregations-response > totalElements prop > minKnownTotal from setPage(). */
418
392
  get baseTotal() {
@@ -423,61 +397,9 @@ export class MrdTable {
423
397
  return this.minKnownTotal;
424
398
  }
425
399
  // ── Aggregation helpers ────────────────────────────────────────────────────
426
- buildAggregationParams() {
427
- var _a;
428
- const groups = { sum: [], avg: [], count: [] };
429
- for (const col of this.columns) {
430
- if (col.type !== 'FIELD' || !col.aggregate)
431
- continue;
432
- const fn = col.aggregate.toLowerCase();
433
- if (fn in groups)
434
- groups[fn].push((_a = col.name) !== null && _a !== void 0 ? _a : '');
435
- }
436
- const params = {};
437
- if (groups.sum.length)
438
- params.sum = groups.sum;
439
- if (groups.avg.length)
440
- params.avg = groups.avg;
441
- if (groups.count.length)
442
- params.count = groups.count;
443
- return Object.keys(params).length > 0 ? params : null;
444
- }
445
- buildAggregationQs() {
446
- var _a, _b, _c;
447
- const p = new URLSearchParams(this.buildQueryParams(0));
448
- p.delete('page');
449
- p.delete('sort');
450
- const groups = this.buildAggregationParams();
451
- if ((_a = groups === null || groups === void 0 ? void 0 : groups.sum) === null || _a === void 0 ? void 0 : _a.length)
452
- p.set('sum', groups.sum.join(','));
453
- if ((_b = groups === null || groups === void 0 ? void 0 : groups.avg) === null || _b === void 0 ? void 0 : _b.length)
454
- p.set('avg', groups.avg.join(','));
455
- if ((_c = groups === null || groups === void 0 ? void 0 : groups.count) === null || _c === void 0 ? void 0 : _c.length)
456
- p.set('count', groups.count.join(','));
457
- return p.toString();
458
- }
459
400
  emitLoadAggregations() {
460
- this.mrdLoadAggregations.emit({ path: this.buildDataPath(), qs: this.buildQueryParams(0), aggQs: this.buildAggregationQs() });
461
- }
462
- renderAggregationValue(col) {
463
- var _a;
464
- if (col.type !== 'FIELD' || !col.aggregate || !this.aggregations)
465
- return '';
466
- const fn = col.aggregate.toLowerCase();
467
- const aggData = this.aggregations[fn];
468
- const val = typeof aggData === 'object' && aggData !== null
469
- ? aggData[(_a = col.name) !== null && _a !== void 0 ? _a : '']
470
- : undefined;
471
- if (val == null)
472
- return '';
473
- const dt = col.dataType;
474
- if (dt === 'INTEGER')
475
- return formatNumber(val, this.locale, { maximumFractionDigits: 0 });
476
- if (dt === 'PERCENTAGE')
477
- return formatPercentage(val, this.locale);
478
- if (dt === 'CURRENCY' && col.currencyCode)
479
- return formatCurrency(val, col.currencyCode, this.locale);
480
- return formatNumber(val, this.locale);
401
+ const qs = this.buildQueryParams(0);
402
+ this.mrdLoadAggregations.emit({ path: this.buildDataPath(), qs, aggQs: buildAggregationQs(qs, this.columns) });
481
403
  }
482
404
  // ── Reset pagination ───────────────────────────────────────────────────────
483
405
  /** Reset pagination state and scroll to top (used after sort or filter change). */
@@ -489,7 +411,11 @@ export class MrdTable {
489
411
  this.pendingPages.clear();
490
412
  this.loadedPages = new Map();
491
413
  this.requestedPages = new Set();
414
+ this.failedPages = new Set();
415
+ this.requestedAt.clear();
416
+ this.retryAttempts.clear();
492
417
  this.colWidths = [];
418
+ this.colWidthMeasureScheduled = false;
493
419
  this.scrollTop = 0;
494
420
  this.renderStart = 0;
495
421
  this.minKnownTotal = 0;
@@ -518,15 +444,19 @@ export class MrdTable {
518
444
  this.resetPages();
519
445
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
520
446
  }
447
+ /** Emits mrdLoadPage for a page and records the request timestamp for stale-request detection. */
448
+ emitLoadPage(page) {
449
+ this.mrdLoadPage.emit({ page, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(page) });
450
+ this.requestedAt.set(page, Date.now());
451
+ }
521
452
  emitPagesForWindow(start, end) {
522
- const firstPage = Math.floor(start / this.pageSize);
523
- const lastPage = Math.floor(end / this.pageSize);
453
+ const { firstPage, lastPage } = pageRange(start, end, this.pageSize);
524
454
  const next = new Set(this.requestedPages);
525
455
  let changed = false;
526
456
  for (let p = firstPage; p <= lastPage; p++) {
527
457
  if (!this.loadedPages.has(p) && !next.has(p)) {
528
458
  next.add(p);
529
- this.mrdLoadPage.emit({ page: p, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(p) });
459
+ this.emitLoadPage(p);
530
460
  changed = true;
531
461
  }
532
462
  }
@@ -534,13 +464,10 @@ export class MrdTable {
534
464
  this.requestedPages = next;
535
465
  }
536
466
  getRow(i) {
537
- var _a;
538
- const page = this.loadedPages.get(Math.floor(i / this.pageSize));
539
- return (_a = page === null || page === void 0 ? void 0 : page[i % this.pageSize]) !== null && _a !== void 0 ? _a : null;
467
+ return getRowFromPages(this.loadedPages, i, this.pageSize);
540
468
  }
541
469
  requestPagesForWindow(start, end) {
542
- const firstPage = Math.floor(start / this.pageSize);
543
- const lastPage = Math.floor(end / this.pageSize);
470
+ const { firstPage, lastPage } = pageRange(start, end, this.pageSize);
544
471
  let anyNew = false;
545
472
  for (let p = firstPage; p <= lastPage; p++) {
546
473
  if (!this.loadedPages.has(p) && !this.requestedPages.has(p) && !this.pendingPages.has(p)) {
@@ -568,13 +495,53 @@ export class MrdTable {
568
495
  if (pageEnd < this.renderStart || pageStart > this.renderEnd)
569
496
  continue;
570
497
  next.add(page);
571
- this.mrdLoadPage.emit({ page, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(page) });
498
+ this.emitLoadPage(page);
572
499
  changed = true;
573
500
  }
574
501
  this.pendingPages.clear();
575
502
  if (changed)
576
503
  this.requestedPages = next;
577
504
  }
505
+ /** Scans outstanding requests for staleness: retries up to MAX_RETRY_ATTEMPTS times,
506
+ * then gives up and marks the page as failed (rendered as a retry row). */
507
+ checkStalePages() {
508
+ var _a;
509
+ if (this.requestedAt.size === 0)
510
+ return;
511
+ const stale = getStalePages(this.requestedAt, Date.now(), this.requestTimeoutMs)
512
+ .filter(p => !this.loadedPages.has(p));
513
+ if (stale.length === 0)
514
+ return;
515
+ let nextFailed = null;
516
+ for (const page of stale) {
517
+ const attempts = (_a = this.retryAttempts.get(page)) !== null && _a !== void 0 ? _a : 0;
518
+ if (attempts < MAX_RETRY_ATTEMPTS) {
519
+ this.retryAttempts.set(page, attempts + 1);
520
+ this.emitLoadPage(page);
521
+ }
522
+ else {
523
+ this.requestedAt.delete(page);
524
+ this.retryAttempts.delete(page);
525
+ if (!nextFailed)
526
+ nextFailed = new Set(this.failedPages);
527
+ nextFailed.add(page);
528
+ this.mrdPageError.emit({ page });
529
+ }
530
+ }
531
+ if (nextFailed)
532
+ this.failedPages = nextFailed;
533
+ }
534
+ /** Manually retry a page the user gave up on (clicked the retry row). */
535
+ retryPage(page) {
536
+ const next = new Set(this.failedPages);
537
+ next.delete(page);
538
+ this.failedPages = next;
539
+ this.retryAttempts.delete(page);
540
+ this.emitLoadPage(page);
541
+ const requested = new Set(this.requestedPages);
542
+ requested.add(page);
543
+ this.requestedPages = requested;
544
+ }
578
545
  // ── Filter helpers ─────────────────────────────────────────────────────────
579
546
  handleFilterToggle() {
580
547
  this.filterMode = !this.filterMode;
@@ -602,17 +569,7 @@ export class MrdTable {
602
569
  // If from and to cover the same local day it was an exact-date filter — restore
603
570
  // to exact mode so the user sees the single-date input again.
604
571
  if (dataType === 'DATETIME' && existing && existing.operator !== 'isEmpty' && existing.operator !== 'isNotEmpty') {
605
- const display = Object.assign({}, existing);
606
- if (typeof display.from === 'string' && display.from)
607
- display.from = this.utcISOToLocalDate(display.from);
608
- if (typeof display.to === 'string' && display.to)
609
- display.to = this.utcISOToLocalDateExclusiveEnd(display.to);
610
- if (display.from && display.to && display.from === display.to) {
611
- this.pendingFilter = Object.assign(Object.assign({}, display), { value: display.from, from: undefined, to: undefined });
612
- }
613
- else {
614
- this.pendingFilter = display;
615
- }
572
+ this.pendingFilter = datetimeFilterToDisplay(existing);
616
573
  }
617
574
  else {
618
575
  this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType, operator: defaultOperator };
@@ -678,81 +635,15 @@ export class MrdTable {
678
635
  const current = (_b = (_a = this.pendingFilter) === null || _a === void 0 ? void 0 : _a.values) !== null && _b !== void 0 ? _b : [];
679
636
  this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { values: checked ? [...current, key] : current.filter(k => k !== key) });
680
637
  }
681
- filterHasValue(f) {
682
- if (f.operator === 'isEmpty' || f.operator === 'isNotEmpty')
683
- return true;
684
- if (f.values !== undefined && f.values.length > 0)
685
- return true;
686
- if (f.value != null && f.value !== '')
687
- return true;
688
- if (typeof f.value === 'boolean')
689
- return true;
690
- if (f.from != null && f.from !== '')
691
- return true;
692
- if (f.to != null && f.to !== '')
693
- return true;
694
- return false;
695
- }
696
- // Convert a local "YYYY-MM-DD" date string to the UTC ISO string at the
697
- // start of that local day (midnight). new Date(y, m, d) uses local time.
698
- dateLocalToUTCStart(dateStr) {
699
- if (!dateStr)
700
- return dateStr;
701
- const [year, month, day] = dateStr.split('-').map(Number);
702
- return new Date(year, month - 1, day).toISOString().replace(/\.\d{3}Z$/, 'Z');
703
- }
704
- // Start of the day AFTER the given local date (exclusive range end).
705
- dateLocalToUTCEndExclusive(dateStr) {
706
- if (!dateStr)
707
- return dateStr;
708
- const [year, month, day] = dateStr.split('-').map(Number);
709
- return new Date(year, month - 1, day + 1).toISOString().replace(/\.\d{3}Z$/, 'Z');
710
- }
711
- // Convert a stored UTC ISO string back to the local "YYYY-MM-DD" date.
712
- utcISOToLocalDate(utcStr) {
713
- if (!utcStr)
714
- return utcStr;
715
- const d = new Date(utcStr);
716
- if (isNaN(d.getTime()))
717
- return utcStr;
718
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
719
- }
720
- // The stored "to" value is the exclusive end (midnight of the next day).
721
- // Subtract one day to recover the local date the user entered.
722
- utcISOToLocalDateExclusiveEnd(utcStr) {
723
- if (!utcStr)
724
- return utcStr;
725
- const d = new Date(utcStr);
726
- if (isNaN(d.getTime()))
727
- return utcStr;
728
- d.setDate(d.getDate() - 1);
729
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
730
- }
731
638
  applyFilter() {
732
639
  const f = this.pendingFilter;
733
640
  if (!(f === null || f === void 0 ? void 0 : f.field)) {
734
641
  this.closeFilterPopup();
735
642
  return;
736
643
  }
737
- // For DATETIME fields the user enters local dates; convert to UTC ISO strings.
738
- // Exact date → range covering the full local day (from = midnight, to = next midnight).
739
- // "to" is always the exclusive end (midnight of the next local day).
740
- let normalized = Object.assign({}, f);
741
- if (f.dataType === 'DATETIME' && f.operator !== 'isEmpty' && f.operator !== 'isNotEmpty') {
742
- if (typeof normalized.value === 'string' && normalized.value) {
743
- normalized.from = this.dateLocalToUTCStart(normalized.value);
744
- normalized.to = this.dateLocalToUTCEndExclusive(normalized.value);
745
- normalized.value = undefined;
746
- }
747
- else {
748
- if (typeof normalized.from === 'string' && normalized.from)
749
- normalized.from = this.dateLocalToUTCStart(normalized.from);
750
- if (typeof normalized.to === 'string' && normalized.to)
751
- normalized.to = this.dateLocalToUTCEndExclusive(normalized.to);
752
- }
753
- }
644
+ const normalized = normalizeDatetimeFilter(f);
754
645
  const next = new Map(this.activeFilters);
755
- if (this.filterHasValue(normalized)) {
646
+ if (filterHasValue(normalized)) {
756
647
  next.set(normalized.field, normalized);
757
648
  }
758
649
  else {
@@ -807,140 +698,53 @@ export class MrdTable {
807
698
  toggleViewPopover(e) {
808
699
  e.stopPropagation();
809
700
  if (this.viewPopoverOpen) {
810
- this.viewPopoverOpen = false;
811
- if (this.viewPopoverClickHandler) {
812
- document.removeEventListener('click', this.viewPopoverClickHandler);
813
- this.viewPopoverClickHandler = null;
814
- }
701
+ this.closeViewPopover();
815
702
  return;
816
703
  }
817
704
  this.viewPopoverOpen = true;
818
- this.viewPopoverClickHandler = () => {
819
- this.viewPopoverOpen = false;
705
+ this.viewPopoverClickHandler = () => this.closeViewPopover();
706
+ document.addEventListener('click', this.viewPopoverClickHandler);
707
+ }
708
+ closeViewPopover() {
709
+ this.viewPopoverOpen = false;
710
+ if (this.viewPopoverClickHandler) {
820
711
  document.removeEventListener('click', this.viewPopoverClickHandler);
821
712
  this.viewPopoverClickHandler = null;
822
- };
823
- document.addEventListener('click', this.viewPopoverClickHandler);
713
+ }
824
714
  }
825
715
  // ── Render: toolbar ────────────────────────────────────────────────────────
826
716
  renderToolbar() {
827
717
  var _a;
828
- const filterCount = this.activeFilters.size;
829
- const actions = this.tableActions;
830
- const allViews = this.allViews;
831
- const hasActions = actions.length > 0;
832
- const hasViewSwitcher = allViews.length > 1;
833
- 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" }, h("div", { class: "mrd-table__view-switcher" }, h("button", { class: "mrd-table__view-arrow", "aria-label": "Previous view", onClick: () => this.handleViewSwitch((this.activeViewIdx - 1 + allViews.length) % allViews.length) }, "\u25C0"), h("div", { class: "mrd-table__view-title-wrap" }, h("button", { class: "mrd-table__view-title", onClick: (e) => this.toggleViewPopover(e) }, (_a = allViews[this.activeViewIdx]) === null || _a === void 0 ? void 0 : _a.label), this.viewPopoverOpen && (h("div", { class: "mrd-table__view-popover", onClick: (e) => e.stopPropagation() }, allViews.map((v, i) => (h("button", { key: String(i), class: `mrd-table__view-popover-item${i === this.activeViewIdx ? ' mrd-table__view-popover-item--active' : ''}`, onClick: () => {
834
- if (i !== this.activeViewIdx)
835
- this.handleViewSwitch(i);
836
- this.viewPopoverOpen = false;
837
- if (this.viewPopoverClickHandler) {
838
- document.removeEventListener('click', this.viewPopoverClickHandler);
839
- this.viewPopoverClickHandler = null;
840
- }
841
- } }, v.label)))))), h("button", { class: "mrd-table__view-arrow", "aria-label": "Next view", onClick: () => this.handleViewSwitch((this.activeViewIdx + 1) % allViews.length) }, "\u25B6")))), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, actions.map(a => {
842
- var _a, _b, _c;
843
- const createTypes = a.action === 'create' ? (_a = this.item) === null || _a === void 0 ? void 0 : _a.createTypes : null;
844
- if (createTypes === null || createTypes === void 0 ? void 0 : createTypes.length) {
845
- return (h("div", { class: "mrd-table__create-picker-wrap", key: `action-${a.action}` }, h("button", { class: `mrd-table__action mrd-table__action--${(_b = a.variant) !== null && _b !== void 0 ? _b : 'secondary'}`, onClick: () => this.toggleCreatePicker() }, a.icon
846
- ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
847
- : a.label, h("span", { class: "mrd-table__action-tooltip" }, a.label)), this.createPickerOpen && (h("div", { class: "mrd-table__create-picker" }, createTypes.map(ct => (h("button", { key: ct.type, class: "mrd-table__create-picker-item", onClick: () => {
848
- this.mrdAction.emit(this.buildActionDetail('create', ct.type));
849
- this.closeCreatePicker();
850
- } }, ct.label)))))));
851
- }
852
- return (h("button", { key: `action-${a.action}`, class: `mrd-table__action mrd-table__action--${(_c = a.variant) !== null && _c !== void 0 ? _c : 'secondary'}`, disabled: a.disabled, onClick: () => this.mrdAction.emit(this.buildActionDetail(a.action)) }, a.icon
853
- ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
854
- : a.label, h("span", { class: "mrd-table__action-tooltip" }, a.label)));
855
- })))));
718
+ return (h(Toolbar, { locale: this.locale, filterMode: this.filterMode, 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: {
719
+ onFilterToggle: () => this.handleFilterToggle(),
720
+ onClearAllFilters: () => this.clearAllFilters(),
721
+ onViewSwitch: idx => this.handleViewSwitch(idx),
722
+ onToggleViewPopover: e => this.toggleViewPopover(e),
723
+ onCloseViewPopover: () => this.closeViewPopover(),
724
+ onToggleCreatePicker: () => this.toggleCreatePicker(),
725
+ onCreate: basicType => {
726
+ this.mrdAction.emit(this.buildActionDetail('create', basicType));
727
+ this.closeCreatePicker();
728
+ },
729
+ onAction: action => this.mrdAction.emit(this.buildActionDetail(action)),
730
+ } }));
856
731
  }
857
732
  // ── Render: filter popup ───────────────────────────────────────────────────
858
- renderFilterEditor(col) {
859
- var _a, _b, _c, _d, _e, _f, _g;
860
- const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
861
- const dataType = this.colDataType(col);
862
- if (NO_FILTER_TYPES.has(dataType)) {
863
- return h("p", { class: "mrd-table__filter-no-support" }, t('filter_no_support', this.locale));
864
- }
865
- if (dataType === 'BOOLEAN') {
866
- const boolOp = pf.operator;
867
- const noValueOp = boolOp === 'isEmpty' || boolOp === 'isNotEmpty';
868
- return (h("div", { class: "mrd-table__filter-radio-group" }, [
869
- { labelKey: 'filter_all', value: null },
870
- { labelKey: 'yes', value: true },
871
- { labelKey: 'no', value: false },
872
- ].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))));
873
- }
874
- if (dataType === 'LIST') {
875
- const items = (_b = col.listItems) !== null && _b !== void 0 ? _b : [];
876
- const selected = (_c = pf.values) !== null && _c !== void 0 ? _c : [];
877
- return (h("div", { class: "mrd-table__filter-list" }, h("div", { class: "mrd-table__filter-list-controls" }, h("button", { class: "mrd-table__filter-list-btn", onClick: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { values: items.map(i => i.key) }); } }, t('filter_select_all', this.locale)), h("button", { class: "mrd-table__filter-list-btn", onClick: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { values: [] }); } }, t('filter_select_none', this.locale))), items.map(item => (h("label", { class: "mrd-table__filter-checkbox-label" }, h("input", { type: "checkbox", checked: selected.includes(item.key), onChange: (e) => this.togglePendingValue(item.key, e.target.checked) }), item.label)))));
878
- }
879
- if (TEXT_TYPES.has(dataType) || dataType === 'RELATION') {
880
- const op = (_d = pf.operator) !== null && _d !== void 0 ? _d : 'startsWith';
881
- const noInput = op === 'isEmpty' || op === 'isNotEmpty';
882
- return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => this.setPending('operator', e.target.value) }, [
883
- { val: 'startsWith', labelKey: 'filter_starts_with' },
884
- { val: 'equals', labelKey: 'filter_equals' },
885
- { val: 'isEmpty', labelKey: 'filter_is_empty' },
886
- { val: 'isNotEmpty', labelKey: 'filter_is_not_empty' },
887
- ].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((_e = pf.value) !== null && _e !== void 0 ? _e : ''), placeholder: t('filter_search_value', this.locale), onInput: (e) => this.setPending('value', e.target.value) }))));
888
- }
889
- if (NUMERIC_TYPES.has(dataType)) {
890
- const numOp = pf.operator;
891
- const noInput = numOp === 'isEmpty' || numOp === 'isNotEmpty';
892
- const rangeMode = !noInput && (pf.from !== undefined || pf.to !== undefined);
893
- return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => {
894
- const val = e.target.value;
895
- if (val === 'isEmpty' || val === 'isNotEmpty') {
896
- this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: val, value: undefined, from: undefined, to: undefined });
897
- }
898
- else {
899
- this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
900
- }
901
- } }, 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) })))))));
902
- }
903
- if (dataType === 'DATETIME') {
904
- const dtOp = pf.operator;
905
- const noInput = dtOp === 'isEmpty' || dtOp === 'isNotEmpty';
906
- const rangeMode = !noInput && (pf.from !== undefined || pf.to !== undefined);
907
- return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => {
908
- const val = e.target.value;
909
- if (val === 'isEmpty' || val === 'isNotEmpty') {
910
- this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: val, value: undefined, from: undefined, to: undefined });
911
- }
912
- else {
913
- this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
914
- }
915
- } }, 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((_f = pf.value) !== null && _f !== void 0 ? _f : ''), 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) })))))));
916
- }
917
- if (DATE_TYPES.has(dataType)) {
918
- const inputType = dataType === 'DATE' ? 'date' : 'time';
919
- const dtdOp = pf.operator;
920
- const noInput = dtdOp === 'isEmpty' || dtdOp === 'isNotEmpty';
921
- const rangeMode = !noInput && (pf.from !== undefined || pf.to !== undefined);
922
- return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => {
923
- const val = e.target.value;
924
- if (val === 'isEmpty' || val === 'isNotEmpty') {
925
- this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: val, value: undefined, from: undefined, to: undefined });
926
- }
927
- else {
928
- this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
929
- }
930
- } }, 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((_g = pf.value) !== null && _g !== void 0 ? _g : ''), 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) })))))));
931
- }
932
- return null;
933
- }
934
733
  renderFilterPopup() {
935
- var _a;
936
734
  if (!this.openFilterCol || !this.pendingFilter)
937
735
  return null;
938
736
  const col = this.columns.find(c => this.colName(c) === this.openFilterCol);
939
737
  if (!col)
940
738
  return null;
941
- const label = (_a = col.label) !== null && _a !== void 0 ? _a : this.openFilterCol;
942
- const sortActive = this.sortField === this.openFilterCol;
943
- return (h("div", { class: "mrd-table__filter-popup", style: { top: `${this.popupPos.top}px`, left: `${this.popupPos.left}px` }, onClick: (e) => e.stopPropagation() }, h("div", { class: "mrd-table__filter-popup-header" }, h("span", { class: "mrd-table__filter-popup-title" }, label), h("button", { class: "mrd-table__filter-close", onClick: () => this.closeFilterPopup() }, "\u2715")), h("div", { class: "mrd-table__filter-section" }, h("div", { class: "mrd-table__filter-section-label" }, t('filter_sorting', this.locale)), h("div", { class: "mrd-table__filter-sort-buttons" }, h("button", { class: `mrd-table__filter-sort-btn${sortActive && this.sortDir === 'asc' ? ' mrd-table__filter-sort-btn--active' : ''}`, onClick: () => this.applySort(col, 'asc') }, "\u25B2 ", t('filter_ascending', this.locale)), h("button", { class: `mrd-table__filter-sort-btn${sortActive && this.sortDir === 'desc' ? ' mrd-table__filter-sort-btn--active' : ''}`, onClick: () => this.applySort(col, 'desc') }, "\u25BC ", t('filter_descending', this.locale)))), h("div", { class: "mrd-table__filter-divider" }), h("div", { class: "mrd-table__filter-section" }, h("div", { class: "mrd-table__filter-section-label" }, t('filter_section', this.locale)), this.renderFilterEditor(col)), h("div", { class: "mrd-table__filter-popup-footer" }, h("button", { class: "mrd-table__filter-btn mrd-table__filter-btn--clear", onClick: () => this.clearFilter() }, t('filter_clear', this.locale)), h("button", { class: "mrd-table__filter-btn mrd-table__filter-btn--apply", onClick: () => this.applyFilter() }, t('filter_apply', this.locale)))));
739
+ return (h(FilterPopup, { col: col, dataType: this.colDataType(col), openFilterCol: this.openFilterCol, pendingFilter: this.pendingFilter, popupPos: this.popupPos, sortField: this.sortField, sortDir: this.sortDir, locale: this.locale, cb: {
740
+ onSetPending: (key, val) => this.setPending(key, val),
741
+ onReplacePending: pf => (this.pendingFilter = pf),
742
+ onToggleValue: (key, checked) => this.togglePendingValue(key, checked),
743
+ onApplySort: dir => this.applySort(col, dir),
744
+ onApply: () => this.applyFilter(),
745
+ onClear: () => this.clearFilter(),
746
+ onClose: () => this.closeFilterPopup(),
747
+ } }));
944
748
  }
945
749
  // ── Render: footer ────────────────────────────────────────────────────────
946
750
  renderFooter(rowCount, effectiveTotal, isTotalKnown = true) {
@@ -965,54 +769,11 @@ export class MrdTable {
965
769
  }
966
770
  // ── Render: cell ──────────────────────────────────────────────────────────
967
771
  renderCell(col, row) {
968
- var _a, _b, _c;
969
- const numericTypes = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
970
- const dataType = (_a = col.dataType) !== null && _a !== void 0 ? _a : '';
971
- const isNumeric = col.type === 'FIELD' && numericTypes.has(dataType);
972
- const isFile = col.type === 'FIELD' && (dataType === 'FILE' || dataType === 'IMAGE');
973
- if (isFile) {
974
- const name = (_b = col.name) !== null && _b !== void 0 ? _b : '';
975
- const fileVal = row === null || row === void 0 ? void 0 : row[name];
976
- const href = fileVal === null || fileVal === void 0 ? void 0 : fileVal.href;
977
- const fileName = fileVal === null || fileVal === void 0 ? void 0 : fileVal.fileName;
978
- return (h("td", { class: "mrd-table__cell" }, href && fileName ? (h("button", { class: "mrd-table__file-btn", title: fileName, onClick: (e) => {
979
- e.stopPropagation();
980
- this.mrdDownload.emit({ href, fileName });
981
- } }, h("svg", { class: "mrd-table__file-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13zm-3 8l-3-3 1.41-1.41L10 14.17l4.59-4.58L16 11l-6 6z" })), t('download', this.locale))) : ''));
982
- }
983
- if (dataType === 'JSON') {
984
- const name = (_c = col.name) !== null && _c !== void 0 ? _c : '';
985
- const rawValue = name ? row === null || row === void 0 ? void 0 : row[name] : undefined;
986
- if (rawValue == null || rawValue === '')
987
- return h("td", { class: "mrd-table__cell" });
988
- const compactHtml = CellRenderer.formatJson(rawValue, true);
989
- const prettyHtml = CellRenderer.formatJson(rawValue, false);
990
- return (h("td", { class: "mrd-table__cell" }, h("span", { class: "mrd-table__json-preview", innerHTML: compactHtml }), h("button", { class: "mrd-table__textblock-btn", onClick: (e) => { e.stopPropagation(); this.openJsonModal(prettyHtml); }, "aria-label": t('textblock_show_more', this.locale) }, "\u22EF")));
991
- }
992
- const TEXTBLOCK_MAX = 200;
993
- if (dataType === 'TEXTBLOCK') {
994
- const full = CellRenderer.render(col, row, this.locale);
995
- if (full.length <= TEXTBLOCK_MAX) {
996
- return h("td", { class: "mrd-table__cell" }, full);
997
- }
998
- const preview = full.slice(0, TEXTBLOCK_MAX) + '…';
999
- return (h("td", { class: "mrd-table__cell" }, preview, h("button", { class: "mrd-table__textblock-btn", onClick: (e) => { e.stopPropagation(); this.openTextblockModal(full); }, "aria-label": t('textblock_show_more', this.locale) }, "\u22EF")));
1000
- }
1001
- const value = CellRenderer.render(col, row, this.locale);
1002
- return (h("td", { class: `mrd-table__cell${isNumeric ? ' mrd-table__cell--numeric' : ''}` }, value));
772
+ return (h(TableCell, { col: col, row: row, locale: this.locale, onDownload: detail => this.mrdDownload.emit(detail), onOpenTextblock: full => this.openTextblockModal(full), onOpenJson: html => this.openJsonModal(html) }));
1003
773
  }
1004
774
  // ── Render: totals row ────────────────────────────────────────────────────
1005
775
  renderTotalsRow() {
1006
- if (!this.aggregations)
1007
- return null;
1008
- if (!this.columns.some(c => c.type === 'FIELD' && c.aggregate))
1009
- return null;
1010
- return (h("tfoot", null, h("tr", { class: "mrd-table__totals-row" }, this.columns.map(col => {
1011
- var _a;
1012
- const val = this.renderAggregationValue(col);
1013
- const isNumeric = col.type === 'FIELD' && NUMERIC_TYPES.has((_a = col.dataType) !== null && _a !== void 0 ? _a : '');
1014
- return (h("td", { class: `mrd-table__totals-cell${isNumeric ? ' mrd-table__totals-cell--numeric' : ''}` }, val));
1015
- }))));
776
+ return h(TotalsRow, { columns: this.columns, aggregations: this.aggregations, locale: this.locale });
1016
777
  }
1017
778
  // ── Render ─────────────────────────────────────────────────────────────────
1018
779
  render() {
@@ -1032,7 +793,7 @@ export class MrdTable {
1032
793
  isFiltered ? 'mrd-table__header--filtered' : '',
1033
794
  this.filterMode ? 'mrd-table__header--sortable' : '',
1034
795
  ].filter(Boolean).join(' ');
1035
- return (h("th", { class: cls, onClick: this.filterMode ? (e) => this.handleFilterOpen(col, e) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isFiltered && this.renderFilterIcon()));
796
+ return (h("th", { class: cls, onClick: this.filterMode ? (e) => this.handleFilterOpen(col, e) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isFiltered && h(FilterIcon, null)));
1036
797
  }))), h("tbody", null, (_b = this.rows) === null || _b === void 0 ? void 0 : _b.map((row, i) => (h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => this.renderCell(col, row)))))), this.renderTotalsRow()), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale)))), this.renderFooter((_c = this.rows) === null || _c === void 0 ? void 0 : _c.length), this.renderFilterPopup(), this.renderTextblockModal(), this.renderJsonModal()));
1037
798
  }
1038
799
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
@@ -1040,12 +801,7 @@ export class MrdTable {
1040
801
  // if any loaded page is shorter than pageSize it is the last page,
1041
802
  // so the true total cannot exceed (pageNum * pageSize + pageRows.length).
1042
803
  // aggregationsTotal (from setAggregations) takes priority over the totalElements prop.
1043
- let effectiveTotal = this.baseTotal;
1044
- for (const [pageNum, pageRows] of this.loadedPages) {
1045
- if (pageRows.length < this.pageSize) {
1046
- effectiveTotal = Math.min(effectiveTotal, pageNum * this.pageSize + pageRows.length);
1047
- }
1048
- }
804
+ const effectiveTotal = computeEffectiveTotal(this.loadedPages, this.pageSize, this.baseTotal);
1049
805
  // Clamp renderEnd to what we actually know exists (-1 when empty)
1050
806
  const clampedEnd = Math.min(this.renderEnd, effectiveTotal - 1);
1051
807
  const colCount = this.columns.length;
@@ -1058,7 +814,13 @@ export class MrdTable {
1058
814
  for (let i = this.renderStart; i <= clampedEnd; i++) {
1059
815
  const row = this.getRow(i);
1060
816
  if (row === null) {
1061
- renderedRows.push(h("tr", { class: "mrd-table__row mrd-table__row--loading" }, h("td", { class: "mrd-table__cell--placeholder", colSpan: colCount }, h("span", { class: "mrd-table__placeholder-bar" }))));
817
+ const page = Math.floor(i / this.pageSize);
818
+ if (this.failedPages.has(page)) {
819
+ renderedRows.push(h("tr", { class: "mrd-table__row mrd-table__row--failed" }, h("td", { class: "mrd-table__cell--failed", colSpan: colCount }, h("button", { type: "button", class: "mrd-table__retry-button", onClick: () => this.retryPage(page) }, t('load_failed_retry', this.locale)))));
820
+ }
821
+ else {
822
+ renderedRows.push(h("tr", { class: "mrd-table__row mrd-table__row--loading" }, h("td", { class: "mrd-table__cell--placeholder", colSpan: colCount }, h("span", { class: "mrd-table__placeholder-bar" }))));
823
+ }
1062
824
  }
1063
825
  else {
1064
826
  renderedRows.push(h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => this.renderCell(col, row))));
@@ -1076,21 +838,14 @@ export class MrdTable {
1076
838
  isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
1077
839
  isFiltered ? 'mrd-table__header--filtered' : '',
1078
840
  ].filter(Boolean).join(' ');
1079
- return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? ((e) => this.filterMode ? this.handleFilterOpen(col, e) : this.handleSortClick(col)) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isInteractive && isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), isInteractive && !isActive && !this.filterMode && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isInteractive && isFiltered && this.renderFilterIcon()));
1080
- }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))), this.renderTotalsRow())), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal, this.isTotalKnown()), this.renderFilterPopup(), this.renderTextblockModal()));
1081
- }
1082
- renderFilterIcon() {
1083
- return (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" }))));
841
+ return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? ((e) => this.filterMode ? this.handleFilterOpen(col, e) : this.handleSortClick(col)) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isInteractive && isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), isInteractive && !isActive && !this.filterMode && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isInteractive && isFiltered && h(FilterIcon, null)));
842
+ }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))), this.renderTotalsRow())), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal, this.isTotalKnown()), this.renderFilterPopup(), this.renderTextblockModal(), this.renderJsonModal()));
1084
843
  }
1085
844
  renderTextblockModal() {
1086
- if (this.textblockModal === null)
1087
- return null;
1088
- return (h("div", { class: "mrd-table__modal-backdrop", onClick: () => this.closeTextblockModal(), role: "dialog", "aria-modal": "true" }, h("div", { class: "mrd-table__modal", onClick: (e) => e.stopPropagation() }, h("button", { class: "mrd-table__modal-close", onClick: () => this.closeTextblockModal(), "aria-label": t('close', this.locale) }, "\u2715"), h("p", { class: "mrd-table__modal-text" }, this.textblockModal))));
845
+ return h(TextblockModal, { text: this.textblockModal, locale: this.locale, onClose: () => this.closeTextblockModal() });
1089
846
  }
1090
847
  renderJsonModal() {
1091
- if (this.jsonModal === null)
1092
- return null;
1093
- return (h("div", { class: "mrd-table__modal-backdrop", onClick: () => this.closeJsonModal(), role: "dialog", "aria-modal": "true" }, h("div", { class: "mrd-table__modal", onClick: (e) => e.stopPropagation() }, h("button", { class: "mrd-table__modal-close", onClick: () => this.closeJsonModal(), "aria-label": t('close', this.locale) }, "\u2715"), h("pre", { class: "mrd-table__modal-json", innerHTML: this.jsonModal }))));
848
+ return h(JsonModal, { html: this.jsonModal, locale: this.locale, onClose: () => this.closeJsonModal() });
1094
849
  }
1095
850
  static get is() { return "mrd-table"; }
1096
851
  static get encapsulation() { return "scoped"; }
@@ -1115,7 +870,7 @@ export class MrdTable {
1115
870
  "references": {
1116
871
  "ClientLayoutItem": {
1117
872
  "location": "import",
1118
- "path": "../../types/client-layout",
873
+ "path": "../../../types/client-layout",
1119
874
  "id": "src/types/client-layout.ts::ClientLayoutItem",
1120
875
  "referenceLocation": "ClientLayoutItem"
1121
876
  }
@@ -1273,6 +1028,26 @@ export class MrdTable {
1273
1028
  "reflect": false,
1274
1029
  "attribute": "table-height",
1275
1030
  "defaultValue": "500"
1031
+ },
1032
+ "requestTimeoutMs": {
1033
+ "type": "number",
1034
+ "mutable": false,
1035
+ "complexType": {
1036
+ "original": "number",
1037
+ "resolved": "number",
1038
+ "references": {}
1039
+ },
1040
+ "required": false,
1041
+ "optional": false,
1042
+ "docs": {
1043
+ "tags": [],
1044
+ "text": "Time (ms) to wait for setPage() before retrying a page request; after\nMAX_RETRY_ATTEMPTS retries the page is marked failed (see failedPages)."
1045
+ },
1046
+ "getter": false,
1047
+ "setter": false,
1048
+ "reflect": false,
1049
+ "attribute": "request-timeout-ms",
1050
+ "defaultValue": "REQUEST_TIMEOUT_MS"
1276
1051
  }
1277
1052
  };
1278
1053
  }
@@ -1281,6 +1056,7 @@ export class MrdTable {
1281
1056
  "activeViewIdx": {},
1282
1057
  "loadedPages": {},
1283
1058
  "requestedPages": {},
1059
+ "failedPages": {},
1284
1060
  "renderStart": {},
1285
1061
  "renderEnd": {},
1286
1062
  "colWidths": {},
@@ -1317,6 +1093,21 @@ export class MrdTable {
1317
1093
  "resolved": "{ page: number; sort: string; path: string; qs: string; }",
1318
1094
  "references": {}
1319
1095
  }
1096
+ }, {
1097
+ "method": "mrdPageError",
1098
+ "name": "mrdPageError",
1099
+ "bubbles": true,
1100
+ "cancelable": true,
1101
+ "composed": true,
1102
+ "docs": {
1103
+ "tags": [],
1104
+ "text": "Fired when a page request timed out MAX_RETRY_ATTEMPTS times without setPage() being called.\nPurely informational \u2014 the table shows its own retry row regardless of whether a host listens."
1105
+ },
1106
+ "complexType": {
1107
+ "original": "{ page: number }",
1108
+ "resolved": "{ page: number; }",
1109
+ "references": {}
1110
+ }
1320
1111
  }, {
1321
1112
  "method": "mrdRowClick",
1322
1113
  "name": "mrdRowClick",
@@ -1455,7 +1246,7 @@ export class MrdTable {
1455
1246
  },
1456
1247
  "AggregationResult": {
1457
1248
  "location": "import",
1458
- "path": "../../types/client-layout",
1249
+ "path": "../../../types/client-layout",
1459
1250
  "id": "src/types/client-layout.ts::AggregationResult",
1460
1251
  "referenceLocation": "AggregationResult"
1461
1252
  }