@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.
- package/dist/Components/Table/ARCHITECTURE.md +22 -22
- package/dist/Components/Table/Misc/ColumnPanel.svelte +1 -61
- package/dist/Components/Table/Misc/ColumnPanel.svelte.d.ts +2 -24
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte +3 -92
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte.d.ts +2 -24
- package/dist/Components/Table/Misc/ColumnPanelModalProps.d.ts +2 -2
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -0
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte.d.ts +26 -0
- package/dist/Components/Table/Misc/TableConfigurationModalProps.d.ts +13 -0
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +27 -0
- package/dist/Components/Table/Misc/TableSidebar.svelte +45 -2
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +175 -0
- package/dist/Components/Table/Misc/TableViewDropdown.svelte.d.ts +25 -0
- package/dist/Components/Table/SubApis.svelte.js +0 -41
- package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +5 -5
- package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.d.ts +5 -5
- package/dist/Components/Table/Types/Context/ColumnLayoutState.svelte.js +5 -5
- package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +106 -147
- package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +156 -203
- package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +323 -44
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +687 -187
- package/dist/Components/Table/Types/Context/TableInitOptions.d.ts +9 -0
- package/dist/Components/Table/Types/Context/ViewState.svelte.d.ts +152 -0
- package/dist/Components/Table/Types/Context/ViewState.svelte.js +223 -0
- package/dist/Components/Table/Types/ExpandedPanel.d.ts +9 -5
- package/dist/Components/Table/Types/ExpandedPanel.js +9 -5
- package/dist/Components/Table/Types/Persistence/DatatableView.d.ts +95 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.d.ts +21 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewEnvelope.js +1 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewKind.d.ts +10 -0
- package/dist/Components/Table/Types/Persistence/DatatableViewKind.js +1 -0
- package/dist/Components/Table/Types/Persistence/JSONActiveView.d.ts +58 -0
- package/dist/Components/Table/Types/Persistence/JSONActiveView.js +41 -0
- package/dist/Components/Table/Types/Persistence/JSONTableFilter.d.ts +53 -0
- package/dist/Components/Table/Types/Persistence/JSONTableFilter.js +42 -0
- package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.d.ts +47 -0
- package/dist/Components/Table/Types/Persistence/RemoteTableLayoutAdapter.js +1 -0
- package/dist/Components/Table/Types/Public/CreateTableOptions.d.ts +10 -0
- package/dist/Components/Table/Types/Public/TablePersistence.d.ts +1 -4
- package/dist/Components/Table/Types/Public/TableSubApis.d.ts +0 -18
- package/dist/Components/Table/Types/Public/index.d.ts +1 -1
- package/dist/Components/Table/createTable.svelte.js +2 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +9 -0
- package/package.json +1 -1
- package/dist/Components/Table/Types/Columns/Definitions/ColumnDefSet.d.ts +0 -20
- package/dist/Components/Table/Types/Columns/Definitions/JSONColumnDefSet.d.ts +0 -25
- /package/dist/Components/Table/{Types/Columns/Definitions/ColumnDefSet.js → Misc/TableConfigurationModalProps.js} +0 -0
- /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
|
-
},
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
* - `
|
|
874
|
-
*
|
|
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
|
-
* -
|
|
880
|
-
*
|
|
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
|
-
|
|
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
|
}
|