@lavalogic/scoria 0.37.55 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/Components/Table/ARCHITECTURE.md +22 -22
  2. package/dist/Components/Table/Misc/ColumnPanel.svelte +1 -61
  3. package/dist/Components/Table/Misc/ColumnPanel.svelte.d.ts +2 -24
  4. package/dist/Components/Table/Misc/ColumnPanelModal.svelte +3 -92
  5. package/dist/Components/Table/Misc/ColumnPanelModal.svelte.d.ts +2 -24
  6. package/dist/Components/Table/Misc/ColumnPanelModalProps.d.ts +2 -2
  7. package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -0
  8. package/dist/Components/Table/Misc/TableConfigurationModal.svelte.d.ts +26 -0
  9. package/dist/Components/Table/Misc/TableConfigurationModalProps.d.ts +13 -0
  10. package/dist/Components/Table/Misc/TableHorizontalBar.svelte +27 -0
  11. package/dist/Components/Table/Misc/TableSidebar.svelte +45 -2
  12. package/dist/Components/Table/Misc/TableViewDropdown.svelte +175 -0
  13. package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +25 -0
  14. package/dist/Components/Table/SubApis.svelte.js +0 -41
  15. package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +5 -5
  16. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +5 -5
  17. package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +5 -5
  18. package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +106 -147
  19. package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +156 -203
  20. package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +323 -44
  21. package/dist/Components/Table/Types/Context/TableContext.svelte.js +687 -187
  22. package/dist/Components/Table/Types/Context/TableInitOptions.d.ts +9 -0
  23. package/dist/Components/Table/Types/Context/ViewState.svelte.d.ts +152 -0
  24. package/dist/Components/Table/Types/Context/ViewState.svelte.js +223 -0
  25. package/dist/Components/Table/Types/ExpandedPanel.d.ts +9 -5
  26. package/dist/Components/Table/Types/ExpandedPanel.js +9 -5
  27. package/dist/Components/Table/Types/Persistence/DatatableView.d.ts +95 -0
  28. package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.d.ts +21 -0
  29. package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.js +1 -0
  30. package/dist/Components/Table/Types/Persistence/DatatableViewKind.d.ts +10 -0
  31. package/dist/Components/Table/Types/Persistence/DatatableViewKind.js +1 -0
  32. package/dist/Components/Table/Types/Persistence/JSONActiveView.d.ts +58 -0
  33. package/dist/Components/Table/Types/Persistence/JSONActiveView.js +41 -0
  34. package/dist/Components/Table/Types/Persistence/JSONTableFilter.d.ts +53 -0
  35. package/dist/Components/Table/Types/Persistence/JSONTableFilter.js +42 -0
  36. package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.d.ts +47 -0
  37. package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.js +1 -0
  38. package/dist/Components/Table/Types/Public/CreateTableOptions.d.ts +10 -0
  39. package/dist/Components/Table/Types/Public/TablePersistence.d.ts +1 -4
  40. package/dist/Components/Table/Types/Public/TableSubApis.d.ts +0 -18
  41. package/dist/Components/Table/Types/Public/index.d.ts +1 -1
  42. package/dist/Components/Table/createTable.svelte.js +2 -0
  43. package/dist/index.d.ts +8 -1
  44. package/dist/index.js +9 -0
  45. package/package.json +1 -1
  46. package/dist/Components/Table/Types/Columns/Definitions/ColumnDefSet.d.ts +0 -20
  47. package/dist/Components/Table/Types/Columns/Definitions/JSONColumnDefSet.d.ts +0 -25
  48. /package/dist/Components/Table/{Types/Columns/Definitions/ColumnDefSet.js → Misc/TableConfigurationModalProps.js} +0 -0
  49. /package/dist/Components/Table/Types/{Columns/Definitions/JSONColumnDefSet.js → Persistence/DatatableView.js} +0 -0
@@ -5,6 +5,7 @@ import { ClipboardState } from './ClipboardState.svelte.js';
5
5
  import { PreferencesState } from './PreferencesState.svelte.js';
6
6
  import { QueryValuesState } from './QueryValuesState.svelte.js';
7
7
  import { SelectionState } from './SelectionState.svelte.js';
8
+ import { ViewState } from './ViewState.svelte.js';
8
9
  import { tick, untrack } from 'svelte';
9
10
  import { SvelteMap } from 'svelte/reactivity';
10
11
  import { ColumnType } from '../Columns/ColumnType.js';
@@ -15,6 +16,8 @@ import { FilteringState } from './FilteringState.svelte.js';
15
16
  import { ColumnLayoutState } from './ColumnLayoutState.svelte.js';
16
17
  import { FocusState } from './FocusState.svelte.js';
17
18
  import { VirtualisationState } from './VirtualisationState.svelte.js';
19
+ import { NumberDateFilterMode } from '../Filtering/NumberDateFilterMode.js';
20
+ import { StringFilterMode } from '../Filtering/StringFilterMode.js';
18
21
  import { PaginationType } from '../Pagination/PaginationType.js';
19
22
  import { ToolbarPosition } from '../Toolbar/ToolbarPosition.js';
20
23
  const SORTING_ICON_WIDTH = 24;
@@ -26,6 +29,29 @@ const InternalSymbol = Symbol();
26
29
  * write once the user settles.
27
30
  */
28
31
  const LAYOUT_PERSIST_DEBOUNCE_MS = 250;
32
+ /**
33
+ * Debounce window (ms) for the auto-persist of the live filter state.
34
+ * Collapses a burst of changes - notably per-keystroke quick-filter
35
+ * typing - into a single `localStorage` write once the user settles.
36
+ * Kept equal to `LAYOUT_PERSIST_DEBOUNCE_MS` so the layout and filter
37
+ * auto-persist effects behave consistently.
38
+ */
39
+ const FILTER_PERSIST_DEBOUNCE_MS = 250;
40
+ /**
41
+ * The complete set of filter-mode operator strings scoria recognises -
42
+ * the union of every `StringFilterMode` and `NumberDateFilterMode`
43
+ * value. `_applyFilter` validates each persisted mode string against
44
+ * this set and drops any it no longer knows (schema drift, a removed
45
+ * operator) rather than forwarding a bogus mode to the repository.
46
+ *
47
+ * Built once at module load. Some literals collide between the two enums
48
+ * (`'='` is both `StringFilterMode.ImplicitEquals` and
49
+ * `NumberDateFilterMode.Equals`); the `Set` naturally de-duplicates.
50
+ */
51
+ const KNOWN_FILTER_MODES = new Set([
52
+ ...Object.values(StringFilterMode),
53
+ ...Object.values(NumberDateFilterMode),
54
+ ]);
29
55
  /**
30
56
  * Default values applied by `TableContext.init` for any `TableInitOptions`
31
57
  * field the consumer omits. Frozen so callers (notably `NestedTable`)
@@ -59,6 +85,8 @@ export class TableContext {
59
85
  selectionExtractor;
60
86
  getUserId;
61
87
  _onEditCell;
88
+ datatableUuid;
89
+ remoteLayouts;
62
90
  debug;
63
91
  static identifier = Symbol();
64
92
  /**
@@ -77,13 +105,25 @@ export class TableContext {
77
105
  // (cell wrappers compare by reference via the keying cache)
78
106
  // and avoids the `selectionExtractor is not a function` crash
79
107
  // inside `SelectionState.cachedKey` and `TableRow.svelte`.
80
- options.selectionExtractor ?? ((row) => row), options.getUserId, options.onEditCell, options.debug ?? DEFAULT_TABLE_INIT_OPTIONS.debug);
108
+ options.selectionExtractor ?? ((row) => row), options.getUserId, options.onEditCell, options.datatableUuid, options.remoteLayouts, options.debug ?? DEFAULT_TABLE_INIT_OPTIONS.debug);
81
109
  return context;
82
110
  }
83
111
  // #region constructor
84
112
  constructor(INTERNAL_ONLY, defaultColumnDefs, tableName, dataRepo, allowCopy, enableResize, allowColumnReordering, allowQuickFiltering, showQuickFilterByDefault, allowAdvancedFiltering, toolbarPosition, displayFooter, selectionExtractor, getUserId, _onEditCell = () => {
85
113
  console.warn('Cannot edit immutable table');
86
- }, debug) {
114
+ },
115
+ /**
116
+ * Stable per-table identifier supplied by the host app, or
117
+ * `undefined` when remote saved views are disabled. Stored only;
118
+ * later steps consume it.
119
+ */
120
+ datatableUuid,
121
+ /**
122
+ * Injected backend adapter for saved layouts/filters, or
123
+ * `undefined` when remote saved views are disabled. Stored only;
124
+ * later steps consume it.
125
+ */
126
+ remoteLayouts, debug) {
87
127
  this.defaultColumnDefs = defaultColumnDefs;
88
128
  this.tableName = tableName;
89
129
  this.allowCopy = allowCopy;
@@ -97,6 +137,8 @@ export class TableContext {
97
137
  this.selectionExtractor = selectionExtractor;
98
138
  this.getUserId = getUserId;
99
139
  this._onEditCell = _onEditCell;
140
+ this.datatableUuid = datatableUuid;
141
+ this.remoteLayouts = remoteLayouts;
100
142
  this.debug = debug;
101
143
  if (INTERNAL_ONLY != InternalSymbol) {
102
144
  throw new Error('This class may only be instantiated through the static init() method');
@@ -353,6 +395,11 @@ export class TableContext {
353
395
  // `_layoutHydrated` so the boot-time hydration below does not race
354
396
  // a write of the default layout over the saved snapshot, and
355
397
  // debounced so a drag-resize storm collapses to a single write.
398
+ //
399
+ // The same debounced callback also drives the FPM 403 view-state
400
+ // machine: it compares the freshly captured layout against the
401
+ // active view's baseline, updates `_viewState.isDirty`, and
402
+ // persists the `{ active, dirty }` `JSONActiveView` envelope.
356
403
  $effect(() => {
357
404
  const snapshot = this._captureLayout();
358
405
  if (!this._layoutHydrated) {
@@ -364,76 +411,101 @@ export class TableContext {
364
411
  this._layoutPersistTimer = setTimeout(() => {
365
412
  this._layoutPersistTimer = undefined;
366
413
  this._preferences.persistLayout(snapshot);
414
+ // Dirty tracking: the layout is dirty when it diverges
415
+ // from the active view's baseline. `_captureLayout`
416
+ // builds its object deterministically, so a stable
417
+ // `JSON.stringify` comparison is sufficient. When no
418
+ // baseline has been captured yet (should not happen
419
+ // past boot) treat the layout as clean.
420
+ const differs = this._viewBaseline !== undefined &&
421
+ JSON.stringify(snapshot) !== JSON.stringify(this._viewBaseline);
422
+ this._viewState.markDirty('layout', differs);
423
+ this._persistActiveView();
367
424
  }, LAYOUT_PERSIST_DEBOUNCE_MS);
368
425
  });
426
+ // Auto-persist the live filter state (per-column values + modes)
427
+ // to localStorage on every change - the filter counterpart of the
428
+ // layout auto-persist effect above. A user's quick-filter / advanced
429
+ // filter tweaks survive navigation and reload without saving a named
430
+ // filter view. Gated on the SAME `_layoutHydrated` boot gate (the
431
+ // boot chain hydrates the filter working copy alongside the layout,
432
+ // so a single gate covers both) and debounced so per-keystroke
433
+ // typing collapses to one write.
434
+ //
435
+ // Fully independent of the layout effect: it reads only the repo's
436
+ // filter state, compares against the FILTER baseline, sets the
437
+ // FILTER dirty flag, and persists the `active-filter-view`
438
+ // envelope. Editing a layout never re-runs this effect and vice
439
+ // versa.
440
+ $effect(() => {
441
+ const snapshot = this._captureFilter();
442
+ if (!this._layoutHydrated) {
443
+ return;
444
+ }
445
+ if (this._filterPersistTimer !== undefined) {
446
+ clearTimeout(this._filterPersistTimer);
447
+ }
448
+ this._filterPersistTimer = setTimeout(() => {
449
+ this._filterPersistTimer = undefined;
450
+ this._preferences.persistFilter(snapshot);
451
+ // Dirty tracking: the filter is dirty when it diverges from
452
+ // the active filter view's baseline. `_captureFilter` builds
453
+ // its object deterministically (sorted entries / sorted mode
454
+ // keys), so a stable `JSON.stringify` comparison is
455
+ // sufficient. When no baseline has been captured yet (should
456
+ // not happen past boot) treat the filter as clean.
457
+ const differs = this._filterBaseline !== undefined &&
458
+ JSON.stringify(snapshot) !== JSON.stringify(this._filterBaseline);
459
+ this._viewState.markDirty('filter', differs);
460
+ this._persistActiveFilterView();
461
+ }, FILTER_PERSIST_DEBOUNCE_MS);
462
+ });
369
463
  if (browser) {
370
464
  this._getUserDefaults();
371
- const removeInvalidPresets = () => {
372
- this._preferences.clearPresetsAndSelection();
373
- const visibilityState = {};
374
- this.columnDefs.forEach((def) => {
375
- if (def.defaultHidden || def.columnType === ColumnType.FilterOnly) {
376
- visibilityState[def.id] = false;
377
- }
378
- });
379
- const defaultPreset = {
380
- name: 'Default',
381
- columnDefs: this.columnDefs,
382
- sortingState: [],
383
- visibilityState,
384
- columnPinningState: {},
385
- };
386
- this.selectedPreset = defaultPreset;
387
- this.columnVisibility = visibilityState;
388
- this._preferences.setPresets([...this.columnPresets, defaultPreset]);
389
- };
390
465
  this.setupDefaultColumnDefs()
391
466
  .then((defs) => {
392
467
  this.showQuickFiltering = this.showQuickFilterByDefault;
393
468
  this.isSelectable = defs.some((it) => it.id.startsWith('is-selectable-'));
394
- if (this.allowColumnReordering) {
395
- const presets = this._preferences.loadPresets();
396
- if (presets.length > 0) {
397
- if (presets[0] && areDefsEquivalent(defs, presets[0].columnDefs)) {
398
- return this.reassignDefsToPresets(presets).then((p) => p);
399
- }
400
- else if (dev && presets[0]) {
401
- console.warn(`invalid presets:`, presets);
402
- }
403
- }
404
- }
405
- return null;
406
- })
407
- .then((presets) => {
408
- if (presets == null) {
409
- removeInvalidPresets();
410
- return;
411
- }
412
- const savedPresetName = this._preferences.loadSelectedPresetName();
413
- if (savedPresetName) {
414
- const preset = presets.find((it) => it.name === savedPresetName);
415
- if (preset && areDefsEquivalent(this.columnDefs, preset.columnDefs)) {
416
- this.columnDefs = preset.columnDefs;
417
- this.columnVisibility = preset.visibilityState;
418
- this.columnPinning = preset.columnPinningState;
419
- this.selectedPreset = preset;
420
- }
421
- else {
422
- localStorage.removeItem(this._preferences.storageKey('selected-preset-name'));
423
- this.selectedPreset = presets[0];
424
- if (presets.at(0)?.visibilityState) {
425
- this.columnVisibility = presets[0].visibilityState;
426
- }
427
- }
428
- this._preferences.setPresets(presets);
429
- }
430
- })
431
- .then(() => {
432
- // Restore the auto-saved live layout on top of whatever
433
- // the preset flow produced, then open the auto-persist
434
- // gate. Runs regardless of which preset branch was taken.
469
+ // Capture the Default-layout baseline: the factory /
470
+ // code-defined column state `setupDefaultColumnDefs` just
471
+ // produced, BEFORE the localStorage working copy is
472
+ // overlaid. This IS the "Default" view - the FPM 403
473
+ // view-state machine measures dirtiness against it and
474
+ // `resetToDefault` restores it.
475
+ this._defaultLayoutSnapshot = this._captureLayout();
476
+ this._viewBaseline = this._defaultLayoutSnapshot;
477
+ // Capture the Default-filter baseline: the empty / initial
478
+ // filter state at boot, BEFORE the localStorage working
479
+ // copy is overlaid. This is the "Default" the FPM 403
480
+ // filter view-state machine measures dirtiness against.
481
+ this._defaultFilterSnapshot = this._captureFilter();
482
+ this._filterBaseline = this._defaultFilterSnapshot;
483
+ // Restore the auto-saved live layout on top of the
484
+ // factory Default.
435
485
  this._hydrateLayout();
486
+ // Restore the auto-saved live filter working copy. The
487
+ // filter counterpart of `_hydrateLayout`; fully
488
+ // independent of the layout restore.
489
+ this._hydrateFilter();
490
+ // Restore the persisted active-view selection on top of
491
+ // the working copy. The working copy that `_hydrateLayout`
492
+ // applied stays the live state; this only re-points
493
+ // `activeView` / the baseline and restores the dirty
494
+ // flag. Done before the `_layoutHydrated` gate opens so
495
+ // the auto-persist effect does not clobber it.
496
+ this._hydrateActiveView();
497
+ // Restore the persisted filter active-view selection,
498
+ // independently of the layout one.
499
+ this._hydrateActiveFilterView();
436
500
  this._layoutHydrated = true;
501
+ // Fire-and-forget: pull the saved views from the backend.
502
+ // Deliberately not awaited so a slow / failing network
503
+ // never blocks the first table render. Once it resolves,
504
+ // `refreshSavedViews` reconciles the baseline against the
505
+ // now-known body of a persisted saved-view ref.
506
+ if (this.remoteLayouts && this.datatableUuid) {
507
+ this.refreshSavedViews().catch(devCatch);
508
+ }
437
509
  })
438
510
  .catch(devCatch);
439
511
  }
@@ -456,6 +528,13 @@ export class TableContext {
456
528
  * to run after the component is gone.
457
529
  */
458
530
  _layoutPersistTimer = undefined;
531
+ /**
532
+ * Pending debounce timer for the filter auto-persist. The filter
533
+ * counterpart of `_layoutPersistTimer`; deliberately not cleared on
534
+ * teardown for the same reason - a still-pending `persistFilter`
535
+ * write should flush, and it only touches `localStorage`.
536
+ */
537
+ _filterPersistTimer = undefined;
459
538
  _paginationRepo = $state();
460
539
  get paginationRepo() {
461
540
  return this._paginationRepo;
@@ -537,22 +616,6 @@ export class TableContext {
537
616
  set columnDefs(v) {
538
617
  this._columnLayout.columnDefs = v;
539
618
  }
540
- /**
541
- * Delegates to `_preferences._columnPresets`. The setter is private
542
- * because the public write path is `saveColumnPreset` (which also
543
- * persists to localStorage); internal call sites that need to
544
- * mutate the list directly call `_preferences.setPresets(...)`.
545
- */
546
- get columnPresets() {
547
- return this._preferences.columnPresets;
548
- }
549
- /** Delegates to `_preferences._selectedPreset`. */
550
- get selectedPreset() {
551
- return this._preferences.selectedPreset;
552
- }
553
- set selectedPreset(v) {
554
- this._preferences.selectedPreset = v;
555
- }
556
619
  /**
557
620
  * Delegates to `_focus._focusedRowIndices`. Populated by the
558
621
  * focus-derived `$effect` in the constructor through
@@ -866,47 +929,85 @@ export class TableContext {
866
929
  /**
867
930
  * Persistence / preferences sub-context:
868
931
  *
869
- * - saved column-presets list and currently-selected preset
870
- * (`columnPresets`, `selectedPreset`);
871
932
  * - `userIdSegment` `$derived` (namespaces every
872
933
  * `scoria-table:<user>:<tableName>:<suffix>` storage key);
873
- * - `saveColumnPreset` / `setSelectedPreset` writers (apply layout
874
- * AND persist);
875
- * - `loadPresets` / `loadSelectedPresetName` / `loadSavedSettings`
876
- * boot-time hydration helpers;
877
- * - `persistSettings` writer used by the in-app settings toggles
934
+ * - `loadSavedSettings` boot-time hydration helper and the
935
+ * `persistSettings` writer used by the in-app settings toggles
878
936
  * (`setIsCompact`, `setAlternateRowColours`, etc.);
879
- * - schema-validation typeguards (exported alongside the sub-context
880
- * for unit testing).
937
+ * - the layout / active-view / filter / active-filter-view
938
+ * load / persist / clear helpers driving the FPM 403 datatable
939
+ * views;
940
+ * - the `isSavedTableSettings` schema-validation typeguard (exported
941
+ * alongside the sub-context for unit testing).
881
942
  *
882
943
  * The settings fields themselves (`isCompact`, `singleClickEditing`,
883
944
  * `alternateRowColours`, etc.) stay on `TableContext` because each
884
945
  * drives reactive UI; only the localStorage round-trip lives here.
885
- * Delegating getters / setters / methods on `TableContext` cover the
886
- * full surface (`columnPresets`, `selectedPreset`, `saveColumnPreset`,
887
- * `setSelectedPreset`, `setIsCompact`, etc.).
888
946
  */
889
947
  _preferences = new PreferencesState({
890
948
  getTableName: () => this.tableName,
891
949
  getUserId: () => this.getUserId(),
892
- getColumnDefs: () => this.columnDefs,
893
- getColumnVisibility: () => this.columnVisibility,
894
- getColumnPinning: () => this.columnPinning,
895
- getSortingArray: () => this.paginationRepo?.sorting,
896
- setColumnDefs: (defs) => {
897
- this.columnDefs = defs;
898
- },
899
- setColumnVisibility: (state) => {
900
- this.columnVisibility = state;
901
- },
902
- setColumnPinning: (state) => {
903
- this.columnPinning = state;
904
- },
905
- applySortingState: (state) => {
906
- this.paginationRepo?.updateSorting?.(state);
907
- },
908
- getDebug: () => this.debug,
950
+ getDatatableUuid: () => this.datatableUuid,
909
951
  });
952
+ /**
953
+ * Datatable view-selection sub-context (FPM 403):
954
+ *
955
+ * - `activeView` - which view the live table belongs to (the
956
+ * code-defined Default layout, or a named saved `DatatableView`);
957
+ * - `isDirty` - whether the live layout has diverged from the active
958
+ * view's saved baseline;
959
+ * - the saved views per kind - `'layout'` and `'filter'` - fetched
960
+ * through the injected `remoteLayouts` adapter;
961
+ * - `dropdownLabel(kind)` - the `$derived` label the toolbar
962
+ * view-picker renders.
963
+ *
964
+ * `ViewState` tracks the `'layout'` and `'filter'` kinds as two fully
965
+ * independent slices: applying or dirtying one never touches the
966
+ * other. It is a pure state holder - the apply / capture / baseline
967
+ * tracking / adapter calls all live on this class (`_applyLayout` /
968
+ * `_captureLayout` / `_applyFilter` / `_captureFilter`,
969
+ * `_viewBaseline` / `_filterBaseline`, the `applyDatatableView` /
970
+ * `resetToDefault` / `saveCurrentAsNewView` / `saveActiveView` /
971
+ * `refreshSavedViews` methods below), mirroring how `SelectionState`
972
+ * keeps its effects on the parent. The dirty comparisons run in the
973
+ * two debounced auto-persist `$effect`s in the constructor.
974
+ */
975
+ _viewState = new ViewState();
976
+ /**
977
+ * Baseline `JSONTableLayout` the dirty comparison is measured
978
+ * against: the saved state of whatever view `_viewState.activeView`
979
+ * points at. Set to the Default snapshot once `setupDefaultColumnDefs`
980
+ * completes, and re-pointed at a saved view's `body.layout` whenever
981
+ * one is applied. `undefined` only in the brief window before the
982
+ * boot chain captures the Default snapshot.
983
+ */
984
+ _viewBaseline = undefined;
985
+ /**
986
+ * The Default-layout snapshot, captured once right after
987
+ * `setupDefaultColumnDefs` (before the working copy is applied). Kept
988
+ * separately from `_viewBaseline` so `resetToDefault` can restore the
989
+ * factory layout regardless of which saved view was active.
990
+ */
991
+ _defaultLayoutSnapshot = undefined;
992
+ /**
993
+ * Baseline `JSONTableFilter` the *filter* dirty comparison is
994
+ * measured against: the saved state of whatever filter view
995
+ * `_viewState.activeView('filter')` points at. Set to the Default
996
+ * filter snapshot once the boot chain runs, and re-pointed at a saved
997
+ * view's `body.filter` whenever one is applied. The filter
998
+ * counterpart of `_viewBaseline`; kept entirely separate so layout
999
+ * and filter dirtiness never cross-contaminate.
1000
+ */
1001
+ _filterBaseline = undefined;
1002
+ /**
1003
+ * The Default-filter snapshot, captured once during the boot chain
1004
+ * (the empty / initial filter state, before the working copy is
1005
+ * applied). Kept separately from `_filterBaseline` so a future
1006
+ * filter-reset can restore the initial filter state regardless of
1007
+ * which saved filter view was active. The filter counterpart of
1008
+ * `_defaultLayoutSnapshot`.
1009
+ */
1010
+ _defaultFilterSnapshot = undefined;
910
1011
  /**
911
1012
  * Selection + highlighting + key-cache sub-context:
912
1013
  *
@@ -1660,31 +1761,6 @@ export class TableContext {
1660
1761
  // });
1661
1762
  // }
1662
1763
  // };
1663
- /**
1664
- * Applies the preset to the live column layout, forwards the
1665
- * preset's `sortingState` to the data-repository updater, and
1666
- * persists the selected name. Delegates to
1667
- * `_preferences.setSelectedPreset`.
1668
- *
1669
- * The "no sorting state found" debug warning is emitted here (not
1670
- * inside the sub-context) so the public surface stays byte-identical
1671
- * - `_preferences.applySortingState` already no-ops when
1672
- * `paginationRepo.updateSorting` is absent.
1673
- */
1674
- setSelectedPreset = (preset) => {
1675
- if (!this.paginationRepo?.updateSorting) {
1676
- console.warn('no sorting state found');
1677
- }
1678
- this._preferences.setSelectedPreset(preset);
1679
- };
1680
- /**
1681
- * Adds or replaces a saved preset for the current column layout and
1682
- * persists the full preset list to localStorage. Delegates to
1683
- * `_preferences.saveColumnPreset`.
1684
- */
1685
- saveColumnPreset = (name) => {
1686
- this._preferences.saveColumnPreset(name);
1687
- };
1688
1764
  /**
1689
1765
  * Delegates to `_focus.setFocusEnd`. This listener should be added
1690
1766
  * and removed based on `isMousingDown`.
@@ -1907,6 +1983,469 @@ export class TableContext {
1907
1983
  this._applyLayout(layout);
1908
1984
  }
1909
1985
  }
1986
+ /**
1987
+ * Boot-time hydration for the FPM 403 view-state machine. Reads the
1988
+ * persisted `JSONActiveView` envelope and restores the active-view
1989
+ * selection on top of whatever `_hydrateLayout` produced:
1990
+ *
1991
+ * - no slot / Default ref: the active view stays Default and the
1992
+ * baseline stays the Default snapshot (already set by the caller);
1993
+ * - a saved-view ref: `activeView` is re-pointed at it, and the
1994
+ * baseline is repointed at the matching view's `body.layout` IF the
1995
+ * saved-views list has already loaded. The list is fetched
1996
+ * fire-and-forget, so it usually has NOT loaded yet at boot - in
1997
+ * that case the persisted ref is restored as-is and `refreshSavedViews`
1998
+ * reconciles the baseline once the network resolves.
1999
+ *
2000
+ * The persisted `dirty` flag is restored verbatim via `markDirty`;
2001
+ * the live working copy that `_hydrateLayout` applied is the source
2002
+ * of truth for the layout contents.
2003
+ *
2004
+ * Runs before the `_layoutHydrated` gate opens so the auto-persist
2005
+ * effect does not clobber the restored selection.
2006
+ */
2007
+ _hydrateActiveView() {
2008
+ const persisted = this._preferences.loadActiveView();
2009
+ if (persisted === null) {
2010
+ return;
2011
+ }
2012
+ if (persisted.active.kind === 'saved') {
2013
+ this._viewState.setActiveView('layout', persisted.active);
2014
+ // If the saved-views list happens to be loaded already,
2015
+ // reconcile the baseline now; otherwise `refreshSavedViews`
2016
+ // does it once the fetch resolves.
2017
+ this._reconcileBaselineForActiveView();
2018
+ }
2019
+ this._viewState.markDirty('layout', persisted.dirty);
2020
+ }
2021
+ /**
2022
+ * Re-point `_viewBaseline` at the active view's saved layout. For a
2023
+ * Default active view the baseline is the Default snapshot; for a
2024
+ * saved active view it is the matching `savedLayouts` entry's
2025
+ * `body.layout` (when that entry is known - if the saved-views list
2026
+ * has not loaded yet the baseline is left unchanged and is reconciled
2027
+ * later by `refreshSavedViews`).
2028
+ *
2029
+ * Does NOT recompute the dirty flag - the next auto-persist tick does
2030
+ * that against the updated baseline.
2031
+ */
2032
+ _reconcileBaselineForActiveView() {
2033
+ const active = this._viewState.activeView('layout');
2034
+ if (active.kind === 'default') {
2035
+ if (this._defaultLayoutSnapshot !== undefined) {
2036
+ this._viewBaseline = this._defaultLayoutSnapshot;
2037
+ }
2038
+ return;
2039
+ }
2040
+ const match = this._viewState.savedViews('layout').find((view) => view.id === active.id);
2041
+ if (match && match.body.kind === 'layout') {
2042
+ this._viewBaseline = match.body.layout;
2043
+ }
2044
+ }
2045
+ /**
2046
+ * Persist the current layout `{ active, dirty }` selection as a
2047
+ * `JSONActiveView` envelope. Called (debounced) from the layout
2048
+ * auto-persist `$effect` alongside the working-copy layout write.
2049
+ */
2050
+ _persistActiveView() {
2051
+ const envelope = {
2052
+ active: this._viewState.activeView('layout'),
2053
+ dirty: this._viewState.isDirty('layout'),
2054
+ };
2055
+ this._preferences.persistActiveView(envelope);
2056
+ }
2057
+ // #region FPM 403 A3 - filter serialisation
2058
+ /**
2059
+ * Snapshot the live filter state - the per-column filter values and
2060
+ * per-column filter modes - into the persistable `JSONTableFilter`
2061
+ * shape. The filter counterpart of `_captureLayout`.
2062
+ *
2063
+ * Reads the repository's `filtering` array (`{ id, value }` entries)
2064
+ * and `filterModes` map; reading both makes the filter auto-persist
2065
+ * `$effect` re-subscribe whenever either changes. The `SvelteMap` of
2066
+ * modes is flattened to a plain `Record<string,string>` (a `Map` does
2067
+ * not survive `JSON.stringify`). Entries and mode keys are sorted so
2068
+ * the captured object is deterministic and a stable `JSON.stringify`
2069
+ * comparison drives the dirty flag.
2070
+ *
2071
+ * Sort order and pagination are deliberately excluded - a saved
2072
+ * filter is values + modes only.
2073
+ *
2074
+ * `filtering` and `filterModes` are mandatory on the `IDataRepository`
2075
+ * contract and present on every repo kind (local, built-in remote,
2076
+ * custom remote), so no method is missing here; `Array.isArray` /
2077
+ * `keys()` guard only against a genuinely malformed value.
2078
+ */
2079
+ _captureFilter() {
2080
+ const repo = this.dataRepo;
2081
+ const liveFiltering = repo.filtering;
2082
+ const liveModes = repo.filterModes;
2083
+ const filters = Array.isArray(liveFiltering)
2084
+ ? liveFiltering
2085
+ .map((entry) => ({ id: entry.id, value: entry.value }))
2086
+ .sort((a, b) => a.id.localeCompare(b.id))
2087
+ : [];
2088
+ const modes = {};
2089
+ // Sort the keys so the flattened record is order-stable, which
2090
+ // keeps the `JSON.stringify` dirty comparison deterministic.
2091
+ for (const id of [...liveModes.keys()].sort((a, b) => a.localeCompare(b))) {
2092
+ const mode = liveModes.get(id);
2093
+ if (mode !== undefined) {
2094
+ modes[id] = mode;
2095
+ }
2096
+ }
2097
+ return { filters, modes };
2098
+ }
2099
+ /**
2100
+ * Apply a persisted `JSONTableFilter` to the live filter state. The
2101
+ * filter counterpart of `_applyLayout`.
2102
+ *
2103
+ * 1. `resetFilters()` clears every active filter value first, so the
2104
+ * applied filter fully replaces (rather than merges into) whatever
2105
+ * was live;
2106
+ * 2. each `{ id, value }` whose `id` still maps to a live column is
2107
+ * re-applied via `filterBy` - stale ids (a column removed since
2108
+ * the filter was saved) are dropped, mirroring how `_applyLayout`
2109
+ * drops unknown ids;
2110
+ * 3. each mode whose `id` still maps to a live column AND whose mode
2111
+ * string is a recognised `StringFilterMode` / `NumberDateFilterMode`
2112
+ * value is re-applied via `setFilterMode`; unknown modes are
2113
+ * dropped rather than forwarded.
2114
+ *
2115
+ * Modes are applied before values so a value-bearing mode (e.g.
2116
+ * "Empty") sees the column's mode already set. Defensive: a repo
2117
+ * missing `resetFilters` / `filterBy` / `setFilterMode` no-ops the
2118
+ * corresponding step rather than crashing.
2119
+ */
2120
+ _applyFilter(filter) {
2121
+ const repo = this.dataRepo;
2122
+ // Keyed by plain `string` so persisted-snapshot ids - read back as
2123
+ // plain strings - look up cleanly against the live column set.
2124
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
2125
+ const liveColumnIds = new Set(this.columnDefs.map((def) => def.id));
2126
+ if (typeof repo.resetFilters === 'function') {
2127
+ repo.resetFilters();
2128
+ }
2129
+ // Modes first: a value-bearing mode must be set before its value.
2130
+ if (typeof repo.setFilterMode === 'function') {
2131
+ for (const [id, mode] of Object.entries(filter.modes)) {
2132
+ // Drop stale column ids and unrecognised mode strings.
2133
+ if (liveColumnIds.has(id) && KNOWN_FILTER_MODES.has(mode)) {
2134
+ repo.setFilterMode(id, mode);
2135
+ }
2136
+ }
2137
+ }
2138
+ if (typeof repo.filterBy === 'function') {
2139
+ for (const entry of filter.filters) {
2140
+ if (liveColumnIds.has(entry.id)) {
2141
+ repo.filterBy(entry.id, entry.value);
2142
+ }
2143
+ }
2144
+ }
2145
+ }
2146
+ /**
2147
+ * Boot-time hydration. Reads the auto-saved live-filter snapshot and
2148
+ * applies it, restoring the user's last per-column filter values /
2149
+ * modes. No-op when no snapshot is stored. The filter counterpart of
2150
+ * `_hydrateLayout`.
2151
+ */
2152
+ _hydrateFilter() {
2153
+ const filter = this._preferences.loadFilter();
2154
+ if (filter) {
2155
+ this._applyFilter(filter);
2156
+ }
2157
+ }
2158
+ /**
2159
+ * Boot-time hydration for the FPM 403 filter view-state machine.
2160
+ * Reads the persisted *filter* `JSONActiveView` envelope and restores
2161
+ * the filter active-view selection on top of whatever `_hydrateFilter`
2162
+ * produced. The filter counterpart of `_hydrateActiveView`; runs
2163
+ * independently and never touches the layout slice.
2164
+ *
2165
+ * Runs before the `_layoutHydrated` gate opens so the filter
2166
+ * auto-persist effect does not clobber the restored selection.
2167
+ */
2168
+ _hydrateActiveFilterView() {
2169
+ const persisted = this._preferences.loadActiveFilterView();
2170
+ if (persisted === null) {
2171
+ return;
2172
+ }
2173
+ if (persisted.active.kind === 'saved') {
2174
+ this._viewState.setActiveView('filter', persisted.active);
2175
+ this._reconcileBaselineForActiveFilterView();
2176
+ }
2177
+ this._viewState.markDirty('filter', persisted.dirty);
2178
+ }
2179
+ /**
2180
+ * Re-point `_filterBaseline` at the active filter view's saved
2181
+ * filter. The filter counterpart of `_reconcileBaselineForActiveView`:
2182
+ * for a Default active filter view the baseline is the Default filter
2183
+ * snapshot; for a saved one it is the matching saved-filter entry's
2184
+ * `body.filter` (when that entry is known).
2185
+ *
2186
+ * Does NOT recompute the dirty flag - the next filter auto-persist
2187
+ * tick does that against the updated baseline.
2188
+ */
2189
+ _reconcileBaselineForActiveFilterView() {
2190
+ const active = this._viewState.activeView('filter');
2191
+ if (active.kind === 'default') {
2192
+ if (this._defaultFilterSnapshot !== undefined) {
2193
+ this._filterBaseline = this._defaultFilterSnapshot;
2194
+ }
2195
+ return;
2196
+ }
2197
+ const match = this._viewState.savedViews('filter').find((view) => view.id === active.id);
2198
+ if (match && match.body.kind === 'filter') {
2199
+ this._filterBaseline = match.body.filter;
2200
+ }
2201
+ }
2202
+ /**
2203
+ * Persist the current filter `{ active, dirty }` selection as a
2204
+ * `JSONActiveView` envelope into the `active-filter-view` slot.
2205
+ * Called (debounced) from the filter auto-persist `$effect` alongside
2206
+ * the working-copy filter write. The filter counterpart of
2207
+ * `_persistActiveView`.
2208
+ */
2209
+ _persistActiveFilterView() {
2210
+ const envelope = {
2211
+ active: this._viewState.activeView('filter'),
2212
+ dirty: this._viewState.isDirty('filter'),
2213
+ };
2214
+ this._preferences.persistActiveFilterView(envelope);
2215
+ }
2216
+ // #endregion FPM 403 A3
2217
+ /**
2218
+ * Apply a saved `DatatableView` to the live table and mark it as the
2219
+ * active view of its kind. Branches on `view.body.kind`:
2220
+ *
2221
+ * - `'layout'`: apply `view.body.layout` via `_applyLayout`, set the
2222
+ * *layout* active view to `{ kind:'saved', id, name }`, re-point the
2223
+ * *layout* baseline at that layout, clear the *layout* dirty flag.
2224
+ * The filter state, columns' filter values and the filter dirty
2225
+ * flag are left untouched.
2226
+ * - `'filter'`: apply `view.body.filter` via `_applyFilter`, set the
2227
+ * *filter* active view, re-point the *filter* baseline at that
2228
+ * filter, clear the *filter* dirty flag. The column layout and the
2229
+ * layout dirty flag are left untouched.
2230
+ *
2231
+ * Applying one kind never mutates the other kind's state - the layout
2232
+ * and filter view machines are fully independent.
2233
+ *
2234
+ * Bound as a class property so the toolbar view-picker (built later)
2235
+ * can pass the reference around without re-binding `this`.
2236
+ */
2237
+ applyDatatableView = (view) => {
2238
+ const body = view.body;
2239
+ switch (body.kind) {
2240
+ case 'layout':
2241
+ this._applyLayout(body.layout);
2242
+ this._viewState.selectSavedView('layout', { id: view.id, name: view.name });
2243
+ this._viewBaseline = body.layout;
2244
+ this._persistActiveView();
2245
+ return;
2246
+ case 'filter':
2247
+ this._applyFilter(body.filter);
2248
+ this._viewState.selectSavedView('filter', { id: view.id, name: view.name });
2249
+ this._filterBaseline = body.filter;
2250
+ this._persistActiveFilterView();
2251
+ return;
2252
+ }
2253
+ };
2254
+ /**
2255
+ * Reset the live table for `kind` back to its code-defined Default:
2256
+ * apply the captured Default snapshot, set that kind's `activeView`
2257
+ * back to Default, re-point that kind's baseline at the Default
2258
+ * snapshot, and clear that kind's dirty flag.
2259
+ *
2260
+ * - `'layout'`: restores `_defaultLayoutSnapshot` via `_applyLayout`,
2261
+ * touching only the layout slice;
2262
+ * - `'filter'`: restores `_defaultFilterSnapshot` via `_applyFilter`,
2263
+ * touching only the filter slice.
2264
+ *
2265
+ * Resetting one kind never mutates the other - it honours the same
2266
+ * layout/filter independence guarantee as `applyDatatableView`.
2267
+ *
2268
+ * `kind` defaults to `'layout'` so the A2/A3 layout call sites that
2269
+ * call `resetToDefault()` with no argument keep working unchanged.
2270
+ *
2271
+ * No-op (with a dev log) when the relevant Default snapshot has not
2272
+ * been captured yet (i.e. before the boot chain completes).
2273
+ *
2274
+ * Bound as a class property so the toolbar view-picker and the Table
2275
+ * Configuration modal can pass the reference around without
2276
+ * re-binding `this`.
2277
+ */
2278
+ resetToDefault = (kind = 'layout') => {
2279
+ if (kind === 'filter') {
2280
+ if (this._defaultFilterSnapshot === undefined) {
2281
+ if (dev) {
2282
+ console.warn('resetToDefault: default filter snapshot not captured yet');
2283
+ }
2284
+ return;
2285
+ }
2286
+ this._applyFilter(this._defaultFilterSnapshot);
2287
+ this._viewState.selectDefault('filter');
2288
+ this._filterBaseline = this._defaultFilterSnapshot;
2289
+ this._persistActiveFilterView();
2290
+ return;
2291
+ }
2292
+ if (this._defaultLayoutSnapshot === undefined) {
2293
+ if (dev) {
2294
+ console.warn('resetToDefault: default layout snapshot not captured yet');
2295
+ }
2296
+ return;
2297
+ }
2298
+ this._applyLayout(this._defaultLayoutSnapshot);
2299
+ this._viewState.selectDefault('layout');
2300
+ this._viewBaseline = this._defaultLayoutSnapshot;
2301
+ this._persistActiveView();
2302
+ };
2303
+ /**
2304
+ * Save the live state of `kind` as a brand-new saved view through the
2305
+ * injected `remoteLayouts` adapter:
2306
+ *
2307
+ * 1. build a `CreateDatatableViewRequest` whose `body` is, by `kind`,
2308
+ * either `{ kind:'layout', layout: _captureLayout() }` or
2309
+ * `{ kind:'filter', filter: _captureFilter() }`, plus the
2310
+ * host-supplied `datatableUuid` and the owning user;
2311
+ * 2. `await remoteLayouts.createView(...)`;
2312
+ * 3. push the returned `DatatableView` into the matching kind's
2313
+ * saved-views list;
2314
+ * 4. apply it via `applyDatatableView`, which re-points that kind's
2315
+ * baseline and clears that kind's dirty flag.
2316
+ *
2317
+ * Only the named kind's state is touched. No-op (dev-logged via
2318
+ * `devCatch`) when remote saved views are disabled (`datatableUuid` /
2319
+ * `remoteLayouts` absent) or no owning user id is available - scoria
2320
+ * stays backend-agnostic, so it never fabricates these. An adapter
2321
+ * rejection is routed to `devCatch` and never breaks the table.
2322
+ */
2323
+ saveCurrentAsNewView = async (name, kind) => {
2324
+ const adapter = this.remoteLayouts;
2325
+ const datatableUuid = this.datatableUuid;
2326
+ const ownerUserId = this.getUserId();
2327
+ if (!adapter || !datatableUuid || ownerUserId === undefined) {
2328
+ if (dev) {
2329
+ console.warn('saveCurrentAsNewView: no adapter / datatableUuid / userId - cannot save remotely');
2330
+ }
2331
+ return;
2332
+ }
2333
+ try {
2334
+ const body = kind === 'filter'
2335
+ ? { kind: 'filter', filter: this._captureFilter() }
2336
+ : { kind: 'layout', layout: this._captureLayout() };
2337
+ const request = {
2338
+ datatableUuid,
2339
+ kind,
2340
+ name,
2341
+ body,
2342
+ ownerUserId,
2343
+ };
2344
+ const created = await adapter.createView(request);
2345
+ this._viewState.setSavedViews(kind, [...this._viewState.savedViews(kind), created]);
2346
+ this.applyDatatableView(created);
2347
+ }
2348
+ catch (e) {
2349
+ devCatch(e);
2350
+ }
2351
+ };
2352
+ /**
2353
+ * Overwrite the currently-active saved view of `kind` with the live
2354
+ * state of that kind through the injected `remoteLayouts` adapter:
2355
+ *
2356
+ * 1. require that kind's `activeView` to be `kind: 'saved'` (Default
2357
+ * cannot be saved over - the caller uses `saveCurrentAsNewView`
2358
+ * instead);
2359
+ * 2. build an `UpdateDatatableViewRequest` whose `body` is the
2360
+ * captured live layout or filter, by `kind`;
2361
+ * 3. `await remoteLayouts.updateView(...)`;
2362
+ * 4. refresh the matching entry in that kind's saved-views list,
2363
+ * re-select it, re-point that kind's baseline, clear that kind's
2364
+ * dirty flag.
2365
+ *
2366
+ * Only the named kind's state is touched. No-op (dev-logged) when
2367
+ * that kind's active view is Default or remote saved views are
2368
+ * disabled. An adapter rejection is routed to `devCatch`.
2369
+ */
2370
+ saveActiveView = async (kind) => {
2371
+ const active = this._viewState.activeView(kind);
2372
+ if (active.kind !== 'saved') {
2373
+ if (dev) {
2374
+ console.warn('saveActiveView: active view is Default - use saveCurrentAsNewView');
2375
+ }
2376
+ return;
2377
+ }
2378
+ const adapter = this.remoteLayouts;
2379
+ if (!adapter) {
2380
+ if (dev) {
2381
+ console.warn('saveActiveView: no adapter - cannot save remotely');
2382
+ }
2383
+ return;
2384
+ }
2385
+ try {
2386
+ if (kind === 'filter') {
2387
+ const filter = this._captureFilter();
2388
+ const request = {
2389
+ id: active.id,
2390
+ body: { kind: 'filter', filter },
2391
+ };
2392
+ const updated = await adapter.updateView(request);
2393
+ this._viewState.setSavedViews('filter', this._viewState
2394
+ .savedViews('filter')
2395
+ .map((view) => (view.id === updated.id ? updated : view)));
2396
+ this._viewState.selectSavedView('filter', { id: updated.id, name: updated.name });
2397
+ this._filterBaseline = updated.body.kind === 'filter' ? updated.body.filter : filter;
2398
+ this._persistActiveFilterView();
2399
+ return;
2400
+ }
2401
+ const layout = this._captureLayout();
2402
+ const request = {
2403
+ id: active.id,
2404
+ body: { kind: 'layout', layout },
2405
+ };
2406
+ const updated = await adapter.updateView(request);
2407
+ this._viewState.setSavedViews('layout', this._viewState
2408
+ .savedViews('layout')
2409
+ .map((view) => (view.id === updated.id ? updated : view)));
2410
+ this._viewState.selectSavedView('layout', { id: updated.id, name: updated.name });
2411
+ this._viewBaseline = updated.body.kind === 'layout' ? updated.body.layout : layout;
2412
+ this._persistActiveView();
2413
+ }
2414
+ catch (e) {
2415
+ devCatch(e);
2416
+ }
2417
+ };
2418
+ /**
2419
+ * Fetch the saved views for this datatable through the injected
2420
+ * `remoteLayouts` adapter and publish them into `_viewState`, split by
2421
+ * `kind`: `kind: 'layout'` views into the layout saved-views list and
2422
+ * `kind: 'filter'` views into the filter saved-views list. After the
2423
+ * lists land, reconcile both `_viewBaseline` and `_filterBaseline`
2424
+ * against the now-known body of any persisted saved-view ref.
2425
+ *
2426
+ * No-op when remote saved views are disabled (`datatableUuid` /
2427
+ * `remoteLayouts` absent). An adapter rejection is routed to
2428
+ * `devCatch` and never breaks the table.
2429
+ */
2430
+ refreshSavedViews = async () => {
2431
+ const adapter = this.remoteLayouts;
2432
+ const datatableUuid = this.datatableUuid;
2433
+ if (!adapter || !datatableUuid) {
2434
+ return;
2435
+ }
2436
+ try {
2437
+ const views = await adapter.listViews(datatableUuid);
2438
+ this._viewState.setSavedViews('layout', views.filter((view) => view.kind === 'layout'));
2439
+ this._viewState.setSavedViews('filter', views.filter((view) => view.kind === 'filter'));
2440
+ // A persisted saved-view ref restored at boot may have had an
2441
+ // unknown body until now; reconcile both baselines against it.
2442
+ this._reconcileBaselineForActiveView();
2443
+ this._reconcileBaselineForActiveFilterView();
2444
+ }
2445
+ catch (e) {
2446
+ devCatch(e);
2447
+ }
2448
+ };
1910
2449
  /**
1911
2450
  * Drop the persisted live-layout snapshot for this table. The next
1912
2451
  * mount falls back to the default column layout. Exposed so a host
@@ -2021,6 +2560,22 @@ export class TableContext {
2021
2560
  return def;
2022
2561
  });
2023
2562
  this.columnDefs = adjustedDefs;
2563
+ // Seed the Default column visibility from each def's `defaultHidden`
2564
+ // flag (and hide `FilterOnly` columns, which never render a body
2565
+ // cell). Only fills ids the caller did not already pin via
2566
+ // `initialState.columnVisibility`; an explicit entry always wins.
2567
+ // This is the factory-Default visibility the boot chain captures as
2568
+ // the `_defaultLayoutSnapshot` baseline.
2569
+ const seededVisibility = { ...this.columnVisibility };
2570
+ for (const def of adjustedDefs) {
2571
+ if (def.id in seededVisibility) {
2572
+ continue;
2573
+ }
2574
+ if (def.defaultHidden || def.columnType === ColumnType.FilterOnly) {
2575
+ seededVisibility[def.id] = false;
2576
+ }
2577
+ }
2578
+ this.columnVisibility = seededVisibility;
2024
2579
  return adjustedDefs;
2025
2580
  }
2026
2581
  /**
@@ -2083,63 +2638,8 @@ export class TableContext {
2083
2638
  return newState;
2084
2639
  });
2085
2640
  };
2086
- // eslint-disable-next-line @typescript-eslint/require-await
2087
- async reassignDefsToPresets(presets) {
2088
- const adjustedDefs = this.columnDefs;
2089
- const asMap = columnDefsToMap(adjustedDefs);
2090
- console.log('reassigning presets');
2091
- return presets.map((preset) => {
2092
- return {
2093
- ...preset,
2094
- columnPinningState: adaptJSONPinningState(preset.columnPinningState),
2095
- columnDefs: preset.columnDefs.map((def) => {
2096
- const realDef = def.columnType == ColumnType.RowSelect || def.columnType == ColumnType.Actions
2097
- ? adjustedDefs.find((it) => it.columnType === def.columnType)
2098
- : asMap.get(def.id);
2099
- if (!realDef) {
2100
- throw new Error('Could not find equivalent column def:', { cause: def });
2101
- }
2102
- return realDef;
2103
- }),
2104
- };
2105
- });
2106
- }
2107
2641
  }
2108
2642
  // #region static functions
2109
- function areDefsEquivalent(newColumnDefs, oldColumnDefs) {
2110
- const defKeys = newColumnDefs.map((def) => def.id);
2111
- const presetKeys = oldColumnDefs.map((def) => def.id);
2112
- if (defKeys.length !== presetKeys.length) {
2113
- return false;
2114
- }
2115
- return newColumnDefs.every((newDef) => {
2116
- if (newDef.columnType == ColumnType.RowSelect || newDef.columnType == ColumnType.Actions) {
2117
- return true;
2118
- }
2119
- const oldDef = oldColumnDefs.find((d) => d.id === newDef.id);
2120
- if (oldDef === undefined ||
2121
- newDef.header !== oldDef.header ||
2122
- newDef.columnType !== oldDef.columnType ||
2123
- newDef.filterValueType !== oldDef.filterValueType
2124
- // ||
2125
- // newDef.defaultHidden !== oldDef.defaultHidden ||
2126
- // newDef.enableHiding !== oldDef.enableHiding ||
2127
- // newDef.enablePinning !== oldDef.enablePinning ||
2128
- // newDef.enableResizing !== oldDef.enableResizing
2129
- ) {
2130
- return false;
2131
- }
2132
- return true;
2133
- });
2134
- }
2135
- function columnDefsToMap(defs) {
2136
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
2137
- const map = new Map();
2138
- defs.forEach((def) => {
2139
- map.set(def.id, def);
2140
- });
2141
- return map;
2142
- }
2143
2643
  function targetIsLI(target) {
2144
2644
  return target.id.includes('-column-settings-');
2145
2645
  }