@mmlogic/components 0.2.0 → 0.3.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.
@@ -22,7 +22,10 @@ export class MrdTable {
22
22
  this.outsideClickHandler = null;
23
23
  this.keydownHandler = null;
24
24
  // ── Props ──────────────────────────────────────────────────────────────────
25
- this.columns = [];
25
+ /** The VIEW or RELATED_VIEW layout item. Contains view config, dataClass, fromClass, actions etc. */
26
+ this.item = null;
27
+ /** Parent record id — required for RELATED_VIEW to build /{fromClass}/{parentId}/{dataClass}. */
28
+ this.parentId = '';
26
29
  /** Direct rows (non-paginated mode, used when totalElements === 0). */
27
30
  this.rows = [];
28
31
  this.locale = navigator.language;
@@ -34,16 +37,9 @@ export class MrdTable {
34
37
  this.rowHeight = 36;
35
38
  /** Height of the scroll container in px. */
36
39
  this.tableHeight = 500;
37
- /** Initial sort applied on load, e.g. "timestamp,desc" or "name".
38
- * Parsed by init() into sortField + sortDir. */
39
- this.defaultSort = '';
40
- /** Toolbar action buttons rendered above the table. */
41
- this.actions = [];
42
- /** Display label of the current view — shown in the toolbar center as a view picker trigger. */
43
- this.viewLabel = '';
44
- /** Alternative views available for this table; renders a dropdown when non-empty. */
45
- this.alternativeViews = [];
46
40
  // ── Internal state ─────────────────────────────────────────────────────────
41
+ /** Index into allViews[] for the currently displayed view. 0 = primary, 1+ = alternatives. */
42
+ this.activeViewIdx = 0;
47
43
  this.loadedPages = new Map();
48
44
  this.requestedPages = new Set();
49
45
  this.renderStart = 0;
@@ -69,10 +65,16 @@ export class MrdTable {
69
65
  this.jsonModal = null;
70
66
  /** Aggregation totals received from the host via setAggregations(). Null = not yet loaded. */
71
67
  this.aggregations = null;
68
+ /** Record count received via setAggregations().total; overrides totalElements for display. */
69
+ this.aggregationsTotal = null;
70
+ /** True when a fresh aggregations request is needed (set on init / filter change). */
71
+ this.aggregationsPending = false;
72
+ /** Lower bound on total derived from setPage() hasNext info; grows as pages load. */
73
+ this.minKnownTotal = 0;
72
74
  this.handleScroll = (e) => {
73
75
  const scroller = e.currentTarget;
74
76
  const scrollTop = scroller.scrollTop;
75
- const total = this.totalElements;
77
+ const total = this.baseTotal;
76
78
  const visStart = Math.floor(scrollTop / this.rowHeight);
77
79
  const visEnd = Math.min(visStart + this.visibleCount(), total - 1);
78
80
  this.scrollTop = scrollTop;
@@ -86,13 +88,16 @@ export class MrdTable {
86
88
  totalElementsChanged(newVal) {
87
89
  this.renderEnd = Math.min(this.renderEnd, Math.max(0, newVal - 1));
88
90
  }
89
- /** Apply defaultSort when the prop changes (e.g. after a view switch). */
90
- defaultSortChanged(newVal) {
91
- this.applyDefaultSort(newVal);
91
+ /** Reset to primary view when the item prop is replaced from outside. */
92
+ itemChanged(newVal) {
93
+ var _a, _b;
94
+ this.activeViewIdx = 0;
95
+ this.applyDefaultSort((_b = (_a = newVal === null || newVal === void 0 ? void 0 : newVal.view) === null || _a === void 0 ? void 0 : _a.defaultSort) !== null && _b !== void 0 ? _b : '');
92
96
  }
93
97
  // ── Lifecycle ──────────────────────────────────────────────────────────────
94
98
  componentWillLoad() {
95
- this.applyDefaultSort(this.defaultSort);
99
+ var _a, _b, _c;
100
+ 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 : '');
96
101
  }
97
102
  // ── Helpers ────────────────────────────────────────────────────────────────
98
103
  applyDefaultSort(defaultSort) {
@@ -124,14 +129,19 @@ export class MrdTable {
124
129
  this.colWidths = [];
125
130
  this.scrollTop = 0;
126
131
  this.renderStart = 0;
127
- // No BUFFER on init — only request what fits the visible area (page 0).
128
- // BUFFER is applied during scroll to pre-fetch the next page proactively.
129
- this.renderEnd = Math.max(0, Math.min(this.visibleCount() - 1, this.totalElements - 1));
132
+ // Always fill the visible viewport on init — totalElements may be stale from a
133
+ // previous view. setPage() clamps renderEnd when the page is shorter than pageSize.
134
+ this.renderEnd = this.visibleCount() - 1;
130
135
  const scroller = this.el.querySelector('.mrd-table__scroll');
131
136
  if (scroller)
132
137
  scroller.scrollTop = 0;
133
138
  this.aggregations = null;
134
- this.emitLoadAggregations();
139
+ this.aggregationsTotal = null;
140
+ this.aggregationsPending = true;
141
+ this.minKnownTotal = 0;
142
+ // Always request page 0 — totalElements may be unknown (0) on first load.
143
+ this.mrdLoadPage.emit({ page: 0, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(0) });
144
+ this.requestedPages = new Set([0]);
135
145
  }
136
146
  /**
137
147
  * Inject the rows for a given page (0-based).
@@ -140,20 +150,41 @@ export class MrdTable {
140
150
  * When the page contains fewer rows than pageSize it is the last page.
141
151
  * renderEnd is clamped immediately so no loading-placeholder rows appear
142
152
  * beyond the actual data — without requiring the host to update totalElements.
153
+ *
154
+ * Pass hasNext (from _links.next in the API response) for accurate last-page
155
+ * detection even when rows.length === pageSize (exact multiple of page size).
143
156
  */
144
- async setPage(pageNumber, rows) {
145
- if (rows.length < this.pageSize) {
157
+ async setPage(pageNumber, rows, hasNext) {
158
+ const isLastPage = hasNext !== undefined ? !hasNext : rows.length < this.pageSize;
159
+ if (isLastPage) {
146
160
  // lastRowIdx is -1 when the page is empty; clamp renderEnd to -1 so the
147
161
  // render loop does not execute and no shimmer rows appear.
148
162
  const lastRowIdx = pageNumber * this.pageSize + rows.length - 1;
149
163
  this.renderEnd = Math.min(this.renderEnd, lastRowIdx);
164
+ // Exact total is known: update minKnownTotal so the scroll container has the right height.
165
+ this.minKnownTotal = pageNumber * this.pageSize + rows.length;
166
+ }
167
+ else {
168
+ // There is at least one more page — ensure the scrollbar covers at least that next page.
169
+ this.minKnownTotal = Math.max(this.minKnownTotal, (pageNumber + 1) * this.pageSize + 1);
150
170
  }
151
171
  const next = new Map(this.loadedPages);
152
172
  next.set(pageNumber, rows);
153
173
  this.loadedPages = next;
174
+ if (pageNumber === 0 && this.aggregationsPending) {
175
+ this.aggregationsPending = false;
176
+ const hasAggColumns = this.columns.some(c => c.type === 'FIELD' && c.aggregate);
177
+ if (!isLastPage || hasAggColumns) {
178
+ this.emitLoadAggregations();
179
+ }
180
+ }
154
181
  }
155
182
  /** Inject aggregation totals returned by the /aggregations endpoint. */
156
183
  async setAggregations(data) {
184
+ var _a;
185
+ const total = (_a = data.total) !== null && _a !== void 0 ? _a : (typeof data.count === 'number' ? data.count : undefined);
186
+ if (total != null)
187
+ this.aggregationsTotal = total;
157
188
  this.aggregations = data;
158
189
  }
159
190
  // ── Lifecycle ──────────────────────────────────────────────────────────────
@@ -184,6 +215,147 @@ export class MrdTable {
184
215
  return '';
185
216
  return this.sortDir === 'desc' ? `${this.sortField},desc` : this.sortField;
186
217
  }
218
+ /** Stable ordered list: primary view first, then alternatives (from the item prop). */
219
+ get allViews() {
220
+ var _a, _b, _c, _d, _e, _f, _g;
221
+ if (!this.item)
222
+ return [];
223
+ const it = this.item;
224
+ return [
225
+ { label: (_e = (_d = (_c = (_a = it.label) !== null && _a !== void 0 ? _a : (_b = it.view) === null || _b === void 0 ? void 0 : _b.pluralLabel) !== null && _c !== void 0 ? _c : it.dataClass) !== null && _d !== void 0 ? _d : it.relatedClass) !== null && _e !== void 0 ? _e : '', dataClass: (_f = it.dataClass) !== null && _f !== void 0 ? _f : it.relatedClass, fromClass: it.fromClass, filterClass: it.filterClass, view: it.view },
226
+ ...((_g = it.alternativeViews) !== null && _g !== void 0 ? _g : []).map(av => {
227
+ var _a, _b, _c, _d;
228
+ return ({
229
+ label: (_d = (_c = (_a = av.label) !== null && _a !== void 0 ? _a : (_b = av.view) === null || _b === void 0 ? void 0 : _b.pluralLabel) !== null && _c !== void 0 ? _c : av.dataClass) !== null && _d !== void 0 ? _d : '',
230
+ dataClass: av.dataClass,
231
+ fromClass: av.fromClass,
232
+ filterClass: av.filterClass,
233
+ view: av.view,
234
+ });
235
+ }),
236
+ ];
237
+ }
238
+ /** Relative excel export path for the current view.
239
+ * VIEW: /excel/{dataClass}
240
+ * RELATED_VIEW: /excel/{fromClass}/{parentId}/{dataClass} */
241
+ buildExcelPath() {
242
+ var _a, _b, _c, _d;
243
+ const v = this.allViews[this.activeViewIdx];
244
+ if (!v)
245
+ return '';
246
+ if (((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW') {
247
+ return `/excel/${(_b = v.fromClass) !== null && _b !== void 0 ? _b : ''}/${this.parentId}/${(_c = v.dataClass) !== null && _c !== void 0 ? _c : ''}`;
248
+ }
249
+ return `/excel/${(_d = v.dataClass) !== null && _d !== void 0 ? _d : ''}`;
250
+ }
251
+ buildActionDetail(action) {
252
+ var _a, _b, _c;
253
+ if (action === 'export') {
254
+ return { action, path: this.buildExcelPath(), qs: this.buildQueryParams(0) };
255
+ }
256
+ if (action === 'create') {
257
+ const v = this.allViews[this.activeViewIdx];
258
+ const parentPath = ((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW'
259
+ ? `/${(_b = v === null || v === void 0 ? void 0 : v.fromClass) !== null && _b !== void 0 ? _b : ''}/${this.parentId}`
260
+ : null;
261
+ return { action, dataClass: (_c = v === null || v === void 0 ? void 0 : v.dataClass) !== null && _c !== void 0 ? _c : '', parentPath };
262
+ }
263
+ return { action };
264
+ }
265
+ /** Relative data path for the current view, without query string.
266
+ * VIEW: /{dataClass}
267
+ * RELATED_VIEW: /{fromClass}/{parentId}/{dataClass} */
268
+ buildDataPath() {
269
+ var _a, _b, _c, _d;
270
+ const v = this.allViews[this.activeViewIdx];
271
+ if (!v)
272
+ return '';
273
+ if (((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === 'RELATED_VIEW') {
274
+ return `/${(_b = v.fromClass) !== null && _b !== void 0 ? _b : ''}/${this.parentId}/${(_c = v.dataClass) !== null && _c !== void 0 ? _c : ''}`;
275
+ }
276
+ return `/${(_d = v.dataClass) !== null && _d !== void 0 ? _d : ''}`;
277
+ }
278
+ /** Build query params for a page request from current sort, view filters, filterClass and active column filters. */
279
+ buildQueryParams(page) {
280
+ var _a, _b, _c, _d, _e, _f, _g;
281
+ const v = this.allViews[this.activeViewIdx];
282
+ const p = new URLSearchParams();
283
+ if (page > 0)
284
+ p.set('page', String(page));
285
+ const sort = this.sortParam();
286
+ if (sort)
287
+ p.set('sort', sort);
288
+ const filterClass = v === null || v === void 0 ? void 0 : v.filterClass;
289
+ if (filterClass)
290
+ p.set('type', filterClass);
291
+ 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 : [])) {
292
+ if (!f.name)
293
+ continue;
294
+ if (f.operator === 'EMPTY') {
295
+ p.set(f.name, '');
296
+ continue;
297
+ }
298
+ if (f.operator === 'NOT_EMPTY') {
299
+ p.set(f.name + '_notempty', 'true');
300
+ continue;
301
+ }
302
+ if (f.operator === 'STARTS_WITH') {
303
+ p.set(f.name + '_startswith', String((_c = f.value) !== null && _c !== void 0 ? _c : ''));
304
+ continue;
305
+ }
306
+ if (f.operator === 'FROM') {
307
+ p.set(f.name + '_from', String((_d = f.value) !== null && _d !== void 0 ? _d : ''));
308
+ continue;
309
+ }
310
+ if (f.operator === 'TO') {
311
+ p.set(f.name + '_to', String((_e = f.value) !== null && _e !== void 0 ? _e : ''));
312
+ continue;
313
+ }
314
+ if (f.value != null) {
315
+ p.set(f.name, String(f.value));
316
+ }
317
+ }
318
+ for (const f of this.activeFilters.values()) {
319
+ if (f.operator === 'isEmpty') {
320
+ p.set(f.field, '');
321
+ continue;
322
+ }
323
+ if (f.operator === 'isNotEmpty') {
324
+ p.set(f.field + '_notempty', 'true');
325
+ continue;
326
+ }
327
+ if (f.operator === 'startsWith') {
328
+ p.set(f.field + '_startswith', String((_f = f.value) !== null && _f !== void 0 ? _f : ''));
329
+ continue;
330
+ }
331
+ if ((_g = f.values) === null || _g === void 0 ? void 0 : _g.length) {
332
+ p.set(f.field, f.values.join(','));
333
+ continue;
334
+ }
335
+ if (f.value != null)
336
+ p.set(f.field, String(f.value));
337
+ if (f.from != null)
338
+ p.set(f.field + '_from', String(f.from));
339
+ if (f.to != null)
340
+ p.set(f.field + '_to', String(f.to));
341
+ }
342
+ return p.toString();
343
+ }
344
+ get columns() {
345
+ var _a, _b, _c;
346
+ return ((_c = (_b = (_a = this.allViews[this.activeViewIdx]) === null || _a === void 0 ? void 0 : _a.view) === null || _b === void 0 ? void 0 : _b.values) !== null && _c !== void 0 ? _c : []);
347
+ }
348
+ get tableActions() {
349
+ var _a, _b;
350
+ const raw = (_b = (_a = this.item) === null || _a === void 0 ? void 0 : _a.actions) !== null && _b !== void 0 ? _b : [];
351
+ return (raw !== null && raw !== void 0 ? raw : []).reduce((acc, a) => {
352
+ if (a === 'NEW')
353
+ acc.push({ action: 'create', label: t('table_new_record', this.locale), icon: 'assets/sprites.svg#icon-plus', variant: 'primary' });
354
+ if (a === 'EXPORT')
355
+ acc.push({ action: 'export', label: t('table_export_excel', this.locale), icon: 'assets/sprites.svg#icon-file-excel' });
356
+ return acc;
357
+ }, []);
358
+ }
187
359
  colName(col) {
188
360
  var _a;
189
361
  return (_a = col.name) !== null && _a !== void 0 ? _a : '';
@@ -194,6 +366,25 @@ export class MrdTable {
194
366
  return 'RELATION';
195
367
  return (_a = col.dataType) !== null && _a !== void 0 ? _a : 'TEXT';
196
368
  }
369
+ /** True when we have a reliable total: either from the aggregations response or because
370
+ * a short page told us it was the last page (exact count from row length). */
371
+ isTotalKnown() {
372
+ if (this.aggregationsTotal != null)
373
+ return true;
374
+ for (const rows of this.loadedPages.values()) {
375
+ if (rows.length < this.pageSize)
376
+ return true;
377
+ }
378
+ return false;
379
+ }
380
+ /** Effective total: aggregations-response > totalElements prop > minKnownTotal from setPage(). */
381
+ get baseTotal() {
382
+ if (this.aggregationsTotal != null)
383
+ return this.aggregationsTotal;
384
+ if (this.totalElements > 0)
385
+ return this.totalElements;
386
+ return this.minKnownTotal;
387
+ }
197
388
  // ── Aggregation helpers ────────────────────────────────────────────────────
198
389
  buildAggregationParams() {
199
390
  var _a;
@@ -214,17 +405,32 @@ export class MrdTable {
214
405
  params.count = groups.count;
215
406
  return Object.keys(params).length > 0 ? params : null;
216
407
  }
408
+ buildAggregationQs() {
409
+ var _a, _b, _c;
410
+ const p = new URLSearchParams(this.buildQueryParams(0));
411
+ p.delete('page');
412
+ p.delete('sort');
413
+ const groups = this.buildAggregationParams();
414
+ if ((_a = groups === null || groups === void 0 ? void 0 : groups.sum) === null || _a === void 0 ? void 0 : _a.length)
415
+ p.set('sum', groups.sum.join(','));
416
+ if ((_b = groups === null || groups === void 0 ? void 0 : groups.avg) === null || _b === void 0 ? void 0 : _b.length)
417
+ p.set('avg', groups.avg.join(','));
418
+ if ((_c = groups === null || groups === void 0 ? void 0 : groups.count) === null || _c === void 0 ? void 0 : _c.length)
419
+ p.set('count', groups.count.join(','));
420
+ return p.toString();
421
+ }
217
422
  emitLoadAggregations() {
218
- const params = this.buildAggregationParams();
219
- if (params)
220
- this.mrdLoadAggregations.emit(params);
423
+ this.mrdLoadAggregations.emit({ path: this.buildDataPath(), qs: this.buildQueryParams(0), aggQs: this.buildAggregationQs() });
221
424
  }
222
425
  renderAggregationValue(col) {
223
- var _a, _b;
426
+ var _a;
224
427
  if (col.type !== 'FIELD' || !col.aggregate || !this.aggregations)
225
428
  return '';
226
429
  const fn = col.aggregate.toLowerCase();
227
- const val = (_a = this.aggregations[fn]) === null || _a === void 0 ? void 0 : _a[(_b = col.name) !== null && _b !== void 0 ? _b : ''];
430
+ const aggData = this.aggregations[fn];
431
+ const val = typeof aggData === 'object' && aggData !== null
432
+ ? aggData[(_a = col.name) !== null && _a !== void 0 ? _a : '']
433
+ : undefined;
228
434
  if (val == null)
229
435
  return '';
230
436
  const dt = col.dataType;
@@ -249,9 +455,10 @@ export class MrdTable {
249
455
  this.colWidths = [];
250
456
  this.scrollTop = 0;
251
457
  this.renderStart = 0;
252
- // No BUFFER here — totalElements may be stale after a filter change.
253
- // Only request what is visible; BUFFER kicks in during scroll as usual.
254
- this.renderEnd = Math.max(0, Math.min(this.visibleCount() - 1, this.totalElements - 1));
458
+ this.minKnownTotal = 0;
459
+ // Mirror init(): use visibleCount so the first page is always requested.
460
+ // setPage() will clamp renderEnd down when the last page is shorter.
461
+ this.renderEnd = this.visibleCount() - 1;
255
462
  const scroller = this.el.querySelector('.mrd-table__scroll');
256
463
  if (scroller)
257
464
  scroller.scrollTop = 0;
@@ -282,7 +489,7 @@ export class MrdTable {
282
489
  for (let p = firstPage; p <= lastPage; p++) {
283
490
  if (!this.loadedPages.has(p) && !next.has(p)) {
284
491
  next.add(p);
285
- this.mrdLoadPage.emit({ page: p, sort: this.sortParam() });
492
+ this.mrdLoadPage.emit({ page: p, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(p) });
286
493
  changed = true;
287
494
  }
288
495
  }
@@ -324,7 +531,7 @@ export class MrdTable {
324
531
  if (pageEnd < this.renderStart || pageStart > this.renderEnd)
325
532
  continue;
326
533
  next.add(page);
327
- this.mrdLoadPage.emit({ page, sort: this.sortParam() });
534
+ this.mrdLoadPage.emit({ page, sort: this.sortParam(), path: this.buildDataPath(), qs: this.buildQueryParams(page) });
328
535
  changed = true;
329
536
  }
330
537
  this.pendingPages.clear();
@@ -516,9 +723,9 @@ export class MrdTable {
516
723
  }
517
724
  this.activeFilters = next;
518
725
  this.closeFilterPopup();
519
- this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
520
726
  this.aggregations = null;
521
- this.emitLoadAggregations();
727
+ this.aggregationsTotal = null;
728
+ this.aggregationsPending = true;
522
729
  if (this.totalElements > 0) {
523
730
  this.resetPages();
524
731
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
@@ -531,9 +738,9 @@ export class MrdTable {
531
738
  next.delete(name);
532
739
  this.activeFilters = next;
533
740
  this.closeFilterPopup();
534
- this.mrdFilter.emit({ filters: Array.from(this.activeFilters.values()) });
535
741
  this.aggregations = null;
536
- this.emitLoadAggregations();
742
+ this.aggregationsTotal = null;
743
+ this.aggregationsPending = true;
537
744
  if (this.totalElements > 0) {
538
745
  this.resetPages();
539
746
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
@@ -541,37 +748,39 @@ export class MrdTable {
541
748
  }
542
749
  clearAllFilters() {
543
750
  this.activeFilters = new Map();
544
- this.mrdFilter.emit({ filters: [] });
545
751
  this.aggregations = null;
546
- this.emitLoadAggregations();
752
+ this.aggregationsTotal = null;
753
+ this.aggregationsPending = true;
547
754
  if (this.totalElements > 0) {
548
755
  this.resetPages();
549
756
  this.emitPagesForWindow(this.renderStart, this.renderEnd);
550
757
  }
551
758
  }
552
759
  // ── View switcher ──────────────────────────────────────────────────────────
553
- handleViewSwitch(view) {
554
- this.mrdSwitchView.emit({ name: view.name, class: view.class });
760
+ handleViewSwitch(targetIdx) {
761
+ var _a, _b;
762
+ const target = this.allViews[targetIdx];
763
+ if (!(target === null || target === void 0 ? void 0 : target.view))
764
+ return;
765
+ this.activeViewIdx = targetIdx;
766
+ this.applyDefaultSort((_b = (_a = target.view) === null || _a === void 0 ? void 0 : _a.defaultSort) !== null && _b !== void 0 ? _b : '');
767
+ this.activeFilters = new Map();
768
+ this.init();
555
769
  }
556
770
  // ── Render: toolbar ────────────────────────────────────────────────────────
557
771
  renderToolbar() {
558
- var _a, _b;
559
772
  const filterCount = this.activeFilters.size;
560
- const hasActions = ((_a = this.actions) === null || _a === void 0 ? void 0 : _a.length) > 0;
561
- const hasViewSwitcher = !!this.viewLabel && ((_b = this.alternativeViews) === null || _b === void 0 ? void 0 : _b.length) > 0;
773
+ const actions = this.tableActions;
774
+ const allViews = this.allViews;
775
+ const hasActions = actions.length > 0;
776
+ const hasViewSwitcher = allViews.length > 1;
562
777
  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("select", { class: "mrd-table__view-select", onChange: (e) => {
563
- const sel = e.target;
564
- const view = this.alternativeViews.find(v => v.name === sel.value);
565
- if (view) {
566
- sel.selectedIndex = 0;
567
- this.handleViewSwitch(view);
568
- }
569
- } }, h("option", { value: "" }, this.viewLabel), this.alternativeViews.map(v => {
570
- var _a;
571
- return (h("option", { value: v.name }, (_a = v.label) !== null && _a !== void 0 ? _a : v.name));
572
- })))), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, this.actions.map(a => {
778
+ const idx = parseInt(e.target.value, 10);
779
+ if (!isNaN(idx) && idx !== this.activeViewIdx)
780
+ this.handleViewSwitch(idx);
781
+ } }, allViews.map((v, i) => (h("option", { value: String(i), selected: i === this.activeViewIdx }, v.label)))))), hasActions && (h("div", { class: "mrd-table__toolbar-right" }, actions.map(a => {
573
782
  var _a;
574
- return (h("button", { class: `mrd-table__action mrd-table__action--${(_a = a.variant) !== null && _a !== void 0 ? _a : 'secondary'}`, disabled: a.disabled, onClick: () => this.mrdAction.emit({ action: a.action }) }, a.icon
783
+ return (h("button", { class: `mrd-table__action mrd-table__action--${(_a = a.variant) !== null && _a !== void 0 ? _a : 'secondary'}`, disabled: a.disabled, onClick: () => this.mrdAction.emit(this.buildActionDetail(a.action)) }, a.icon
575
784
  ? h("svg", { class: "mrd-table__action-icon", "aria-hidden": "true" }, h("use", { href: a.icon }))
576
785
  : a.label, h("span", { class: "mrd-table__action-tooltip" }, a.label)));
577
786
  })))));
@@ -665,10 +874,9 @@ export class MrdTable {
665
874
  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)))));
666
875
  }
667
876
  // ── Render: footer ────────────────────────────────────────────────────────
668
- renderFooter(rowCount, effectiveTotal) {
669
- const total = this.totalElements;
670
- // Non-paginated mode: show plain row count
671
- if (total === 0) {
877
+ renderFooter(rowCount, effectiveTotal, isTotalKnown = true) {
878
+ // Non-paginated mode: totalElements=0 and no paginated data loaded yet
879
+ if (this.totalElements === 0 && this.loadedPages.size === 0) {
672
880
  const count = rowCount !== null && rowCount !== void 0 ? rowCount : 0;
673
881
  if (count === 0)
674
882
  return null;
@@ -677,13 +885,14 @@ export class MrdTable {
677
885
  // Paginated mode: only show once page 0 has loaded (avoids stale total during filter reset)
678
886
  if (!this.loadedPages.has(0))
679
887
  return null;
680
- // Use effectiveTotal (derived from actual page lengths) so the counter
681
- // is correct even when the host has not yet updated totalElements.
682
- const displayTotal = effectiveTotal !== null && effectiveTotal !== void 0 ? effectiveTotal : total;
888
+ // effectiveTotal from render(); fall back to baseTotal when not provided.
889
+ const displayTotal = effectiveTotal !== null && effectiveTotal !== void 0 ? effectiveTotal : this.baseTotal;
683
890
  // Compute from/to independently so partial rows at top/bottom are included.
684
891
  const from = Math.min(Math.floor(this.scrollTop / this.rowHeight) + 1, displayTotal);
685
892
  const to = Math.min(Math.ceil((this.scrollTop + this.tableHeight) / this.rowHeight), displayTotal);
686
- return (h("div", { class: "mrd-table__footer" }, from, "\u2013", to, " ", t('table_of', this.locale), " ", displayTotal));
893
+ // Show '…' for the total until we have a reliable count (aggregations or last page loaded).
894
+ const totalLabel = isTotalKnown ? String(displayTotal) : '…';
895
+ return (h("div", { class: "mrd-table__footer" }, from, "\u2013", to, " ", t('table_of', this.locale), " ", totalLabel));
687
896
  }
688
897
  // ── Render: cell ──────────────────────────────────────────────────────────
689
898
  renderCell(col, row) {
@@ -742,7 +951,9 @@ export class MrdTable {
742
951
  if (!((_a = this.columns) === null || _a === void 0 ? void 0 : _a.length))
743
952
  return null;
744
953
  // ── Non-paginated mode ──────────────────────────────────────────────────
745
- if (this.totalElements === 0) {
954
+ // Only enter non-paginated mode when totalElements is 0 AND no paginated data
955
+ // has been loaded yet — prevents the wrong branch when the host omits totalElements.
956
+ if (this.totalElements === 0 && this.loadedPages.size === 0) {
746
957
  return (h(Host, null, this.renderToolbar(), h("div", { class: "mrd-table" }, h("table", { class: "mrd-table__table" }, h("thead", null, h("tr", null, this.columns.map(col => {
747
958
  var _a;
748
959
  const name = this.colName(col);
@@ -759,8 +970,8 @@ export class MrdTable {
759
970
  // Derive the authoritative row count from loaded pages:
760
971
  // if any loaded page is shorter than pageSize it is the last page,
761
972
  // so the true total cannot exceed (pageNum * pageSize + pageRows.length).
762
- // This self-corrects without requiring the host to update totalElements.
763
- let effectiveTotal = this.totalElements;
973
+ // aggregationsTotal (from setAggregations) takes priority over the totalElements prop.
974
+ let effectiveTotal = this.baseTotal;
764
975
  for (const [pageNum, pageRows] of this.loadedPages) {
765
976
  if (pageRows.length < this.pageSize) {
766
977
  effectiveTotal = Math.min(effectiveTotal, pageNum * this.pageSize + pageRows.length);
@@ -797,7 +1008,7 @@ export class MrdTable {
797
1008
  isFiltered ? 'mrd-table__header--filtered' : '',
798
1009
  ].filter(Boolean).join(' ');
799
1010
  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()));
800
- }))), 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.renderFilterPopup(), this.renderTextblockModal()));
1011
+ }))), 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()));
801
1012
  }
802
1013
  renderFilterIcon() {
803
1014
  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" }))));
@@ -826,18 +1037,18 @@ export class MrdTable {
826
1037
  }
827
1038
  static get properties() {
828
1039
  return {
829
- "columns": {
1040
+ "item": {
830
1041
  "type": "unknown",
831
1042
  "mutable": false,
832
1043
  "complexType": {
833
- "original": "TableColumn[]",
834
- "resolved": "ClientLayoutItem[]",
1044
+ "original": "ClientLayoutItem | null",
1045
+ "resolved": "ClientLayoutItem | null",
835
1046
  "references": {
836
- "TableColumn": {
1047
+ "ClientLayoutItem": {
837
1048
  "location": "import",
838
- "path": "../../utils/cell-renderer",
839
- "id": "src/utils/cell-renderer.ts::TableColumn",
840
- "referenceLocation": "TableColumn"
1049
+ "path": "../../types/client-layout",
1050
+ "id": "src/types/client-layout.ts::ClientLayoutItem",
1051
+ "referenceLocation": "ClientLayoutItem"
841
1052
  }
842
1053
  }
843
1054
  },
@@ -845,11 +1056,31 @@ export class MrdTable {
845
1056
  "optional": false,
846
1057
  "docs": {
847
1058
  "tags": [],
848
- "text": ""
1059
+ "text": "The VIEW or RELATED_VIEW layout item. Contains view config, dataClass, fromClass, actions etc."
849
1060
  },
850
1061
  "getter": false,
851
1062
  "setter": false,
852
- "defaultValue": "[]"
1063
+ "defaultValue": "null"
1064
+ },
1065
+ "parentId": {
1066
+ "type": "string",
1067
+ "mutable": false,
1068
+ "complexType": {
1069
+ "original": "string",
1070
+ "resolved": "string",
1071
+ "references": {}
1072
+ },
1073
+ "required": false,
1074
+ "optional": false,
1075
+ "docs": {
1076
+ "tags": [],
1077
+ "text": "Parent record id \u2014 required for RELATED_VIEW to build /{fromClass}/{parentId}/{dataClass}."
1078
+ },
1079
+ "getter": false,
1080
+ "setter": false,
1081
+ "reflect": false,
1082
+ "attribute": "parent-id",
1083
+ "defaultValue": "''"
853
1084
  },
854
1085
  "rows": {
855
1086
  "type": "unknown",
@@ -973,101 +1204,12 @@ export class MrdTable {
973
1204
  "reflect": false,
974
1205
  "attribute": "table-height",
975
1206
  "defaultValue": "500"
976
- },
977
- "defaultSort": {
978
- "type": "string",
979
- "mutable": false,
980
- "complexType": {
981
- "original": "string",
982
- "resolved": "string",
983
- "references": {}
984
- },
985
- "required": false,
986
- "optional": false,
987
- "docs": {
988
- "tags": [],
989
- "text": "Initial sort applied on load, e.g. \"timestamp,desc\" or \"name\".\nParsed by init() into sortField + sortDir."
990
- },
991
- "getter": false,
992
- "setter": false,
993
- "reflect": false,
994
- "attribute": "default-sort",
995
- "defaultValue": "''"
996
- },
997
- "actions": {
998
- "type": "unknown",
999
- "mutable": false,
1000
- "complexType": {
1001
- "original": "TableAction[]",
1002
- "resolved": "TableAction[]",
1003
- "references": {
1004
- "TableAction": {
1005
- "location": "import",
1006
- "path": "../../utils/cell-renderer",
1007
- "id": "src/utils/cell-renderer.ts::TableAction",
1008
- "referenceLocation": "TableAction"
1009
- }
1010
- }
1011
- },
1012
- "required": false,
1013
- "optional": false,
1014
- "docs": {
1015
- "tags": [],
1016
- "text": "Toolbar action buttons rendered above the table."
1017
- },
1018
- "getter": false,
1019
- "setter": false,
1020
- "defaultValue": "[]"
1021
- },
1022
- "viewLabel": {
1023
- "type": "string",
1024
- "mutable": false,
1025
- "complexType": {
1026
- "original": "string",
1027
- "resolved": "string",
1028
- "references": {}
1029
- },
1030
- "required": false,
1031
- "optional": false,
1032
- "docs": {
1033
- "tags": [],
1034
- "text": "Display label of the current view \u2014 shown in the toolbar center as a view picker trigger."
1035
- },
1036
- "getter": false,
1037
- "setter": false,
1038
- "reflect": false,
1039
- "attribute": "view-label",
1040
- "defaultValue": "''"
1041
- },
1042
- "alternativeViews": {
1043
- "type": "unknown",
1044
- "mutable": false,
1045
- "complexType": {
1046
- "original": "AlternativeView[]",
1047
- "resolved": "AlternativeView[]",
1048
- "references": {
1049
- "AlternativeView": {
1050
- "location": "import",
1051
- "path": "../../utils/cell-renderer",
1052
- "id": "src/utils/cell-renderer.ts::AlternativeView",
1053
- "referenceLocation": "AlternativeView"
1054
- }
1055
- }
1056
- },
1057
- "required": false,
1058
- "optional": false,
1059
- "docs": {
1060
- "tags": [],
1061
- "text": "Alternative views available for this table; renders a dropdown when non-empty."
1062
- },
1063
- "getter": false,
1064
- "setter": false,
1065
- "defaultValue": "[]"
1066
1207
  }
1067
1208
  };
1068
1209
  }
1069
1210
  static get states() {
1070
1211
  return {
1212
+ "activeViewIdx": {},
1071
1213
  "loadedPages": {},
1072
1214
  "requestedPages": {},
1073
1215
  "renderStart": {},
@@ -1083,7 +1225,9 @@ export class MrdTable {
1083
1225
  "scrollTop": {},
1084
1226
  "textblockModal": {},
1085
1227
  "jsonModal": {},
1086
- "aggregations": {}
1228
+ "aggregations": {},
1229
+ "aggregationsTotal": {},
1230
+ "minKnownTotal": {}
1087
1231
  };
1088
1232
  }
1089
1233
  static get events() {
@@ -1098,8 +1242,8 @@ export class MrdTable {
1098
1242
  "text": "Fired when a page needs to be fetched. Host fetches and calls setPage().\n`sort` is the raw query-param value, e.g. \"name\" or \"name,desc\"."
1099
1243
  },
1100
1244
  "complexType": {
1101
- "original": "{ page: number; sort: string }",
1102
- "resolved": "{ page: number; sort: string; }",
1245
+ "original": "{ page: number; sort: string; path: string; qs: string }",
1246
+ "resolved": "{ page: number; sort: string; path: string; qs: string; }",
1103
1247
  "references": {}
1104
1248
  }
1105
1249
  }, {
@@ -1130,35 +1274,13 @@ export class MrdTable {
1130
1274
  "composed": true,
1131
1275
  "docs": {
1132
1276
  "tags": [],
1133
- "text": "Fired when a toolbar action button is clicked. Detail contains the action identifier."
1277
+ "text": "Fired when a toolbar action button is clicked.\nFor 'export': includes `path` (relative excel path) and `qs` (current sort+filter params).\nFor 'create': includes `dataClass` (target type) and `parentPath` (e.g. /buyers/123 for RELATED_VIEW)."
1134
1278
  },
1135
1279
  "complexType": {
1136
- "original": "{ action: string }",
1137
- "resolved": "{ action: string; }",
1280
+ "original": "{ action: string; path?: string; qs?: string; dataClass?: string; parentPath?: string | null }",
1281
+ "resolved": "{ action: string; path?: string | undefined; qs?: string | undefined; dataClass?: string | undefined; parentPath?: string | null | undefined; }",
1138
1282
  "references": {}
1139
1283
  }
1140
- }, {
1141
- "method": "mrdFilter",
1142
- "name": "mrdFilter",
1143
- "bubbles": true,
1144
- "cancelable": true,
1145
- "composed": true,
1146
- "docs": {
1147
- "tags": [],
1148
- "text": "Fired when active filters change. Host translates filters to API query params."
1149
- },
1150
- "complexType": {
1151
- "original": "{ filters: ColumnFilter[] }",
1152
- "resolved": "{ filters: ColumnFilter[]; }",
1153
- "references": {
1154
- "ColumnFilter": {
1155
- "location": "import",
1156
- "path": "../../utils/cell-renderer",
1157
- "id": "src/utils/cell-renderer.ts::ColumnFilter",
1158
- "referenceLocation": "ColumnFilter"
1159
- }
1160
- }
1161
- }
1162
1284
  }, {
1163
1285
  "method": "mrdDownload",
1164
1286
  "name": "mrdDownload",
@@ -1174,21 +1296,6 @@ export class MrdTable {
1174
1296
  "resolved": "{ href: string; fileName: string; }",
1175
1297
  "references": {}
1176
1298
  }
1177
- }, {
1178
- "method": "mrdSwitchView",
1179
- "name": "mrdSwitchView",
1180
- "bubbles": true,
1181
- "cancelable": true,
1182
- "composed": true,
1183
- "docs": {
1184
- "tags": [],
1185
- "text": "Fired when the user selects an alternative view from the view switcher dropdown."
1186
- },
1187
- "complexType": {
1188
- "original": "{ name: string; class?: string }",
1189
- "resolved": "{ name: string; class?: string | undefined; }",
1190
- "references": {}
1191
- }
1192
1299
  }, {
1193
1300
  "method": "mrdLoadAggregations",
1194
1301
  "name": "mrdLoadAggregations",
@@ -1197,11 +1304,11 @@ export class MrdTable {
1197
1304
  "composed": true,
1198
1305
  "docs": {
1199
1306
  "tags": [],
1200
- "text": "Fired when aggregation totals need to be (re-)fetched.\nDetail contains the fields grouped by aggregate function.\nHost calls the /aggregations endpoint and passes the result to setAggregations()."
1307
+ "text": "Fired when aggregation totals need to be (re-)fetched.\n`aggQs` is a ready-to-use query string: active filters (no page/sort) + sum/avg/count params.\nHost calls the /aggregations endpoint and passes the result to setAggregations()."
1201
1308
  },
1202
1309
  "complexType": {
1203
- "original": "{ sum?: string[]; avg?: string[]; count?: string[] }",
1204
- "resolved": "{ sum?: string[] | undefined; avg?: string[] | undefined; count?: string[] | undefined; }",
1310
+ "original": "{ path: string; qs: string; aggQs: string }",
1311
+ "resolved": "{ path: string; qs: string; aggQs: string; }",
1205
1312
  "references": {}
1206
1313
  }
1207
1314
  }];
@@ -1231,7 +1338,7 @@ export class MrdTable {
1231
1338
  },
1232
1339
  "setPage": {
1233
1340
  "complexType": {
1234
- "signature": "(pageNumber: number, rows: Record<string, any>[]) => Promise<void>",
1341
+ "signature": "(pageNumber: number, rows: Record<string, any>[], hasNext?: boolean) => Promise<void>",
1235
1342
  "parameters": [{
1236
1343
  "name": "pageNumber",
1237
1344
  "type": "number",
@@ -1240,6 +1347,10 @@ export class MrdTable {
1240
1347
  "name": "rows",
1241
1348
  "type": "Record<string, any>[]",
1242
1349
  "docs": ""
1350
+ }, {
1351
+ "name": "hasNext",
1352
+ "type": "boolean | undefined",
1353
+ "docs": ""
1243
1354
  }],
1244
1355
  "references": {
1245
1356
  "Promise": {
@@ -1254,7 +1365,7 @@ export class MrdTable {
1254
1365
  "return": "Promise<void>"
1255
1366
  },
1256
1367
  "docs": {
1257
- "text": "Inject the rows for a given page (0-based).\nCreates a new Map reference so Stencil detects the state change.\n\nWhen the page contains fewer rows than pageSize it is the last page.\nrenderEnd is clamped immediately so no loading-placeholder rows appear\nbeyond the actual data \u2014 without requiring the host to update totalElements.",
1368
+ "text": "Inject the rows for a given page (0-based).\nCreates a new Map reference so Stencil detects the state change.\n\nWhen the page contains fewer rows than pageSize it is the last page.\nrenderEnd is clamped immediately so no loading-placeholder rows appear\nbeyond the actual data \u2014 without requiring the host to update totalElements.\n\nPass hasNext (from _links.next in the API response) for accurate last-page\ndetection even when rows.length === pageSize (exact multiple of page size).",
1258
1369
  "tags": []
1259
1370
  }
1260
1371
  },
@@ -1293,8 +1404,8 @@ export class MrdTable {
1293
1404
  "propName": "totalElements",
1294
1405
  "methodName": "totalElementsChanged"
1295
1406
  }, {
1296
- "propName": "defaultSort",
1297
- "methodName": "defaultSortChanged"
1407
+ "propName": "item",
1408
+ "methodName": "itemChanged"
1298
1409
  }];
1299
1410
  }
1300
1411
  }