spiderly 19.12.0 → 19.12.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.
- package/agent/docs/angular-customization/index.md +3 -3
- package/agent/docs/e2e-testing/index.md +1 -1
- package/fesm2022/spiderly.mjs +339 -80
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +158 -14
- package/lib/filters/allowed-operators.d.ts +5 -4
- package/lib/filters/filter-store.d.ts +9 -0
- package/package.json +1 -1
package/fesm2022/spiderly.mjs
CHANGED
|
@@ -4754,6 +4754,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
4754
4754
|
function rootFontSizePx() {
|
|
4755
4755
|
return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
4756
4756
|
}
|
|
4757
|
+
/** Order matters: `[a, b]` and `[b, a]` ask the server for different orderings. */
|
|
4758
|
+
function sortMetaEquals(a, b) {
|
|
4759
|
+
return (a.length === b.length &&
|
|
4760
|
+
a.every((m, i) => m.field === b[i].field && m.order === b[i].order));
|
|
4761
|
+
}
|
|
4757
4762
|
const DEFAULT_COLUMN_WIDTH_REM = {
|
|
4758
4763
|
text: 12,
|
|
4759
4764
|
date: 10,
|
|
@@ -4852,7 +4857,8 @@ class SpiderlyDataTableComponent {
|
|
|
4852
4857
|
/** Paginator page-size choices; merge rules and persistence: CLAUDE.md → "Rows-per-page". */
|
|
4853
4858
|
this.rowsPerPageOptions = [10, 25, 50, 100];
|
|
4854
4859
|
/**
|
|
4855
|
-
* The view currently selected. Seeded from the first view in `ngOnInit
|
|
4860
|
+
* The view currently selected. Seeded from the first view in `ngOnInit` — then overridden by
|
|
4861
|
+
* `restoreActiveView`, so a reload lands where the operator was — and never left null on a
|
|
4856
4862
|
* table that has views: `viewScope` keys the layout off it, so an unseeded table writes its
|
|
4857
4863
|
* width and order to the SAME key an unmigrated table uses — orphaned the moment the operator
|
|
4858
4864
|
* clicks the first tab — and no tab reads as selected until they do.
|
|
@@ -5020,9 +5026,12 @@ class SpiderlyDataTableComponent {
|
|
|
5020
5026
|
this.table.saveState();
|
|
5021
5027
|
}
|
|
5022
5028
|
this.table.anchorRowIndex = null;
|
|
5023
|
-
return;
|
|
5024
5029
|
}
|
|
5025
|
-
|
|
5030
|
+
else {
|
|
5031
|
+
originalSort(event);
|
|
5032
|
+
}
|
|
5033
|
+
// Every explicit header-sort gesture, un-sort included, records the view's override.
|
|
5034
|
+
this.persistActiveViewSort();
|
|
5026
5035
|
};
|
|
5027
5036
|
}
|
|
5028
5037
|
/**
|
|
@@ -5048,12 +5057,21 @@ class SpiderlyDataTableComponent {
|
|
|
5048
5057
|
}
|
|
5049
5058
|
}
|
|
5050
5059
|
}
|
|
5051
|
-
ngOnChanges() {
|
|
5060
|
+
ngOnChanges(changes) {
|
|
5052
5061
|
// Only meaningful once ngOnInit has resolved the defaults; before that it is a no-op re-run.
|
|
5053
5062
|
if (this.rows != null)
|
|
5054
5063
|
this.mergeActivePageSizesIntoOptions();
|
|
5064
|
+
if (changes['views'])
|
|
5065
|
+
this.adoptStoredView();
|
|
5055
5066
|
}
|
|
5056
5067
|
ngOnInit() {
|
|
5068
|
+
// Fail loud, not half-broken: the store commits through the paginated request, and a
|
|
5069
|
+
// client-side (form-array) table has none — armed anyway, the first commit would call the
|
|
5070
|
+
// absent getPaginatedListObservableMethod inside the requery effect and strand the pending
|
|
5071
|
+
// overlay forever. Client-side store predicates are unbuilt (spiderly#407 territory).
|
|
5072
|
+
if (this.filters && !this.hasLazyLoad) {
|
|
5073
|
+
throw new Error('spiderly-data-table: [filters] requires a lazy table — client-side filtering through a store is not implemented. Remove [filters] or make the table lazy.');
|
|
5074
|
+
}
|
|
5057
5075
|
if (this.rows == null)
|
|
5058
5076
|
this.rows = this.configService.defaultPageSize;
|
|
5059
5077
|
this.activeViewId ??= this.views?.[0]?.id ?? null;
|
|
@@ -5071,23 +5089,31 @@ class SpiderlyDataTableComponent {
|
|
|
5071
5089
|
this.clientLoad();
|
|
5072
5090
|
}
|
|
5073
5091
|
this.mergeActivePageSizesIntoOptions();
|
|
5092
|
+
// AFTER `resolvedStateKey` is derived, which is what every restore below keys off — placed
|
|
5093
|
+
// above it they read a null key and silently restore nothing. The view restore runs FIRST,
|
|
5094
|
+
// because the other three read the `viewScope` it decides. And all of it before the effect
|
|
5095
|
+
// is armed, so the first request already carries the filters and the effect's skipped first
|
|
5096
|
+
// run sees the state it will be watching.
|
|
5097
|
+
const restoredView = this.restoreActiveView();
|
|
5074
5098
|
this.restoreColumnVisibility();
|
|
5075
5099
|
this.restoreColumnLayout();
|
|
5076
|
-
|
|
5077
|
-
// first, they read a null key and silently restored nothing. And before the effect is armed,
|
|
5078
|
-
// so the first request already carries the filters and the effect's skipped first run sees
|
|
5079
|
-
// the state it will be watching.
|
|
5080
|
-
this.restoreAppliedFilters();
|
|
5100
|
+
this.enterViewFilters(restoredView, 'refresh');
|
|
5081
5101
|
if (this.filters)
|
|
5082
5102
|
this.requeryOnAppliedFilters();
|
|
5083
5103
|
this.chooserCols = this.cols.filter(SpiderlyDataTableComponent.isDataColumn);
|
|
5084
5104
|
this.refreshVisibleCols();
|
|
5085
5105
|
// Bound to p-table's [multiSortMeta] so the FIRST request already carries the right
|
|
5086
|
-
// sort.
|
|
5087
|
-
//
|
|
5088
|
-
//
|
|
5106
|
+
// sort. The view's own sort wins; the persisted table-global sort sits deliberately
|
|
5107
|
+
// BETWEEN it and the table default, so a view declaring nothing keeps the ordering the
|
|
5108
|
+
// operator last chose. We read it all ourselves because PrimeNG's restoreState() runs on
|
|
5109
|
+
// the first [value] change — after the initial lazy emit has already left — and is then
|
|
5110
|
+
// told to agree (see `alignPersistedTableStateSort`).
|
|
5111
|
+
const tableState = this.persistedTableState();
|
|
5089
5112
|
this.initialMultiSortMeta =
|
|
5090
|
-
this.
|
|
5113
|
+
this.viewSort() ??
|
|
5114
|
+
this.sanitizedSortMeta(tableState?.multiSortMeta) ??
|
|
5115
|
+
this.tableDefaultMultiSortMeta();
|
|
5116
|
+
this.alignPersistedTableStateSort(tableState);
|
|
5091
5117
|
}
|
|
5092
5118
|
/** A data column shows values for a `field`; anything else (the actions column) always renders. */
|
|
5093
5119
|
static isDataColumn(col) {
|
|
@@ -5225,24 +5251,48 @@ class SpiderlyDataTableComponent {
|
|
|
5225
5251
|
}
|
|
5226
5252
|
this.persistColumnVisibility();
|
|
5227
5253
|
this.refreshVisibleCols();
|
|
5254
|
+
if (!visible)
|
|
5255
|
+
this.dropHiddenColumnSort([col]);
|
|
5228
5256
|
}
|
|
5229
5257
|
/**
|
|
5230
5258
|
* Restores every column to its declared default and forgets the stored overrides — visibility,
|
|
5231
5259
|
* wrap, order and width alike. It undid visibility only until the header menu shipped three
|
|
5232
5260
|
* more gestures beside it, and a layout with no way back is worse than one with no knobs.
|
|
5233
5261
|
*
|
|
5234
|
-
*
|
|
5235
|
-
* applied filter and the sort chip names the sort, hidden column or not
|
|
5236
|
-
*
|
|
5262
|
+
* On a STORE table, hiding — here or from the menu — never touches filters or sort: the chip
|
|
5263
|
+
* bar names every applied filter and the sort chip names the sort, hidden column or not
|
|
5264
|
+
* (decision 2b). A storeless table keeps the sort half of the legacy invariant instead — see
|
|
5265
|
+
* `dropHiddenColumnSort`.
|
|
5237
5266
|
*/
|
|
5238
5267
|
resetColumnLayout() {
|
|
5239
5268
|
this.columnWrap = {};
|
|
5240
5269
|
this.columnOrder = [];
|
|
5241
5270
|
this.columnWidths = {};
|
|
5242
5271
|
this.persistColumnLayout();
|
|
5272
|
+
const wasVisible = this.visibleCols.filter(SpiderlyDataTableComponent.isDataColumn);
|
|
5243
5273
|
this.columnVisibilityOverrides = {};
|
|
5244
5274
|
this.persistColumnVisibility();
|
|
5245
5275
|
this.refreshVisibleCols();
|
|
5276
|
+
// Columns the reset just hid follow the same rule as a manual hide.
|
|
5277
|
+
this.dropHiddenColumnSort(wasVisible.filter((col) => !this.isColumnVisible(col)));
|
|
5278
|
+
}
|
|
5279
|
+
/**
|
|
5280
|
+
* The sort half of the legacy "hidden contributes nothing" invariant, kept ONLY for storeless
|
|
5281
|
+
* tables (the generated M2M details grids, until spiderly#407 scaffolds their stores): they
|
|
5282
|
+
* have no sort chip, so a hidden column's sort would order the grid with nothing on screen
|
|
5283
|
+
* naming it and no control left to clear it. The filter half needs no heir — `lazyLoad`
|
|
5284
|
+
* overwrites the outgoing filters unconditionally. On a store table this is a no-op:
|
|
5285
|
+
* decision 2b's chips are the visible surface, hidden column or not.
|
|
5286
|
+
*/
|
|
5287
|
+
dropHiddenColumnSort(cols) {
|
|
5288
|
+
if (!this.table || this.filters)
|
|
5289
|
+
return;
|
|
5290
|
+
const sorted = cols.filter((col) => this.table._multiSortMeta?.some((m) => m.field === col.field));
|
|
5291
|
+
if (!sorted.length)
|
|
5292
|
+
return;
|
|
5293
|
+
this.table._multiSortMeta = this.table._multiSortMeta.filter((m) => !sorted.some((col) => col.field === m.field));
|
|
5294
|
+
this.table.tableService.onSort(this.table._multiSortMeta);
|
|
5295
|
+
this.table._filter(); // re-emits the lazy load and saves the cleaned state
|
|
5246
5296
|
}
|
|
5247
5297
|
/**
|
|
5248
5298
|
* Layout keys carry the ACTIVE VIEW, which is what makes a view more than a saved filter: a
|
|
@@ -5271,6 +5321,65 @@ class SpiderlyDataTableComponent {
|
|
|
5271
5321
|
get filterStorage() {
|
|
5272
5322
|
return this.stateStorage === 'local' ? localStorage : sessionStorage;
|
|
5273
5323
|
}
|
|
5324
|
+
/**
|
|
5325
|
+
* Where the active view id lives. Deliberately NOT view-scoped — it is the input `viewScope`
|
|
5326
|
+
* derives from — and it follows `stateStorage` like the filters rather than the layout's
|
|
5327
|
+
* always-local rule, because the id and the filters it scopes are one question: a durable id
|
|
5328
|
+
* over session-scoped filters would restore a tab with nothing under it.
|
|
5329
|
+
*/
|
|
5330
|
+
get viewStateKey() {
|
|
5331
|
+
return this.resolvedStateKey ? `${this.resolvedStateKey}:view` : null;
|
|
5332
|
+
}
|
|
5333
|
+
/** Only `selectView` writes; the `ngOnInit` seed does not — absence means "the default". */
|
|
5334
|
+
persistActiveView() {
|
|
5335
|
+
if (!this.viewStateKey || !this.activeViewId)
|
|
5336
|
+
return;
|
|
5337
|
+
writeStoredJson(this.filterStorage, this.viewStateKey, this.activeViewId);
|
|
5338
|
+
}
|
|
5339
|
+
/**
|
|
5340
|
+
* Puts a reload back on the view the operator was using. Every persisted key carries the view
|
|
5341
|
+
* segment, so before this existed the seed decided what got restored: F5 landed on the first
|
|
5342
|
+
* view and read `…:all:filters` — the tab snapped to "Sve" and the bar came back empty while
|
|
5343
|
+
* the operator's filters sat intact under the old view's segment (Filip, on /porudzbine).
|
|
5344
|
+
*
|
|
5345
|
+
* An id naming no current view keeps the seed AND the stored key: "vanished by deploy" and
|
|
5346
|
+
* "not yet arrived" are indistinguishable here (order-list's catalog-licensed "Za pakovanje"
|
|
5347
|
+
* is literally both), so `adoptStoredView` re-reads the key on every `[views]` change and the
|
|
5348
|
+
* key self-heals on the next explicit click.
|
|
5349
|
+
*/
|
|
5350
|
+
restoreActiveView() {
|
|
5351
|
+
if (!this.viewStateKey || !this.views?.length)
|
|
5352
|
+
return null;
|
|
5353
|
+
const storedId = readStoredJson(this.filterStorage, this.viewStateKey);
|
|
5354
|
+
if (typeof storedId !== 'string')
|
|
5355
|
+
return null;
|
|
5356
|
+
const view = this.viewById(storedId) ?? null;
|
|
5357
|
+
if (view)
|
|
5358
|
+
this.activeViewId = view.id;
|
|
5359
|
+
return view;
|
|
5360
|
+
}
|
|
5361
|
+
/**
|
|
5362
|
+
* The second half of `restoreActiveView`: a stored view that did not exist at init is adopted
|
|
5363
|
+
* when a later `[views]` change brings it — through `selectView`, so it gets the same
|
|
5364
|
+
* stored-wins/transient/layout/requery semantics a click would have had. There is no parked
|
|
5365
|
+
* copy of the stored id — the key itself is re-read, and because every explicit pick
|
|
5366
|
+
* overwrites it, the operator's own choice wins by construction. Accepted cost of adopting
|
|
5367
|
+
* late instead of holding the first request: a brief flash of the seeded view's rows and a
|
|
5368
|
+
* second request when the late view lands.
|
|
5369
|
+
*/
|
|
5370
|
+
adoptStoredView() {
|
|
5371
|
+
if (!this.viewStateKey)
|
|
5372
|
+
return;
|
|
5373
|
+
const storedId = readStoredJson(this.filterStorage, this.viewStateKey);
|
|
5374
|
+
if (typeof storedId !== 'string' || storedId === this.activeViewId)
|
|
5375
|
+
return;
|
|
5376
|
+
const view = this.viewById(storedId);
|
|
5377
|
+
if (view)
|
|
5378
|
+
this.selectView(view);
|
|
5379
|
+
}
|
|
5380
|
+
viewById(id) {
|
|
5381
|
+
return this.views?.find((candidate) => candidate.id === id);
|
|
5382
|
+
}
|
|
5274
5383
|
persistAppliedFilters() {
|
|
5275
5384
|
if (!this.filtersStateKey || !this.filters)
|
|
5276
5385
|
return;
|
|
@@ -5282,12 +5391,27 @@ class SpiderlyDataTableComponent {
|
|
|
5282
5391
|
writeStoredJson(this.filterStorage, this.filtersStateKey, snapshot);
|
|
5283
5392
|
}
|
|
5284
5393
|
}
|
|
5285
|
-
|
|
5286
|
-
|
|
5394
|
+
/**
|
|
5395
|
+
* Applies the right filters for entering `view` — the transient rule's one home. A TRANSIENT
|
|
5396
|
+
* view's `apply` is a function of NOW, so it always re-derives; restoring would put
|
|
5397
|
+
* yesterday's date under a tab claiming "today" (`persistAppliedFilters` has no transient
|
|
5398
|
+
* guard, so storage holds the LAST answer). A non-transient view's own stored answer wins
|
|
5399
|
+
* over its `apply`. The semantics argument is the one deliberate difference between the
|
|
5400
|
+
* callers: a 'click' re-asks the view's question when nothing is stored, a 'refresh' returns
|
|
5401
|
+
* the empty state the operator left — they cleared it, and F5 must give the cleared state
|
|
5402
|
+
* back. On a table with no views, `view` is null and this is a plain restore of the
|
|
5403
|
+
* `persistAppliedFilters` snapshot.
|
|
5404
|
+
*/
|
|
5405
|
+
enterViewFilters(view, semantics) {
|
|
5406
|
+
if (!this.filters)
|
|
5287
5407
|
return;
|
|
5288
|
-
const
|
|
5289
|
-
|
|
5290
|
-
|
|
5408
|
+
const stored = !view?.transient && this.filtersStateKey
|
|
5409
|
+
? readStoredJson(this.filterStorage, this.filtersStateKey)
|
|
5410
|
+
: null;
|
|
5411
|
+
if (stored)
|
|
5412
|
+
this.filters.restore(stored);
|
|
5413
|
+
else if (view?.transient || semantics === 'click')
|
|
5414
|
+
view?.apply?.(this.filters);
|
|
5291
5415
|
}
|
|
5292
5416
|
get layoutStateKey() {
|
|
5293
5417
|
return this.resolvedStateKey
|
|
@@ -5344,25 +5468,135 @@ class SpiderlyDataTableComponent {
|
|
|
5344
5468
|
return null;
|
|
5345
5469
|
return readStoredJson(this.stateStorage === 'local' ? localStorage : sessionStorage, this.resolvedStateKey);
|
|
5346
5470
|
}
|
|
5471
|
+
activeView() {
|
|
5472
|
+
return this.activeViewId ? this.viewById(this.activeViewId) : undefined;
|
|
5473
|
+
}
|
|
5474
|
+
/**
|
|
5475
|
+
* Where the operator's per-view sort override lives. Null on a table without views — those
|
|
5476
|
+
* keep the pure PrimeNG sort persistence they always had — and null on a STORELESS table:
|
|
5477
|
+
* there "hidden contributes nothing" retires a hidden column's sort (`dropHiddenColumnSort`)
|
|
5478
|
+
* with no persist call at the hide site, so an override restored blind at the next init
|
|
5479
|
+
* would re-impose a sort no chip can name. Store tables — where every viewed consumer
|
|
5480
|
+
* lives — have the chip. Follows `stateStorage` like the filters and the view id: the sort
|
|
5481
|
+
* is part of the same question they scope.
|
|
5482
|
+
*/
|
|
5483
|
+
get viewSortStateKey() {
|
|
5484
|
+
if (!this.views?.length || !this.filters)
|
|
5485
|
+
return null;
|
|
5486
|
+
return this.resolvedStateKey
|
|
5487
|
+
? `${this.resolvedStateKey}${this.viewScope}:sort`
|
|
5488
|
+
: null;
|
|
5489
|
+
}
|
|
5490
|
+
/** The operator's stored sort for the active view, sanitized like any persisted sort. */
|
|
5491
|
+
persistedViewSort() {
|
|
5492
|
+
if (!this.viewSortStateKey)
|
|
5493
|
+
return null;
|
|
5494
|
+
const stored = readStoredJson(this.filterStorage, this.viewSortStateKey);
|
|
5495
|
+
return Array.isArray(stored) ? this.sanitizedSortMeta(stored) : null;
|
|
5496
|
+
}
|
|
5497
|
+
/** The sort this view asks for: the operator's stored override, else its declared sort. */
|
|
5498
|
+
viewSort() {
|
|
5499
|
+
return this.persistedViewSort() ?? this.viewDeclaredMultiSortMeta();
|
|
5500
|
+
}
|
|
5501
|
+
/**
|
|
5502
|
+
* Records the current sort as the active view's override — called from the EXPLICIT sort
|
|
5503
|
+
* gestures only (header click, menu sort, Clear filters), never from a load: a sort merely
|
|
5504
|
+
* inherited at init must not crystallize into an override, or a later change to the view's
|
|
5505
|
+
* declared sort would stop flowing to operators who never touched sort — the same
|
|
5506
|
+
* flows-through rule `columnVisibilityOverrides` documents. A sort equal to the declared
|
|
5507
|
+
* default is stored as ABSENCE for the same reason.
|
|
5508
|
+
*/
|
|
5509
|
+
persistActiveViewSort() {
|
|
5510
|
+
const key = this.viewSortStateKey;
|
|
5511
|
+
if (!key)
|
|
5512
|
+
return;
|
|
5513
|
+
const current = this.table._multiSortMeta ?? [];
|
|
5514
|
+
if (sortMetaEquals(current, this.defaultMultiSortMeta() ?? [])) {
|
|
5515
|
+
this.filterStorage.removeItem(key);
|
|
5516
|
+
}
|
|
5517
|
+
else {
|
|
5518
|
+
writeStoredJson(this.filterStorage, key, current);
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
/**
|
|
5522
|
+
* Puts the entering view's sort on the grid — stored override first, then the declared sort;
|
|
5523
|
+
* a view stating no preference keeps the ordering already there (the pre-decision-10-sort
|
|
5524
|
+
* behavior, and the least surprising for the tab an operator never sorted). Broadcast only —
|
|
5525
|
+
* the caller owns the refetch, because which request carries the new sort differs between the
|
|
5526
|
+
* store and storeless paths.
|
|
5527
|
+
*/
|
|
5528
|
+
applyViewSort() {
|
|
5529
|
+
const target = this.viewSort();
|
|
5530
|
+
if (!target)
|
|
5531
|
+
return false;
|
|
5532
|
+
if (sortMetaEquals(target, this.table._multiSortMeta ?? []))
|
|
5533
|
+
return false;
|
|
5534
|
+
this.broadcastSort(target);
|
|
5535
|
+
return true;
|
|
5536
|
+
}
|
|
5537
|
+
/**
|
|
5538
|
+
* Rewrites the sort inside PrimeNG's persisted state to the one `ngOnInit` resolved. The two
|
|
5539
|
+
* can disagree only on a viewed table — PrimeNG's state is table-global while the resolved
|
|
5540
|
+
* sort is the view's — and PrimeNG's restoreState() applies its copy on the first [value]
|
|
5541
|
+
* change, AFTER the initial request already left with ours: without this line the grid's
|
|
5542
|
+
* arrows and every later request silently fall back to the other view's sort. Handed the
|
|
5543
|
+
* already-parsed state: `ngOnInit` reads the blob once for the whole init pass.
|
|
5544
|
+
*/
|
|
5545
|
+
alignPersistedTableStateSort(state) {
|
|
5546
|
+
if (!this.views?.length || !this.resolvedStateKey || !state)
|
|
5547
|
+
return;
|
|
5548
|
+
const resolved = this.initialMultiSortMeta ?? [];
|
|
5549
|
+
if (sortMetaEquals(state.multiSortMeta ?? [], resolved))
|
|
5550
|
+
return;
|
|
5551
|
+
state.multiSortMeta = resolved.length ? resolved : undefined;
|
|
5552
|
+
writeStoredJson(this.filterStorage, this.resolvedStateKey, state);
|
|
5553
|
+
}
|
|
5347
5554
|
//#endregion
|
|
5348
5555
|
/**
|
|
5349
|
-
* The
|
|
5350
|
-
*
|
|
5351
|
-
*
|
|
5556
|
+
* The DECLARED default in the current context: the active view's own sort when it declares
|
|
5557
|
+
* one, else the table-level `defaultSortField`. Every "fall back to the declared default"
|
|
5558
|
+
* site — tri-state un-sort, Clear filters, the unsorted-load safety net — reads this, so
|
|
5559
|
+
* un-sorting inside a view lands on the view's ordering, not the table's.
|
|
5352
5560
|
*/
|
|
5353
5561
|
defaultMultiSortMeta() {
|
|
5562
|
+
return this.viewDeclaredMultiSortMeta() ?? this.tableDefaultMultiSortMeta();
|
|
5563
|
+
}
|
|
5564
|
+
/**
|
|
5565
|
+
* The active view's declared sort, sanitized: a non-sortable field would answer every load
|
|
5566
|
+
* with a 400 (`keepSortableMeta`'s rationale), so it is dropped rather than sent; a view
|
|
5567
|
+
* whose declaration sanitizes to nothing falls through to the table default.
|
|
5568
|
+
*/
|
|
5569
|
+
viewDeclaredMultiSortMeta() {
|
|
5570
|
+
return this.sanitizedSortMeta(this.activeView()?.sort);
|
|
5571
|
+
}
|
|
5572
|
+
/**
|
|
5573
|
+
* The table-level declared default as PrimeNG sort meta, or null when none is declared. On a
|
|
5574
|
+
* store table it applies even while its column is hidden — the sort chip names the ordering
|
|
5575
|
+
* whether or not the column is on screen (decision 2b). A STORELESS table has no chip, so a
|
|
5576
|
+
* hidden default would order the grid with nothing on screen naming it; there the default
|
|
5577
|
+
* dies with the column, exactly as the legacy invariant had it (see `dropHiddenColumnSort`).
|
|
5578
|
+
*/
|
|
5579
|
+
tableDefaultMultiSortMeta() {
|
|
5354
5580
|
if (!this.defaultSortField)
|
|
5355
5581
|
return null;
|
|
5356
5582
|
const defaultSortCol = this.cols?.find((col) => col.field === this.defaultSortField);
|
|
5583
|
+
if (defaultSortCol &&
|
|
5584
|
+
!this.filters &&
|
|
5585
|
+
!this.isColumnVisible(defaultSortCol))
|
|
5586
|
+
return null;
|
|
5357
5587
|
// A declared default on a non-sortable column is a consumer mistake the backend answers
|
|
5358
5588
|
// with a 400 on every load; fall back to its implicit Id DESC instead (see keepSortableMeta).
|
|
5359
5589
|
if (defaultSortCol && !this.isColumnSortable(defaultSortCol))
|
|
5360
5590
|
return null;
|
|
5361
5591
|
return [{ field: this.defaultSortField, order: this.defaultSortOrder }];
|
|
5362
5592
|
}
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5593
|
+
/**
|
|
5594
|
+
* `keepSortableMeta` with "sanitized to nothing" surfaced as null — the rule every sort
|
|
5595
|
+
* SOURCE shares ("a sort that sanitizes to nothing is no sort"), so the resolution chains
|
|
5596
|
+
* can `??` past an empty answer instead of stopping on an empty array.
|
|
5597
|
+
*/
|
|
5598
|
+
sanitizedSortMeta(multiSortMeta) {
|
|
5599
|
+
const sortable = this.keepSortableMeta(multiSortMeta);
|
|
5366
5600
|
return sortable.length ? sortable : null;
|
|
5367
5601
|
}
|
|
5368
5602
|
/**
|
|
@@ -5393,10 +5627,13 @@ class SpiderlyDataTableComponent {
|
|
|
5393
5627
|
if (defaultSort == null)
|
|
5394
5628
|
return;
|
|
5395
5629
|
event.multiSortMeta = defaultSort;
|
|
5396
|
-
if (this.table)
|
|
5397
|
-
this.
|
|
5398
|
-
|
|
5399
|
-
|
|
5630
|
+
if (this.table)
|
|
5631
|
+
this.broadcastSort(defaultSort);
|
|
5632
|
+
}
|
|
5633
|
+
/** Puts `meta` on the grid and tells the header icons — no fetch; the caller owns that. */
|
|
5634
|
+
broadcastSort(meta) {
|
|
5635
|
+
this.table._multiSortMeta = meta;
|
|
5636
|
+
this.table.tableService.onSort(meta);
|
|
5400
5637
|
}
|
|
5401
5638
|
onPageChange(container) {
|
|
5402
5639
|
scrollElementIntoViewIfAboveViewport(container);
|
|
@@ -5558,25 +5795,35 @@ class SpiderlyDataTableComponent {
|
|
|
5558
5795
|
*/
|
|
5559
5796
|
selectView(view) {
|
|
5560
5797
|
this.activeViewId = view.id;
|
|
5798
|
+
// Always writes — including the first view, so a deliberate "Sve" during a deploy race
|
|
5799
|
+
// sticks, and so an explicit pick beats a stored id whose view has not arrived yet
|
|
5800
|
+
// (`adoptStoredView` compares against this key).
|
|
5801
|
+
this.persistActiveView();
|
|
5561
5802
|
// The keys just changed under us, so the layout has to be re-read for the view being entered
|
|
5562
5803
|
// — otherwise the one being left keeps rendering until something else forces a reload.
|
|
5563
5804
|
this.restoreColumnVisibility();
|
|
5564
5805
|
this.restoreColumnLayout();
|
|
5565
5806
|
this.refreshVisibleCols();
|
|
5566
|
-
|
|
5807
|
+
// Sort is view state too (decision 10's sort half). Swapped BEFORE the filter transition so
|
|
5808
|
+
// the one request the requery effect sends carries both.
|
|
5809
|
+
const sortChanged = this.applyViewSort();
|
|
5810
|
+
if (!this.filters) {
|
|
5811
|
+
if (sortChanged)
|
|
5812
|
+
this.table.sortMultiple();
|
|
5567
5813
|
return;
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
//
|
|
5571
|
-
//
|
|
5814
|
+
}
|
|
5815
|
+
const appliedBefore = this.filters.applied();
|
|
5816
|
+
// The filter key moved with the view too; `enterViewFilters` holds the stored-wins and
|
|
5817
|
+
// transient rules.
|
|
5572
5818
|
this.filters.clear();
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5819
|
+
this.enterViewFilters(view, 'click');
|
|
5820
|
+
// The requery effect runs only when the applied set actually changed — `clear()` on an
|
|
5821
|
+
// already-empty store writes nothing (two views sharing one filter answer are exactly that)
|
|
5822
|
+
// — so a sort-only switch fetches here. `applied` is a computed: reference equality across
|
|
5823
|
+
// the transition means none of its sources moved.
|
|
5824
|
+
if (sortChanged && this.filters.applied() === appliedBefore) {
|
|
5825
|
+
this.refetchFromFirstPage();
|
|
5826
|
+
}
|
|
5580
5827
|
}
|
|
5581
5828
|
/**
|
|
5582
5829
|
* The filter this column stands for. Defaults to the column's own `field`, because a store id
|
|
@@ -5627,6 +5874,7 @@ class SpiderlyDataTableComponent {
|
|
|
5627
5874
|
return;
|
|
5628
5875
|
this.table._multiSortMeta = [{ field: col.field, order }];
|
|
5629
5876
|
this.table.sortMultiple();
|
|
5877
|
+
this.persistActiveViewSort();
|
|
5630
5878
|
}
|
|
5631
5879
|
/**
|
|
5632
5880
|
* Sizes the column to the widest thing in it. The cheaper escape from a cramped column: no
|
|
@@ -6003,11 +6251,24 @@ class SpiderlyDataTableComponent {
|
|
|
6003
6251
|
return;
|
|
6004
6252
|
}
|
|
6005
6253
|
this.persistAppliedFilters();
|
|
6006
|
-
this.
|
|
6007
|
-
this.table.firstChange.emit(0);
|
|
6008
|
-
this.lazyLoad(this.table.createLazyLoadMetadata());
|
|
6254
|
+
this.refetchFromFirstPage();
|
|
6009
6255
|
}, { injector: this.injector });
|
|
6010
6256
|
}
|
|
6257
|
+
/**
|
|
6258
|
+
* Back to page one, state saved, one fetch off FRESH metadata — the shared tail of the
|
|
6259
|
+
* requery effect and `selectView`'s sort-only switch. The save is load-bearing: PrimeNG
|
|
6260
|
+
* saves state only from its own interactions, so without it the pre-change page offset
|
|
6261
|
+
* survives in storage and the NEXT visit replays it against a narrowed result set — the
|
|
6262
|
+
* backend answers data=[] for an offset the filtered total no longer reaches, and the grid
|
|
6263
|
+
* renders empty under chips claiming matches.
|
|
6264
|
+
*/
|
|
6265
|
+
refetchFromFirstPage() {
|
|
6266
|
+
this.table.first = 0;
|
|
6267
|
+
this.table.firstChange.emit(0);
|
|
6268
|
+
if (this.table.isStateful())
|
|
6269
|
+
this.table.saveState();
|
|
6270
|
+
this.lazyLoad(this.table.createLazyLoadMetadata());
|
|
6271
|
+
}
|
|
6011
6272
|
reload() {
|
|
6012
6273
|
// Nothing to replay before the table's own initial load, which is already on its way.
|
|
6013
6274
|
if (this.lastLazyLoadEvent == null)
|
|
@@ -6116,6 +6377,10 @@ class SpiderlyDataTableComponent {
|
|
|
6116
6377
|
this.filters?.clear();
|
|
6117
6378
|
table.clear();
|
|
6118
6379
|
table.clearState();
|
|
6380
|
+
// `table.clear()` landed the grid on the declared default (`applyDefaultSortIfUnsorted`),
|
|
6381
|
+
// so the view's sort override must go with it — or F5 would bring back the sort Clear
|
|
6382
|
+
// visibly removed.
|
|
6383
|
+
this.persistActiveViewSort();
|
|
6119
6384
|
}
|
|
6120
6385
|
//#region Selection
|
|
6121
6386
|
setFakeIsAllSelected() {
|
|
@@ -6415,12 +6680,13 @@ class Action {
|
|
|
6415
6680
|
}
|
|
6416
6681
|
}
|
|
6417
6682
|
class Column {
|
|
6418
|
-
constructor({ name, field, filterType, filterId, showMatchModes, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
|
|
6683
|
+
constructor({ name, field, filterType, filterId, showMatchModes, filterField, dropdownOrMultiselectValues, actions, editable, showTime, decimalPlaces, sortable, visible, lockVisible, width, onCellClick, } = {}) {
|
|
6419
6684
|
this.name = name;
|
|
6420
6685
|
this.field = field;
|
|
6421
6686
|
this.filterType = filterType;
|
|
6422
6687
|
this.filterId = filterId;
|
|
6423
6688
|
this.showMatchModes = showMatchModes;
|
|
6689
|
+
this.filterField = filterField;
|
|
6424
6690
|
this.dropdownOrMultiselectValues = dropdownOrMultiselectValues;
|
|
6425
6691
|
this.actions = actions;
|
|
6426
6692
|
this.editable = editable;
|
|
@@ -6462,10 +6728,11 @@ var MatchModeCodes;
|
|
|
6462
6728
|
* paginator implements per type (`PaginatedResultGenerator`): `In` on a string column answers
|
|
6463
6729
|
* with InvalidMatchMode, PrimeNG's own text list adds three modes the backend 400s, and so on.
|
|
6464
6730
|
*
|
|
6465
|
-
* Deliberately a LEAF module (imports nothing but the codes) so
|
|
6466
|
-
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`),
|
|
6467
|
-
*
|
|
6468
|
-
* hand-kept copies; edit the
|
|
6731
|
+
* Deliberately a LEAF module (imports nothing but the codes) so every consumer can derive from
|
|
6732
|
+
* it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), the
|
|
6733
|
+
* compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`), and the data view's
|
|
6734
|
+
* kind-level lists (via `operatorOptionsForKind`). These used to be hand-kept copies; edit the
|
|
6735
|
+
* table here and every surface follows.
|
|
6469
6736
|
*
|
|
6470
6737
|
* Tuple order is DISPLAY order for every operator picker (Equals, then "less/before", then
|
|
6471
6738
|
* "more/after" — the order the header dropdowns always shipped).
|
|
@@ -6563,6 +6830,17 @@ function toOperatorOptions(kind, operators) {
|
|
|
6563
6830
|
labelKey: OPERATOR_WORDS[kind][value]?.pickerKey ?? value,
|
|
6564
6831
|
}));
|
|
6565
6832
|
}
|
|
6833
|
+
/**
|
|
6834
|
+
* A KIND's operator picker options, for callers with no filter behind them — today that is
|
|
6835
|
+
* `spiderly-data-view`, whose own filter config derives its operator lists here instead of
|
|
6836
|
+
* hand-keeping copies (the drift `allowed-operators.ts` exists to end). Per-FILTER narrowing
|
|
6837
|
+
* lives in the store (`createFilterStore` resolves it once per declaration), so this cannot be
|
|
6838
|
+
* handed a fake definition to satisfy — that shape shipped once and interpolated an empty
|
|
6839
|
+
* filter name into its own error message.
|
|
6840
|
+
*/
|
|
6841
|
+
function operatorOptionsForKind(kind, hasOptions = false) {
|
|
6842
|
+
return toOperatorOptions(kind, baseOperators(kind, hasOptions));
|
|
6843
|
+
}
|
|
6566
6844
|
/**
|
|
6567
6845
|
* The operator applied when nobody picked one, which has to be what a person MEANS by filling in
|
|
6568
6846
|
* the control. `Contains` for a fragment typed into a box; `GreaterThan` for a date, never
|
|
@@ -6916,34 +7194,15 @@ class SpiderlyDataViewComponent {
|
|
|
6916
7194
|
};
|
|
6917
7195
|
}
|
|
6918
7196
|
ngOnInit() {
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
{
|
|
6929
|
-
label: this.translocoService.translate('DatesAfter'),
|
|
6930
|
-
value: MatchModeCodes.GreaterThan,
|
|
6931
|
-
},
|
|
6932
|
-
];
|
|
6933
|
-
this.matchModeNumberOptions = [
|
|
6934
|
-
{
|
|
6935
|
-
label: this.translocoService.translate('Equals'),
|
|
6936
|
-
value: MatchModeCodes.Equals,
|
|
6937
|
-
},
|
|
6938
|
-
{
|
|
6939
|
-
label: this.translocoService.translate('LessThan'),
|
|
6940
|
-
value: MatchModeCodes.LessThan,
|
|
6941
|
-
},
|
|
6942
|
-
{
|
|
6943
|
-
label: this.translocoService.translate('MoreThan'),
|
|
6944
|
-
value: MatchModeCodes.GreaterThan,
|
|
6945
|
-
},
|
|
6946
|
-
];
|
|
7197
|
+
// Derived from the ONE operator table (`filters/allowed-operators.ts`) rather than
|
|
7198
|
+
// hand-kept: these were the last hand-written copies of the per-kind lists, and a backend
|
|
7199
|
+
// operator change edited in the shared table would silently never have reached them.
|
|
7200
|
+
const matchModeOptionsFor = (kind) => operatorOptionsForKind(kind).map((option) => ({
|
|
7201
|
+
label: this.translocoService.translate(option.labelKey),
|
|
7202
|
+
value: option.value,
|
|
7203
|
+
}));
|
|
7204
|
+
this.matchModeDateOptions = matchModeOptionsFor('date');
|
|
7205
|
+
this.matchModeNumberOptions = matchModeOptionsFor('number');
|
|
6947
7206
|
}
|
|
6948
7207
|
// Paging semantics and the scroll-margin contract are documented once, on the data table:
|
|
6949
7208
|
// spiderly-data-table/CLAUDE.md → "Scroll back to the table on page change".
|
|
@@ -7584,5 +7843,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.13", ngImpo
|
|
|
7584
7843
|
* Generated bundle index. Do not edit.
|
|
7585
7844
|
*/
|
|
7586
7845
|
|
|
7587
|
-
export { ALLOWED_OPERATORS, Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, DEFAULT_OPERATOR, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCellTemplateDirective, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableActionsDirective, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFilterBarComponent, SpiderlyFilterTemplateDirective, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyOverflowTitleDirective, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, booleanFilter, capitalizeFirstChar, createFilterStore, dateFilter, deleteAction, exportListToExcel, firstCharToUpper, foldForSearch, getFileNameFromContentDisposition, getFilterOptions, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, numberFilter, parseDateOnlyLocal, pushAction, saveResponseAsFile, scrollElementIntoViewIfAboveViewport, selectedTab, singleOrDefault, splitPascalCase, textFilter, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
7846
|
+
export { ALLOWED_OPERATORS, Action, AllClickEvent, ApiErrorCodes, ApiSecurityService, AppSidebarComponent, AuthCardComponent, AuthGuard, AuthResult, AuthResultWithCookies, AuthServiceBase, BaseAutocompleteControl, BaseControl, BaseDropdownControl, BaseEntity, BaseFormComponent, BaseFormService, CardSkeletonComponent, Codebook, Column, ConfigServiceBase, DEFAULT_EXTERNAL_PROVIDER_ICONS, DEFAULT_OPERATOR, ExternalLoginComponent, ExternalProvider, ExternalProviderPublic, Filter, FilterRule, FilterSortMeta, IndexCardComponent, InfoCardComponent, InitCompanyAuthDialogDetails, InitTopBarData, IsAuthorizedForSaveEvent, LastMenuIconIndexClicked, LayoutServiceBase, LazyLoadSelectedIdsResult, Login, LoginVerificationComponent, LoginVerificationToken, MatchModeCodes, MenuChangeEvent, MenuitemComponent, Namebook, NotAuthGuard, NotFoundComponent, PROPS_KEY, PaginatedResult, PanelBodyComponent, PanelFooterComponent, PanelHeaderComponent, PrimengOption, ProfileAvatarComponent, ReflectProp, RefreshTokenRequest, RequiredComponent, RowClickEvent, SecurityPermissionCodes, SendLoginVerificationEmailResult, SideMenuTopBarComponent, SidebarMenuComponent, SimpleSaveResult, SpiderlyAutocompleteComponent, SpiderlyButtonBaseComponent, SpiderlyButtonComponent, SpiderlyCalendarComponent, SpiderlyCardComponent, SpiderlyCellTemplateDirective, SpiderlyCheckboxComponent, SpiderlyColorPickerComponent, SpiderlyControlsModule, SpiderlyDataTableActionsDirective, SpiderlyDataTableComponent, SpiderlyDataViewComponent, SpiderlyDeleteConfirmationComponent, SpiderlyDropdownComponent, SpiderlyEditorComponent, SpiderlyErrorHandler, SpiderlyFileComponent, SpiderlyFileSelectEvent, SpiderlyFilterBarComponent, SpiderlyFilterTemplateDirective, SpiderlyFormArray, SpiderlyFormControl, SpiderlyFormGroup, SpiderlyLayoutComponent, SpiderlyLoginComponent, SpiderlyMarkdownComponent, SpiderlyMessageService, SpiderlyMultiAutocompleteComponent, SpiderlyMultiSelectComponent, SpiderlyNumberComponent, SpiderlyOverflowTitleDirective, SpiderlyPanelComponent, SpiderlyPanelsModule, SpiderlyPasswordComponent, SpiderlyReturnButtonComponent, SpiderlySplitButtonComponent, SpiderlyTab, SpiderlyTemplateTypeDirective, SpiderlyTextareaComponent, SpiderlyTextboxComponent, SpiderlyTranslocoLoader, TopBarComponent, UserBase, UserRole, ValidatorAbstractService, VerificationTokenRequest, VerificationTypeCodes, VerificationWrapperComponent, adjustColor, authInitializer, booleanFilter, capitalizeFirstChar, createFilterStore, dateFilter, deleteAction, exportListToExcel, firstCharToUpper, foldForSearch, getFileNameFromContentDisposition, getFilterOptions, getHtmlImgDisplayString64, getImageDimensions, getMimeTypeForFileName, getMonth, getParentUrl, getPrimengAutocompleteCodebookOptions, getPrimengAutocompleteNamebookOptions, getPrimengDropdownCodebookOptions, getPrimengDropdownNamebookOptions, getPrimengNamebookOptions, httpLoadingInterceptor, isExcelFileType, isFileImageType, isNullOrEmpty, jsonHttpInterceptor, jwtInterceptor, kebabToTitleCase, nameOf, nameof, numberFilter, operatorOptionsForKind, parseDateOnlyLocal, pushAction, saveResponseAsFile, scrollElementIntoViewIfAboveViewport, selectedTab, singleOrDefault, splitPascalCase, textFilter, toCommaSeparatedString, unauthorizedInterceptor, validatePrecisionScale };
|
|
7588
7847
|
//# sourceMappingURL=spiderly.mjs.map
|