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.
@@ -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`, never left null on a
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
- originalSort(event);
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
- // AFTER `resolvedStateKey` is derived, which is what both of these key off — placed above it
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. Persisted user sort wins over the declared default; we read it ourselves
5087
- // because PrimeNG's restoreState() runs on the first [value] change — after the
5088
- // initial lazy emit has already left.
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.persistedMultiSortMeta() ?? this.defaultMultiSortMeta();
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
- * Hiding — here or from the menu — never touches filters or sort: the chip bar names every
5235
- * applied filter and the sort chip names the sort, hidden column or not (decision 2b). The
5236
- * legacy "hidden contributes nothing" apparatus died with the header-filter path.
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
- restoreAppliedFilters() {
5286
- if (!this.filtersStateKey || !this.filters)
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 snapshot = readStoredJson(this.filterStorage, this.filtersStateKey);
5289
- if (snapshot)
5290
- this.filters.restore(snapshot);
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 declared default as PrimeNG sort meta, or null when none is declared. Applies even
5350
- * while its column is hidden: the sort chip names the ordering whether or not the column
5351
- * is on screen (decision 2b).
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
- persistedMultiSortMeta() {
5364
- const state = this.persistedTableState();
5365
- const sortable = this.keepSortableMeta(state?.multiSortMeta);
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.table._multiSortMeta = defaultSort;
5398
- this.table.tableService.onSort(defaultSort);
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
- if (!this.filters)
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
- // The filter key moved with the view too, so this view's own stored answer wins over whatever
5569
- // the last one left applied; `apply` runs only when the view has nothing stored yet. A
5570
- // TRANSIENT view inverts that: its apply is a function of now, so it always re-derives —
5571
- // restoring would put yesterday's date under a tab claiming "today".
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
- const stored = !view.transient && this.filtersStateKey
5574
- ? readStoredJson(this.filterStorage, this.filtersStateKey)
5575
- : null;
5576
- if (stored)
5577
- this.filters.restore(stored);
5578
- else
5579
- view.apply?.(this.filters);
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.table.first = 0;
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 both consumers can derive from
6466
- * it without a cycle: the store's runtime validation and offered lists (`filter-store.ts`), and
6467
- * the compile-time `FilterRule.matchMode` union (`entities/filter-rule.ts`). These used to be
6468
- * hand-kept copies; edit the table here and every surface follows.
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
- this.matchModeDateOptions = [
6920
- {
6921
- label: this.translocoService.translate('OnDate'),
6922
- value: MatchModeCodes.Equals,
6923
- },
6924
- {
6925
- label: this.translocoService.translate('DatesBefore'),
6926
- value: MatchModeCodes.LessThan,
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