@lavalogic/scoria 0.37.52 → 0.37.54
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/ColumnFactory.svelte.js +37 -22
- package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.d.ts +15 -6
- package/dist/Components/Table/Types/Columns/Definitions/Actions/ActionsDef.svelte.js +5 -4
- package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.d.ts +10 -2
- package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.js +4 -3
- package/dist/Components/Table/Types/Columns/JSONTableLayout.d.ts +42 -0
- package/dist/Components/Table/Types/Columns/JSONTableLayout.js +58 -0
- package/dist/Components/Table/Types/Context/PreferencesState.svelte.d.ts +26 -1
- package/dist/Components/Table/Types/Context/PreferencesState.svelte.js +49 -1
- package/dist/Components/Table/Types/Context/TableContext.svelte.d.ts +42 -0
- package/dist/Components/Table/Types/Context/TableContext.svelte.js +138 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `minSize`, …) via the `commonProps(...)` helper. `id` is passed explicitly
|
|
12
12
|
* at each call site so its narrowed literal type survives the object spread.
|
|
13
13
|
*/
|
|
14
|
-
import { formatDateTime,
|
|
14
|
+
import { formatDateTime, syntheticColumnId } from '../../Helpers/Helpers.svelte.js';
|
|
15
15
|
import { CheckboxDef } from './Types/Columns/Definitions/Accessors/CheckboxDef.svelte.js';
|
|
16
16
|
import { DateInputDef } from './Types/Columns/Definitions/Accessors/DateInputDef.svelte.js';
|
|
17
17
|
import { NumberInputDef } from './Types/Columns/Definitions/Accessors/NumberInputDef.svelte.js';
|
|
@@ -130,22 +130,6 @@ function commonProps(opts, kind) {
|
|
|
130
130
|
filterGroup: opts.filterGroup,
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
-
/** Generate a column id for a kind whose option type does not require an
|
|
134
|
-
* `id` from the caller.
|
|
135
|
-
*
|
|
136
|
-
* - With `hint`: used verbatim. `idHint` is the server-side-filtering
|
|
137
|
-
* contract; the column id flows through `createRemoteRowSource` as
|
|
138
|
-
* the query-string key the server expects, so a synthetic prefix
|
|
139
|
-
* would make every server reject the field as unknown.
|
|
140
|
-
* - Without `hint`: fall back to `${prefix}-${UUID}` so the id is unique
|
|
141
|
-
* within the table. Such columns cannot participate in server-driven
|
|
142
|
-
* filtering (the server has no way to know the random suffix). */
|
|
143
|
-
function mintSyntheticId(prefix, hint) {
|
|
144
|
-
if (hint != null) {
|
|
145
|
-
return syntheticColumnId(hint);
|
|
146
|
-
}
|
|
147
|
-
return syntheticColumnId(`${prefix}-${generateUUID()}`);
|
|
148
|
-
}
|
|
149
133
|
/** Resolve an `options` field that may be a static array or an async loader
|
|
150
134
|
* into the always-loader form the Def expects. The spread drops the
|
|
151
135
|
* readonly modifier the Def's signature lacks. Loaders may return either
|
|
@@ -185,6 +169,35 @@ function formatLocaleDate(value) {
|
|
|
185
169
|
return '';
|
|
186
170
|
}
|
|
187
171
|
export function createColumnFactory() {
|
|
172
|
+
// Per-factory deterministic counters for synthetic (hint-less) column
|
|
173
|
+
// ids. The `columns: (col) => [...]` closure runs the factory methods
|
|
174
|
+
// in the same order every time `createTable` rebuilds the table, so
|
|
175
|
+
// `${prefix}-${n}` is stable across re-creations - which the
|
|
176
|
+
// persisted-layout and column-preset round-trips key on. A random
|
|
177
|
+
// UUID per build silently broke both (every mount produced fresh ids
|
|
178
|
+
// that no saved snapshot could match).
|
|
179
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
180
|
+
const syntheticIdCounters = new Map();
|
|
181
|
+
/** Mint a column id for a kind whose option type does not require an
|
|
182
|
+
* `id` from the caller.
|
|
183
|
+
*
|
|
184
|
+
* - With `hint`: used verbatim. `idHint` is the server-side-filtering
|
|
185
|
+
* contract; the column id flows through `createRemoteRowSource` as
|
|
186
|
+
* the query-string key the server expects, so a synthetic prefix
|
|
187
|
+
* would make every server reject the field as unknown.
|
|
188
|
+
* - Without `hint`: fall back to a deterministic `${prefix}-${n}`
|
|
189
|
+
* (n is a per-prefix, per-factory counter) so the id is unique
|
|
190
|
+
* within the table and stable across rebuilds. Such columns cannot
|
|
191
|
+
* participate in server-driven filtering (the server has no column
|
|
192
|
+
* matching the synthetic id). */
|
|
193
|
+
function mintId(prefix, hint) {
|
|
194
|
+
if (hint != null) {
|
|
195
|
+
return syntheticColumnId(hint);
|
|
196
|
+
}
|
|
197
|
+
const n = syntheticIdCounters.get(prefix) ?? 0;
|
|
198
|
+
syntheticIdCounters.set(prefix, n + 1);
|
|
199
|
+
return syntheticColumnId(`${prefix}-${n}`);
|
|
200
|
+
}
|
|
188
201
|
const factory = {
|
|
189
202
|
// ─── Accessor kinds ─────────────────────────────────────────
|
|
190
203
|
text(opts) {
|
|
@@ -296,7 +309,7 @@ export function createColumnFactory() {
|
|
|
296
309
|
},
|
|
297
310
|
// ─── Display kinds ──────────────────────────────────────────
|
|
298
311
|
display(opts) {
|
|
299
|
-
const id =
|
|
312
|
+
const id = mintId('display', opts.idHint);
|
|
300
313
|
const normalisedOptions = opts.getOptions
|
|
301
314
|
? normaliseOptionsLoader(opts.getOptions)
|
|
302
315
|
: undefined;
|
|
@@ -357,7 +370,7 @@ export function createColumnFactory() {
|
|
|
357
370
|
return makeSpec('display', def);
|
|
358
371
|
},
|
|
359
372
|
bubble(opts) {
|
|
360
|
-
const id =
|
|
373
|
+
const id = mintId('bubble', opts.idHint);
|
|
361
374
|
const normalisedOptions = opts.getOptions
|
|
362
375
|
? normaliseOptionsLoader(opts.getOptions)
|
|
363
376
|
: undefined;
|
|
@@ -384,7 +397,7 @@ export function createColumnFactory() {
|
|
|
384
397
|
return makeSpec('bubble', def);
|
|
385
398
|
},
|
|
386
399
|
progress(opts) {
|
|
387
|
-
const id =
|
|
400
|
+
const id = mintId('progress', opts.idHint);
|
|
388
401
|
// Normalise scalar ratio to `[fraction, 1]` tuple for the Def.
|
|
389
402
|
const tupleAccessor = (row, index) => {
|
|
390
403
|
const raw = opts.value(row, index);
|
|
@@ -417,7 +430,7 @@ export function createColumnFactory() {
|
|
|
417
430
|
return makeSpec('progress', def);
|
|
418
431
|
},
|
|
419
432
|
validity(opts) {
|
|
420
|
-
const id =
|
|
433
|
+
const id = mintId('validity', opts?.idHint);
|
|
421
434
|
const fallbackValid = () => 'Valid';
|
|
422
435
|
const def = new ValidityDef({
|
|
423
436
|
id,
|
|
@@ -437,6 +450,7 @@ export function createColumnFactory() {
|
|
|
437
450
|
rowSelection(opts) {
|
|
438
451
|
const def = new RowSelectionDef({
|
|
439
452
|
isSelectable: opts?.isSelectable ?? (() => true),
|
|
453
|
+
id: mintId('is-selectable'),
|
|
440
454
|
});
|
|
441
455
|
// RowSelection has no public width option — apply the per-kind
|
|
442
456
|
// default so the column does not fall back to the 48px ColumnDef
|
|
@@ -464,6 +478,7 @@ export function createColumnFactory() {
|
|
|
464
478
|
const def = new ActionsDef({
|
|
465
479
|
actions: wrapped,
|
|
466
480
|
isEnabled: isEnabledFn ? (item) => isEnabledFn(item) : undefined,
|
|
481
|
+
id: mintId('actions'),
|
|
467
482
|
});
|
|
468
483
|
// Actions has no public width option — apply the per-kind default.
|
|
469
484
|
def.width = KIND_DEFAULT_WIDTHS.actions;
|
|
@@ -480,7 +495,7 @@ export function createColumnFactory() {
|
|
|
480
495
|
: (inner) => inner[opts.childRowId];
|
|
481
496
|
const userIsEnabled = opts.isEnabled;
|
|
482
497
|
const def = new ExpandDef({
|
|
483
|
-
id:
|
|
498
|
+
id: mintId('expand'),
|
|
484
499
|
header: opts.header ?? '',
|
|
485
500
|
isEnabled: userIsEnabled ?? (() => true),
|
|
486
501
|
accessorFn: opts.childRows,
|
|
@@ -2,9 +2,10 @@ import type { ColourSet } from '../../../../../../scss/colours.js';
|
|
|
2
2
|
import type { TableActionButton } from '../../TableActionButton.js';
|
|
3
3
|
import { ColumnDef, type ColumnDefProps } from '../ColumnDef.svelte.js';
|
|
4
4
|
/**
|
|
5
|
-
* Construction-time props for `ActionsDef`. Omits `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Construction-time props for `ActionsDef`. Omits `header` (pinned to
|
|
6
|
+
* `'Actions'`) and makes `id` optional: the typed column factory passes
|
|
7
|
+
* a deterministic `actions-<n>` id, and a `generateUUID`-suffixed id is
|
|
8
|
+
* minted only when none is supplied.
|
|
8
9
|
*
|
|
9
10
|
* Notes:
|
|
10
11
|
* - `isEnabled` defaults to `true` for every button if omitted.
|
|
@@ -19,15 +20,23 @@ export interface ActionsDefProps<T extends object, in FilterValueType> extends O
|
|
|
19
20
|
isEnabled?: (item: T, index: number, button: TableActionButton<T>) => boolean | Promise<boolean>;
|
|
20
21
|
/** Per-row, per-button colour override. */
|
|
21
22
|
getColourSet?: (item: T, index: number, button: TableActionButton<T>) => ColourSet;
|
|
23
|
+
/**
|
|
24
|
+
* Optional explicit column id. The typed column factory passes a
|
|
25
|
+
* deterministic id (`actions-<n>`) so the column keeps the same id
|
|
26
|
+
* across table re-creations - which persisted-layout and preset
|
|
27
|
+
* round-trips key on. When omitted a random id is minted.
|
|
28
|
+
*/
|
|
29
|
+
id?: ColumnDefProps<T, FilterValueType>['id'];
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
32
|
* `ActionsDef<T, FilterValueType>` is a column whose cells render a
|
|
25
33
|
* row of buttons. It is non-sortable, non-filterable, and pins its
|
|
26
34
|
* header to `'Actions'`.
|
|
27
35
|
*
|
|
28
|
-
* Note:
|
|
29
|
-
*
|
|
30
|
-
*
|
|
36
|
+
* Note: when constructed without an explicit `id` the fallback id uses
|
|
37
|
+
* `generateUUID()`, which is NOT stable across table rebuilds; consumers
|
|
38
|
+
* that need a persisted-layout-safe id (the typed column factory) pass
|
|
39
|
+
* a deterministic `id` instead.
|
|
31
40
|
*/
|
|
32
41
|
export declare class ActionsDef<T extends object, in FilterValueType> extends ColumnDef<T, FilterValueType> {
|
|
33
42
|
constructor(props: ActionsDefProps<T, FilterValueType>);
|
|
@@ -6,14 +6,15 @@ import { ColumnDef } from '../ColumnDef.svelte.js';
|
|
|
6
6
|
* row of buttons. It is non-sortable, non-filterable, and pins its
|
|
7
7
|
* header to `'Actions'`.
|
|
8
8
|
*
|
|
9
|
-
* Note:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Note: when constructed without an explicit `id` the fallback id uses
|
|
10
|
+
* `generateUUID()`, which is NOT stable across table rebuilds; consumers
|
|
11
|
+
* that need a persisted-layout-safe id (the typed column factory) pass
|
|
12
|
+
* a deterministic `id` instead.
|
|
12
13
|
*/
|
|
13
14
|
export class ActionsDef extends ColumnDef {
|
|
14
15
|
constructor(props) {
|
|
15
16
|
super({
|
|
16
|
-
id: syntheticColumnId(`actions-${generateUUID()}`),
|
|
17
|
+
id: props.id ?? syntheticColumnId(`actions-${generateUUID()}`),
|
|
17
18
|
header: 'Actions',
|
|
18
19
|
allowSorting: false,
|
|
19
20
|
allowFiltering: false,
|
package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.d.ts
CHANGED
|
@@ -11,12 +11,20 @@ export interface RowSelectionDefProps<T extends object, FilterValueType = BoolFi
|
|
|
11
11
|
isSelectable: (item: T, index: number) => boolean | Promise<boolean>;
|
|
12
12
|
/** Optional row validity predicate. */
|
|
13
13
|
isValid?: ColumnDefProps<T, FilterValueType>['isValid'];
|
|
14
|
+
/**
|
|
15
|
+
* Optional explicit column id. The typed column factory passes a
|
|
16
|
+
* deterministic id (`is-selectable-<n>`) so the column keeps the
|
|
17
|
+
* same id across table re-creations - which persisted-layout and
|
|
18
|
+
* preset round-trips key on. When omitted a random id is minted.
|
|
19
|
+
*/
|
|
20
|
+
id?: ColumnDefProps<T, FilterValueType>['id'];
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* `RowSelectionDef<T, FilterValueType>` is the checkbox column that
|
|
17
24
|
* drives row selection. Non-sortable and non-filterable; the header
|
|
18
|
-
* reads `'Row Selected'
|
|
19
|
-
* (
|
|
25
|
+
* reads `'Row Selected'`. The id is taken from `props.id` when supplied
|
|
26
|
+
* (the typed column factory passes a deterministic `is-selectable-<n>`)
|
|
27
|
+
* and otherwise falls back to a `generateUUID`-suffixed id.
|
|
20
28
|
*
|
|
21
29
|
* @remarks
|
|
22
30
|
*
|
package/dist/Components/Table/Types/Columns/Definitions/RowSelection/RowSelectionDef.svelte.js
CHANGED
|
@@ -4,8 +4,9 @@ import { ColumnDef } from '../ColumnDef.svelte.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* `RowSelectionDef<T, FilterValueType>` is the checkbox column that
|
|
6
6
|
* drives row selection. Non-sortable and non-filterable; the header
|
|
7
|
-
* reads `'Row Selected'
|
|
8
|
-
* (
|
|
7
|
+
* reads `'Row Selected'`. The id is taken from `props.id` when supplied
|
|
8
|
+
* (the typed column factory passes a deterministic `is-selectable-<n>`)
|
|
9
|
+
* and otherwise falls back to a `generateUUID`-suffixed id.
|
|
9
10
|
*
|
|
10
11
|
* @remarks
|
|
11
12
|
*
|
|
@@ -28,7 +29,7 @@ export class RowSelectionDef extends ColumnDef {
|
|
|
28
29
|
? { isSelectable: propsOrIsSelectable, isValid: legacyIsValid }
|
|
29
30
|
: propsOrIsSelectable;
|
|
30
31
|
super({
|
|
31
|
-
id: syntheticColumnId(`is-selectable-${generateUUID()}`),
|
|
32
|
+
id: props.id ?? syntheticColumnId(`is-selectable-${generateUUID()}`),
|
|
32
33
|
header: 'Row Selected',
|
|
33
34
|
allowSorting: false,
|
|
34
35
|
allowFiltering: false,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ColumnSizingState } from './ColumnSizingState.js';
|
|
2
|
+
import type { JSONColumnPinningState } from './JSONColumnPinningState.js';
|
|
3
|
+
import type { VisibilityState } from './VisibilityState.js';
|
|
4
|
+
/**
|
|
5
|
+
* Persisted-to-localStorage snapshot of a table's *live* column layout -
|
|
6
|
+
* the order / visibility / pinning / width state a user builds up by
|
|
7
|
+
* dragging, hiding and resizing columns, as distinct from an explicitly
|
|
8
|
+
* saved named preset (`JSONColumnDefSet`).
|
|
9
|
+
*
|
|
10
|
+
* `TableContext` writes this automatically on every layout change and
|
|
11
|
+
* re-applies it on the next mount, so a user's tweaks survive navigation
|
|
12
|
+
* and reloads without having to save a preset - persisted until the
|
|
13
|
+
* browser storage is cleared or the table's column set changes (schema
|
|
14
|
+
* drift, which drops the stale snapshot).
|
|
15
|
+
*
|
|
16
|
+
* Only ids and primitive maps are stored - never the column-def objects
|
|
17
|
+
* themselves - so the snapshot stays small and is trivially validated.
|
|
18
|
+
*/
|
|
19
|
+
export interface JSONTableLayout {
|
|
20
|
+
/** Column ids in display order (memory-index order). */
|
|
21
|
+
columnOrder: Array<string>;
|
|
22
|
+
/** Tri-state per-column visibility map. */
|
|
23
|
+
visibility: VisibilityState;
|
|
24
|
+
/** Pinned-left / pinned-right column ids with their pinned widths. */
|
|
25
|
+
pinning: JSONColumnPinningState;
|
|
26
|
+
/** Per-column user-set widths in pixels. */
|
|
27
|
+
sizing: ColumnSizingState;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Schema version for the persisted `JSONTableLayout` envelope. Bump when
|
|
31
|
+
* the shape changes in a backwards-incompatible way; `readVersionedJSON`
|
|
32
|
+
* silently drops envelopes written under a different version.
|
|
33
|
+
*/
|
|
34
|
+
export declare const TABLE_LAYOUT_SCHEMA_VERSION = 1;
|
|
35
|
+
/**
|
|
36
|
+
* Conservative typeguard for a persisted `JSONTableLayout`. localStorage
|
|
37
|
+
* is same-origin-writable so the decoded value is untrusted; this checks
|
|
38
|
+
* the envelope shape and every field's primitive type. `TableContext`
|
|
39
|
+
* performs the deeper "do these ids still match the live column set"
|
|
40
|
+
* check before applying.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isJSONTableLayout(raw: unknown): raw is JSONTableLayout;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema version for the persisted `JSONTableLayout` envelope. Bump when
|
|
3
|
+
* the shape changes in a backwards-incompatible way; `readVersionedJSON`
|
|
4
|
+
* silently drops envelopes written under a different version.
|
|
5
|
+
*/
|
|
6
|
+
export const TABLE_LAYOUT_SCHEMA_VERSION = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Conservative typeguard for a persisted `JSONTableLayout`. localStorage
|
|
9
|
+
* is same-origin-writable so the decoded value is untrusted; this checks
|
|
10
|
+
* the envelope shape and every field's primitive type. `TableContext`
|
|
11
|
+
* performs the deeper "do these ids still match the live column set"
|
|
12
|
+
* check before applying.
|
|
13
|
+
*/
|
|
14
|
+
export function isJSONTableLayout(raw) {
|
|
15
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const candidate = raw;
|
|
19
|
+
if (!Array.isArray(candidate.columnOrder) ||
|
|
20
|
+
!candidate.columnOrder.every((id) => typeof id === 'string')) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (!isPrimitiveRecord(candidate.visibility, (v) => typeof v === 'boolean')) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (!isPrimitiveRecord(candidate.sizing, (v) => typeof v === 'number')) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return isJSONPinningState(candidate.pinning);
|
|
30
|
+
}
|
|
31
|
+
/** Validates a plain (non-array) object whose values all satisfy `valueGuard`. */
|
|
32
|
+
function isPrimitiveRecord(raw, valueGuard) {
|
|
33
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return Object.values(raw).every(valueGuard);
|
|
37
|
+
}
|
|
38
|
+
/** Validates the `{ left?, right? }` array-pair pinning shape. */
|
|
39
|
+
function isJSONPinningState(raw) {
|
|
40
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const candidate = raw;
|
|
44
|
+
return isPinSide(candidate.left) && isPinSide(candidate.right);
|
|
45
|
+
}
|
|
46
|
+
/** Validates one optional pinning side: absent, or an array of `[id, width]`. */
|
|
47
|
+
function isPinSide(raw) {
|
|
48
|
+
if (raw === undefined) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(raw)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return raw.every((entry) => Array.isArray(entry) &&
|
|
55
|
+
entry.length === 2 &&
|
|
56
|
+
typeof entry[0] === 'string' &&
|
|
57
|
+
typeof entry[1] === 'number');
|
|
58
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnPinningState } from '../Columns/ColumnPinningState.js';
|
|
2
|
+
import { type JSONTableLayout } from '../Columns/JSONTableLayout.js';
|
|
2
3
|
import type { ColumnDef } from '../Columns/Definitions/ColumnDef.svelte.js';
|
|
3
4
|
import type { ColumnDefSet } from '../Columns/Definitions/ColumnDefSet.js';
|
|
4
5
|
import type { JSONColumnDefSet } from '../Columns/Definitions/JSONColumnDefSet.js';
|
|
@@ -139,7 +140,7 @@ export declare class PreferencesState<T extends object> {
|
|
|
139
140
|
* for the same namespacing convention; the `<user>` segment is
|
|
140
141
|
* `getUserId() ?? 'anon'` (see `userIdSegment`).
|
|
141
142
|
*/
|
|
142
|
-
storageKey(suffix: 'presets' | 'selected-preset-name' | 'settings'): string;
|
|
143
|
+
storageKey(suffix: 'presets' | 'selected-preset-name' | 'settings' | 'layout'): string;
|
|
143
144
|
/**
|
|
144
145
|
* Boot-time hydration helper. Reads the persisted preset list from
|
|
145
146
|
* `localStorage`, validates it against `isJSONColumnDefSetArray`,
|
|
@@ -172,6 +173,30 @@ export declare class PreferencesState<T extends object> {
|
|
|
172
173
|
* the live column-def shape (schema drift or column-set change).
|
|
173
174
|
*/
|
|
174
175
|
clearPresetsAndSelection(): void;
|
|
176
|
+
/**
|
|
177
|
+
* Boot-time hydration helper. Reads the auto-saved live-layout
|
|
178
|
+
* snapshot (column order / visibility / pinning / sizing the user
|
|
179
|
+
* built up without saving a named preset). Returns `null` for a
|
|
180
|
+
* missing slot, a schema-version mismatch, or a value that fails
|
|
181
|
+
* validation, so the parent can fall back to the default layout.
|
|
182
|
+
*
|
|
183
|
+
* `localStorage` access is gated by the parent's `browser` check;
|
|
184
|
+
* this method assumes the caller already verified the environment.
|
|
185
|
+
*/
|
|
186
|
+
loadLayout(): JSONTableLayout | null;
|
|
187
|
+
/**
|
|
188
|
+
* Persist the live-layout snapshot. Written by the parent on every
|
|
189
|
+
* column order / visibility / pinning / sizing change, wrapped in a
|
|
190
|
+
* versioned envelope so a future schema bump drops stale snapshots
|
|
191
|
+
* cleanly. Storage failures are swallowed inside `writeJSON`.
|
|
192
|
+
*/
|
|
193
|
+
persistLayout(layout: JSONTableLayout): void;
|
|
194
|
+
/**
|
|
195
|
+
* Drop the persisted live-layout snapshot. Called when the snapshot
|
|
196
|
+
* no longer matches the table's column set (schema drift) or when
|
|
197
|
+
* table state is explicitly reset.
|
|
198
|
+
*/
|
|
199
|
+
clearLayout(): void;
|
|
175
200
|
/**
|
|
176
201
|
* Persist the selected-preset name as plain text (no JSON
|
|
177
202
|
* envelope - the slot is a single string by design).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dev } from '$app/environment';
|
|
2
|
-
import { readJSON } from '../../../../Helpers/Storage.js';
|
|
2
|
+
import { readJSON, readVersionedJSON, writeJSON } from '../../../../Helpers/Storage.js';
|
|
3
|
+
import { isJSONTableLayout, TABLE_LAYOUT_SCHEMA_VERSION, } from '../Columns/JSONTableLayout.js';
|
|
3
4
|
/**
|
|
4
5
|
* Conservative typeguard for the persisted preset list. Storage is
|
|
5
6
|
* same-origin-writable so only the array shape can be trusted;
|
|
@@ -192,6 +193,53 @@ export class PreferencesState {
|
|
|
192
193
|
localStorage.removeItem(this.storageKey('presets'));
|
|
193
194
|
localStorage.removeItem(this.storageKey('selected-preset-name'));
|
|
194
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Boot-time hydration helper. Reads the auto-saved live-layout
|
|
198
|
+
* snapshot (column order / visibility / pinning / sizing the user
|
|
199
|
+
* built up without saving a named preset). Returns `null` for a
|
|
200
|
+
* missing slot, a schema-version mismatch, or a value that fails
|
|
201
|
+
* validation, so the parent can fall back to the default layout.
|
|
202
|
+
*
|
|
203
|
+
* `localStorage` access is gated by the parent's `browser` check;
|
|
204
|
+
* this method assumes the caller already verified the environment.
|
|
205
|
+
*/
|
|
206
|
+
loadLayout() {
|
|
207
|
+
// Reference-equality sentinel: `readVersionedJSON` demands a
|
|
208
|
+
// fallback of the value type, so a distinct object instance is
|
|
209
|
+
// used to distinguish "no usable slot" from a real snapshot.
|
|
210
|
+
const fallback = {
|
|
211
|
+
columnOrder: [],
|
|
212
|
+
visibility: {},
|
|
213
|
+
pinning: {},
|
|
214
|
+
sizing: {},
|
|
215
|
+
};
|
|
216
|
+
const saved = readVersionedJSON(localStorage, this.storageKey('layout'), TABLE_LAYOUT_SCHEMA_VERSION, isJSONTableLayout, fallback);
|
|
217
|
+
return saved === fallback ? null : saved;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Persist the live-layout snapshot. Written by the parent on every
|
|
221
|
+
* column order / visibility / pinning / sizing change, wrapped in a
|
|
222
|
+
* versioned envelope so a future schema bump drops stale snapshots
|
|
223
|
+
* cleanly. Storage failures are swallowed inside `writeJSON`.
|
|
224
|
+
*/
|
|
225
|
+
persistLayout(layout) {
|
|
226
|
+
writeJSON(localStorage, this.storageKey('layout'), layout, TABLE_LAYOUT_SCHEMA_VERSION);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Drop the persisted live-layout snapshot. Called when the snapshot
|
|
230
|
+
* no longer matches the table's column set (schema drift) or when
|
|
231
|
+
* table state is explicitly reset.
|
|
232
|
+
*/
|
|
233
|
+
clearLayout() {
|
|
234
|
+
try {
|
|
235
|
+
localStorage.removeItem(this.storageKey('layout'));
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
if (dev) {
|
|
239
|
+
console.warn('table layout slot clear failed:', e);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
195
243
|
/**
|
|
196
244
|
* Persist the selected-preset name as plain text (no JSON
|
|
197
245
|
* envelope - the slot is a single string by design).
|
|
@@ -73,6 +73,21 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
|
|
|
73
73
|
static init<T extends object, RowIdType extends Primitive>(defaultColumnDefs: DefsWrapper<T>, tableName: string, dataRepo: IDataRepository<T>, options: TableInitOptions<T, RowIdType>): TableContext<T, RowIdType>;
|
|
74
74
|
protected constructor(INTERNAL_ONLY: symbol, defaultColumnDefs: DefsWrapper<T>, tableName: string, dataRepo: IDataRepository<T>, allowCopy: boolean, enableResize: boolean, allowColumnReordering: boolean, allowQuickFiltering: boolean, showQuickFilterByDefault: boolean, allowAdvancedFiltering: boolean, toolbarPosition: ToolbarPosition, displayFooter: boolean, selectionExtractor: TableInitOptions<T, RowIdType>['selectionExtractor'], getUserId: () => string | undefined, _onEditCell: TableInitOptions<T, RowIdType>['onEditCell'], debug: boolean);
|
|
75
75
|
private isSelectable;
|
|
76
|
+
/**
|
|
77
|
+
* `false` until the boot-time layout hydration (`_hydrateLayout`)
|
|
78
|
+
* has run. The auto-persist `$effect` reads this gate so the live
|
|
79
|
+
* layout is not written back during construction / hydration -
|
|
80
|
+
* which would clobber the saved snapshot with the default layout.
|
|
81
|
+
*/
|
|
82
|
+
private _layoutHydrated;
|
|
83
|
+
/**
|
|
84
|
+
* Pending debounce timer for the layout auto-persist. Deliberately
|
|
85
|
+
* not cleared on teardown: a still-pending write should flush even
|
|
86
|
+
* if the user navigates away mid-debounce, and `persistLayout` only
|
|
87
|
+
* touches `localStorage` (no reactive state, no DOM) so it is safe
|
|
88
|
+
* to run after the component is gone.
|
|
89
|
+
*/
|
|
90
|
+
private _layoutPersistTimer;
|
|
76
91
|
private _paginationRepo;
|
|
77
92
|
get paginationRepo(): IDataRepository<T> | undefined;
|
|
78
93
|
set paginationRepo(v: IDataRepository<T> | undefined);
|
|
@@ -620,6 +635,33 @@ export declare class TableContext<T extends object, RowIdType extends Primitive>
|
|
|
620
635
|
* persistence boundary lives in the sub-context.
|
|
621
636
|
*/
|
|
622
637
|
private _updateLocalStorageSettings;
|
|
638
|
+
/**
|
|
639
|
+
* Snapshot the live column layout - order / visibility / pinning /
|
|
640
|
+
* sizing - into the persistable `JSONTableLayout` shape. Reads every
|
|
641
|
+
* layout `$state` slot so the auto-persist `$effect` re-runs whenever
|
|
642
|
+
* any of them change.
|
|
643
|
+
*/
|
|
644
|
+
private _captureLayout;
|
|
645
|
+
/**
|
|
646
|
+
* Apply a persisted `JSONTableLayout` to the live column state. The
|
|
647
|
+
* snapshot is dropped (and ignored) when its column-id set no longer
|
|
648
|
+
* matches the table's current columns - a column added or removed
|
|
649
|
+
* since the snapshot was written invalidates it.
|
|
650
|
+
*/
|
|
651
|
+
private _applyLayout;
|
|
652
|
+
/**
|
|
653
|
+
* Boot-time hydration. Reads the auto-saved live-layout snapshot and
|
|
654
|
+
* applies it on top of whatever the preset flow produced, restoring
|
|
655
|
+
* the user's last column order / visibility / pinning / sizing.
|
|
656
|
+
* No-op when no snapshot is stored.
|
|
657
|
+
*/
|
|
658
|
+
private _hydrateLayout;
|
|
659
|
+
/**
|
|
660
|
+
* Drop the persisted live-layout snapshot for this table. The next
|
|
661
|
+
* mount falls back to the default column layout. Exposed so a host
|
|
662
|
+
* app can wire an explicit "reset table layout" control.
|
|
663
|
+
*/
|
|
664
|
+
clearPersistedLayout(): void;
|
|
623
665
|
/**
|
|
624
666
|
* Delegates to `_columnLayout.visualColumns`. The underscore prefix
|
|
625
667
|
* is preserved so the existing read sites in `tabForward` /
|
|
@@ -19,6 +19,13 @@ import { PaginationType } from '../Pagination/PaginationType.js';
|
|
|
19
19
|
import { ToolbarPosition } from '../Toolbar/ToolbarPosition.js';
|
|
20
20
|
const SORTING_ICON_WIDTH = 24;
|
|
21
21
|
const InternalSymbol = Symbol();
|
|
22
|
+
/**
|
|
23
|
+
* Debounce window (ms) for the auto-persist of the live column layout.
|
|
24
|
+
* Collapses a burst of changes - notably a drag-resize, which fires a
|
|
25
|
+
* width update on every pointer move - into a single `localStorage`
|
|
26
|
+
* write once the user settles.
|
|
27
|
+
*/
|
|
28
|
+
const LAYOUT_PERSIST_DEBOUNCE_MS = 250;
|
|
22
29
|
/**
|
|
23
30
|
* Default values applied by `TableContext.init` for any `TableInitOptions`
|
|
24
31
|
* field the consumer omits. Frozen so callers (notably `NestedTable`)
|
|
@@ -339,6 +346,26 @@ export class TableContext {
|
|
|
339
346
|
if (hasPagination(dataRepo)) {
|
|
340
347
|
this.paginationRepo = dataRepo;
|
|
341
348
|
}
|
|
349
|
+
// Auto-persist the live column layout (order / visibility /
|
|
350
|
+
// pinning / sizing) to localStorage on every change, so a user's
|
|
351
|
+
// drag-reorder, hide/show and resize tweaks survive navigation
|
|
352
|
+
// and reload without having to save a named preset. Gated on
|
|
353
|
+
// `_layoutHydrated` so the boot-time hydration below does not race
|
|
354
|
+
// a write of the default layout over the saved snapshot, and
|
|
355
|
+
// debounced so a drag-resize storm collapses to a single write.
|
|
356
|
+
$effect(() => {
|
|
357
|
+
const snapshot = this._captureLayout();
|
|
358
|
+
if (!this._layoutHydrated) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (this._layoutPersistTimer !== undefined) {
|
|
362
|
+
clearTimeout(this._layoutPersistTimer);
|
|
363
|
+
}
|
|
364
|
+
this._layoutPersistTimer = setTimeout(() => {
|
|
365
|
+
this._layoutPersistTimer = undefined;
|
|
366
|
+
this._preferences.persistLayout(snapshot);
|
|
367
|
+
}, LAYOUT_PERSIST_DEBOUNCE_MS);
|
|
368
|
+
});
|
|
342
369
|
if (browser) {
|
|
343
370
|
this._getUserDefaults();
|
|
344
371
|
const removeInvalidPresets = () => {
|
|
@@ -400,6 +427,13 @@ export class TableContext {
|
|
|
400
427
|
}
|
|
401
428
|
this._preferences.setPresets(presets);
|
|
402
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.
|
|
435
|
+
this._hydrateLayout();
|
|
436
|
+
this._layoutHydrated = true;
|
|
403
437
|
})
|
|
404
438
|
.catch(devCatch);
|
|
405
439
|
}
|
|
@@ -407,6 +441,21 @@ export class TableContext {
|
|
|
407
441
|
// #endregion
|
|
408
442
|
// #region stores
|
|
409
443
|
isSelectable = $state(false);
|
|
444
|
+
/**
|
|
445
|
+
* `false` until the boot-time layout hydration (`_hydrateLayout`)
|
|
446
|
+
* has run. The auto-persist `$effect` reads this gate so the live
|
|
447
|
+
* layout is not written back during construction / hydration -
|
|
448
|
+
* which would clobber the saved snapshot with the default layout.
|
|
449
|
+
*/
|
|
450
|
+
_layoutHydrated = $state(false);
|
|
451
|
+
/**
|
|
452
|
+
* Pending debounce timer for the layout auto-persist. Deliberately
|
|
453
|
+
* not cleared on teardown: a still-pending write should flush even
|
|
454
|
+
* if the user navigates away mid-debounce, and `persistLayout` only
|
|
455
|
+
* touches `localStorage` (no reactive state, no DOM) so it is safe
|
|
456
|
+
* to run after the component is gone.
|
|
457
|
+
*/
|
|
458
|
+
_layoutPersistTimer = undefined;
|
|
410
459
|
_paginationRepo = $state();
|
|
411
460
|
get paginationRepo() {
|
|
412
461
|
return this._paginationRepo;
|
|
@@ -1762,6 +1811,95 @@ export class TableContext {
|
|
|
1762
1811
|
};
|
|
1763
1812
|
this._preferences.persistSettings(settingsObject);
|
|
1764
1813
|
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Snapshot the live column layout - order / visibility / pinning /
|
|
1816
|
+
* sizing - into the persistable `JSONTableLayout` shape. Reads every
|
|
1817
|
+
* layout `$state` slot so the auto-persist `$effect` re-runs whenever
|
|
1818
|
+
* any of them change.
|
|
1819
|
+
*/
|
|
1820
|
+
_captureLayout() {
|
|
1821
|
+
const columnOrder = this.columnDefs
|
|
1822
|
+
.slice()
|
|
1823
|
+
.sort((a, b) => a.index - b.index)
|
|
1824
|
+
.map((def) => def.id);
|
|
1825
|
+
const pinning = {
|
|
1826
|
+
left: [...(this.columnPinning.left?.entries() ?? [])],
|
|
1827
|
+
right: [...(this.columnPinning.right?.entries() ?? [])],
|
|
1828
|
+
};
|
|
1829
|
+
return {
|
|
1830
|
+
columnOrder,
|
|
1831
|
+
visibility: { ...this.columnVisibility },
|
|
1832
|
+
pinning,
|
|
1833
|
+
sizing: { ...this.columnSizing },
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Apply a persisted `JSONTableLayout` to the live column state. The
|
|
1838
|
+
* snapshot is dropped (and ignored) when its column-id set no longer
|
|
1839
|
+
* matches the table's current columns - a column added or removed
|
|
1840
|
+
* since the snapshot was written invalidates it.
|
|
1841
|
+
*/
|
|
1842
|
+
_applyLayout(layout) {
|
|
1843
|
+
const defs = this.columnDefs;
|
|
1844
|
+
// Keyed by plain `string` (not the branded `ColumnId`) so the
|
|
1845
|
+
// persisted-snapshot ids - read back as plain strings - look up
|
|
1846
|
+
// cleanly.
|
|
1847
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
1848
|
+
const byId = new Map(defs.map((def) => [def.id, def]));
|
|
1849
|
+
// Schema-drift guard: the snapshot must describe exactly the
|
|
1850
|
+
// current column set, otherwise it is stale and is discarded.
|
|
1851
|
+
const matchesColumnSet = layout.columnOrder.length === defs.length && layout.columnOrder.every((id) => byId.has(id));
|
|
1852
|
+
if (!matchesColumnSet) {
|
|
1853
|
+
this._preferences.clearLayout();
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
// Order: re-index the defs into the saved order.
|
|
1857
|
+
const ordered = layout.columnOrder.map((id, index) => {
|
|
1858
|
+
const def = byId.get(id);
|
|
1859
|
+
if (!def) {
|
|
1860
|
+
throw new Error(`_applyLayout: missing column def for id "${id}"`);
|
|
1861
|
+
}
|
|
1862
|
+
def.index = index;
|
|
1863
|
+
return def;
|
|
1864
|
+
});
|
|
1865
|
+
this.columnDefs = ordered;
|
|
1866
|
+
// Visibility / sizing: keep only ids still present in the table.
|
|
1867
|
+
const visibility = {};
|
|
1868
|
+
for (const [id, visible] of Object.entries(layout.visibility)) {
|
|
1869
|
+
if (byId.has(id)) {
|
|
1870
|
+
visibility[id] = visible;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
this.columnVisibility = visibility;
|
|
1874
|
+
const sizing = {};
|
|
1875
|
+
for (const [id, width] of Object.entries(layout.sizing)) {
|
|
1876
|
+
if (byId.has(id)) {
|
|
1877
|
+
sizing[id] = width;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
this.columnSizing = sizing;
|
|
1881
|
+
this.columnPinning = adaptJSONPinningState(layout.pinning);
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Boot-time hydration. Reads the auto-saved live-layout snapshot and
|
|
1885
|
+
* applies it on top of whatever the preset flow produced, restoring
|
|
1886
|
+
* the user's last column order / visibility / pinning / sizing.
|
|
1887
|
+
* No-op when no snapshot is stored.
|
|
1888
|
+
*/
|
|
1889
|
+
_hydrateLayout() {
|
|
1890
|
+
const layout = this._preferences.loadLayout();
|
|
1891
|
+
if (layout) {
|
|
1892
|
+
this._applyLayout(layout);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Drop the persisted live-layout snapshot for this table. The next
|
|
1897
|
+
* mount falls back to the default column layout. Exposed so a host
|
|
1898
|
+
* app can wire an explicit "reset table layout" control.
|
|
1899
|
+
*/
|
|
1900
|
+
clearPersistedLayout() {
|
|
1901
|
+
this._preferences.clearLayout();
|
|
1902
|
+
}
|
|
1765
1903
|
/**
|
|
1766
1904
|
* Delegates to `_columnLayout.visualColumns`. The underscore prefix
|
|
1767
1905
|
* is preserved so the existing read sites in `tabForward` /
|