@happyvertical/smrt-content 0.43.4 → 0.43.5
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/AGENTS.md +9 -86
- package/agents/content-list.md +869 -0
- package/dist/content-query.d.ts +310 -0
- package/dist/content-query.d.ts.map +1 -0
- package/dist/contents.d.ts +22 -0
- package/dist/contents.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +672 -4
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +22 -2
- package/dist/smrt-knowledge.json +38 -5
- package/dist/svelte/components/ContentList.svelte +941 -30
- package/dist/svelte/components/ContentList.svelte.d.ts +44 -1
- package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.d.ts +98 -1
- package/dist/svelte/content-list-controller.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.js +290 -19
- package/dist/svelte/content-list-query.d.ts +498 -0
- package/dist/svelte/content-list-query.d.ts.map +1 -0
- package/dist/svelte/content-list-query.js +1294 -0
- package/dist/svelte/content-list-saved-views.d.ts +172 -0
- package/dist/svelte/content-list-saved-views.d.ts.map +1 -0
- package/dist/svelte/content-list-saved-views.js +298 -0
- package/dist/svelte/content-list-url-state.d.ts +211 -0
- package/dist/svelte/content-list-url-state.d.ts.map +1 -0
- package/dist/svelte/content-list-url-state.js +856 -0
- package/dist/svelte/i18n.contribution.d.ts +24 -0
- package/dist/svelte/i18n.contribution.d.ts.map +1 -1
- package/dist/svelte/i18n.contribution.js +26 -0
- package/dist/svelte/index.d.ts +5 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +8 -1
- package/package.json +16 -15
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved views for ContentList (#2452).
|
|
3
|
+
*
|
|
4
|
+
* A saved view is a named `DataTableSnapshot` an operator can come back to. The
|
|
5
|
+
* payload is persisted outside the application's control — today in
|
|
6
|
+
* `localStorage`, which is writable by anyone with a console — so it is treated
|
|
7
|
+
* exactly like a URL: untrusted on the way back in.
|
|
8
|
+
*
|
|
9
|
+
* Restoration is therefore two gates, in order:
|
|
10
|
+
*
|
|
11
|
+
* 1. `hydrateDataTableSnapshot` (smrt-ui) parses the stored payload
|
|
12
|
+
* structurally, upgrades version 1/2 into normalized version 3, and throws
|
|
13
|
+
* on anything it cannot make sense of. It intentionally runs *without*
|
|
14
|
+
* column ids, so it normalizes shape but does not know which columns this
|
|
15
|
+
* list publishes.
|
|
16
|
+
* 2. `sanitizeContentListViewState` (`content-list-url-state.ts`) — the same
|
|
17
|
+
* validator the URL path uses — then holds the hydrated state to the
|
|
18
|
+
* adapter's published column and operator vocabulary. A stored view can no
|
|
19
|
+
* more restore a filter on the hidden `description` column, a structural
|
|
20
|
+
* `select`/`actions` column, or a column that has since been removed than a
|
|
21
|
+
* crafted link can.
|
|
22
|
+
*
|
|
23
|
+
* A stale view is not an error: a snapshot that references a column which no
|
|
24
|
+
* longer exists restores the valid remainder and reports the drops, so the UI
|
|
25
|
+
* can say what it discarded instead of refusing to open the list.
|
|
26
|
+
*
|
|
27
|
+
* The storage seam is deliberately narrow (`list`/`get`/`save`/`delete`) and
|
|
28
|
+
* asynchronous, so the server-backed store that lands later is a drop-in
|
|
29
|
+
* replacement rather than a signature change. Nothing here knows about a
|
|
30
|
+
* backend, a tenant, or a principal.
|
|
31
|
+
*/
|
|
32
|
+
import { type DataTableSnapshot, type DataTableViewState } from '@happyvertical/smrt-ui/data';
|
|
33
|
+
import { type ContentListStateDrop, type ContentListStateValidationOptions } from './content-list-url-state.js';
|
|
34
|
+
export type { ContentListStateDrop, ContentListStateDropReason, ContentListStateDropScope, } from './content-list-url-state.js';
|
|
35
|
+
/**
|
|
36
|
+
* Envelope version for a stored view. Bumped only when the envelope around the
|
|
37
|
+
* snapshot changes; the snapshot carries its own `version` for the state.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION = 1;
|
|
40
|
+
/** Prefix of the `localStorage` key a saved-view store owns. */
|
|
41
|
+
export declare const CONTENT_LIST_SAVED_VIEW_STORAGE_PREFIX = "smrt:content-list:saved-views";
|
|
42
|
+
/**
|
|
43
|
+
* A stored snapshot exactly as it was persisted: structurally hydrated by
|
|
44
|
+
* `hydrateDataTableSnapshot`, but **not** validated against this list's column
|
|
45
|
+
* and operator vocabulary.
|
|
46
|
+
*
|
|
47
|
+
* The raw payload is kept on purpose — a stale view has to be able to report
|
|
48
|
+
* what it referenced (`restoreContentListSavedView` returns those drops), and
|
|
49
|
+
* sanitizing on read would erase the evidence. Never hand one of these to a
|
|
50
|
+
* controller directly: go through `restoreContentListSavedView` when the drops
|
|
51
|
+
* matter, or `applyContentListViewState`, which validates its patch.
|
|
52
|
+
*/
|
|
53
|
+
export type RawContentListViewSnapshot = DataTableSnapshot;
|
|
54
|
+
/** A named, restorable content list view. */
|
|
55
|
+
export interface ContentListSavedView {
|
|
56
|
+
/** Stable identity. Survives renames and re-saves. */
|
|
57
|
+
id: string;
|
|
58
|
+
/** Operator-facing name. */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Envelope version, currently `CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION`. */
|
|
61
|
+
schemaVersion: number;
|
|
62
|
+
/**
|
|
63
|
+
* The persisted view payload — structurally normalized on write, and
|
|
64
|
+
* deliberately NOT column-validated. See {@link RawContentListViewSnapshot}.
|
|
65
|
+
*/
|
|
66
|
+
snapshot: RawContentListViewSnapshot;
|
|
67
|
+
/** ISO timestamps, for ordering the operator's list. */
|
|
68
|
+
createdAt: string;
|
|
69
|
+
updatedAt: string;
|
|
70
|
+
}
|
|
71
|
+
/** What a caller supplies to create or update a view. */
|
|
72
|
+
export interface ContentListSavedViewInput {
|
|
73
|
+
/** Omit to create; supply to overwrite an existing view in place. */
|
|
74
|
+
id?: string;
|
|
75
|
+
name: string;
|
|
76
|
+
snapshot: DataTableSnapshot;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The persistence seam. Asynchronous on purpose: the `localStorage` default is
|
|
80
|
+
* synchronous underneath, but a server-backed store cannot be, and changing
|
|
81
|
+
* the signature later would break every consumer.
|
|
82
|
+
*/
|
|
83
|
+
export interface ContentListSavedViewStore {
|
|
84
|
+
/**
|
|
85
|
+
* Every stored view, most recently updated first. Corrupt entries are
|
|
86
|
+
* skipped. Each `snapshot` is a {@link RawContentListViewSnapshot} — hydrated
|
|
87
|
+
* but unvalidated. Restore it with `restoreContentListSavedView`, or apply it
|
|
88
|
+
* through `applyContentListViewState`, which validates.
|
|
89
|
+
*/
|
|
90
|
+
list(): Promise<ContentListSavedView[]>;
|
|
91
|
+
/** One view, or `null` when it is absent or unreadable. Snapshot is raw. */
|
|
92
|
+
get(id: string): Promise<ContentListSavedView | null>;
|
|
93
|
+
/** Creates or replaces a view. Throws `TypeError` on an unusable snapshot. */
|
|
94
|
+
save(input: ContentListSavedViewInput): Promise<ContentListSavedView>;
|
|
95
|
+
/** Removes a view. Resolves `false` when there was nothing to remove. */
|
|
96
|
+
delete(id: string): Promise<boolean>;
|
|
97
|
+
}
|
|
98
|
+
/** The concrete store also reports whether writes actually persist. */
|
|
99
|
+
export interface ContentListLocalSavedViewStore extends ContentListSavedViewStore {
|
|
100
|
+
/**
|
|
101
|
+
* False once the store has fallen back to memory — blocked storage, private
|
|
102
|
+
* browsing, or a server render. A UI can warn that views will not survive a
|
|
103
|
+
* reload instead of silently losing them.
|
|
104
|
+
*/
|
|
105
|
+
isPersistent(): boolean;
|
|
106
|
+
}
|
|
107
|
+
/** The slice of the DOM Storage API this module uses. */
|
|
108
|
+
export interface ContentListSavedViewStorage {
|
|
109
|
+
getItem(key: string): string | null;
|
|
110
|
+
setItem(key: string, value: string): void;
|
|
111
|
+
removeItem(key: string): void;
|
|
112
|
+
}
|
|
113
|
+
export interface ContentListSavedViewStoreOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Storage backend. Defaults to `globalThis.localStorage` when it is reachable
|
|
116
|
+
* and writable; `null` forces the in-memory store.
|
|
117
|
+
*/
|
|
118
|
+
storage?: ContentListSavedViewStorage | null;
|
|
119
|
+
/** Distinguishes several mounted lists. Defaults to the adapter's surface id. */
|
|
120
|
+
surfaceId?: string;
|
|
121
|
+
/** Full storage key override; takes precedence over `surfaceId`. */
|
|
122
|
+
storageKey?: string;
|
|
123
|
+
/** Injectable clock, for deterministic tests. */
|
|
124
|
+
now?: () => Date;
|
|
125
|
+
/** Injectable id factory, for deterministic tests. */
|
|
126
|
+
createId?: () => string;
|
|
127
|
+
}
|
|
128
|
+
/** The outcome of restoring a saved view. */
|
|
129
|
+
export interface ContentListSavedViewRestoration {
|
|
130
|
+
/**
|
|
131
|
+
* The validated patch to apply, via `applyContentListViewState`. Selection
|
|
132
|
+
* and expansion are never present: a saved view names a query, not a set of
|
|
133
|
+
* rows someone had checked.
|
|
134
|
+
*/
|
|
135
|
+
state: Partial<DataTableViewState>;
|
|
136
|
+
/** Everything the validator refused, for reporting to the operator. */
|
|
137
|
+
dropped: ContentListStateDrop[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Creates the default saved-view store.
|
|
141
|
+
*
|
|
142
|
+
* Backed by `localStorage` when it is reachable and writable, and by an
|
|
143
|
+
* in-memory map otherwise — a server render, a private window, or a browser
|
|
144
|
+
* with site data blocked degrades to a store that works for the session
|
|
145
|
+
* instead of throwing on the first save.
|
|
146
|
+
*/
|
|
147
|
+
export declare function createContentListSavedViewStore(options?: ContentListSavedViewStoreOptions): ContentListLocalSavedViewStore;
|
|
148
|
+
/**
|
|
149
|
+
* An explicitly non-persistent store, for server rendering and for tests that
|
|
150
|
+
* must not touch ambient storage.
|
|
151
|
+
*/
|
|
152
|
+
export declare function createContentListMemorySavedViewStore(options?: Omit<ContentListSavedViewStoreOptions, 'storage'>): ContentListLocalSavedViewStore;
|
|
153
|
+
/**
|
|
154
|
+
* Validates a stored payload and returns the patch to apply.
|
|
155
|
+
*
|
|
156
|
+
* Accepts either a whole `ContentListSavedView` or a bare snapshot. Throws
|
|
157
|
+
* `TypeError` — via `hydrateDataTableSnapshot` — when the payload is not a
|
|
158
|
+
* supported snapshot at all; column-level problems are reported in `dropped`
|
|
159
|
+
* so a stale view still opens.
|
|
160
|
+
*/
|
|
161
|
+
export declare function restoreContentListSavedView(value: unknown, options?: ContentListStateValidationOptions): ContentListSavedViewRestoration;
|
|
162
|
+
/**
|
|
163
|
+
* Builds the input for `store.save` from a controller snapshot.
|
|
164
|
+
*
|
|
165
|
+
* Selection and expansion are stripped before the snapshot is stored: a saved
|
|
166
|
+
* view names a query, and persisting the rows someone had checked would restore
|
|
167
|
+
* a selection into a list whose rows have since changed.
|
|
168
|
+
*/
|
|
169
|
+
export declare function toContentListSavedViewInput(name: string, snapshot: DataTableSnapshot, options?: {
|
|
170
|
+
id?: string;
|
|
171
|
+
}): ContentListSavedViewInput;
|
|
172
|
+
//# sourceMappingURL=content-list-saved-views.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-list-saved-views.d.ts","sourceRoot":"","sources":["../../src/svelte/content-list-saved-views.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved views for ContentList (#2452).
|
|
3
|
+
*
|
|
4
|
+
* A saved view is a named `DataTableSnapshot` an operator can come back to. The
|
|
5
|
+
* payload is persisted outside the application's control — today in
|
|
6
|
+
* `localStorage`, which is writable by anyone with a console — so it is treated
|
|
7
|
+
* exactly like a URL: untrusted on the way back in.
|
|
8
|
+
*
|
|
9
|
+
* Restoration is therefore two gates, in order:
|
|
10
|
+
*
|
|
11
|
+
* 1. `hydrateDataTableSnapshot` (smrt-ui) parses the stored payload
|
|
12
|
+
* structurally, upgrades version 1/2 into normalized version 3, and throws
|
|
13
|
+
* on anything it cannot make sense of. It intentionally runs *without*
|
|
14
|
+
* column ids, so it normalizes shape but does not know which columns this
|
|
15
|
+
* list publishes.
|
|
16
|
+
* 2. `sanitizeContentListViewState` (`content-list-url-state.ts`) — the same
|
|
17
|
+
* validator the URL path uses — then holds the hydrated state to the
|
|
18
|
+
* adapter's published column and operator vocabulary. A stored view can no
|
|
19
|
+
* more restore a filter on the hidden `description` column, a structural
|
|
20
|
+
* `select`/`actions` column, or a column that has since been removed than a
|
|
21
|
+
* crafted link can.
|
|
22
|
+
*
|
|
23
|
+
* A stale view is not an error: a snapshot that references a column which no
|
|
24
|
+
* longer exists restores the valid remainder and reports the drops, so the UI
|
|
25
|
+
* can say what it discarded instead of refusing to open the list.
|
|
26
|
+
*
|
|
27
|
+
* The storage seam is deliberately narrow (`list`/`get`/`save`/`delete`) and
|
|
28
|
+
* asynchronous, so the server-backed store that lands later is a drop-in
|
|
29
|
+
* replacement rather than a signature change. Nothing here knows about a
|
|
30
|
+
* backend, a tenant, or a principal.
|
|
31
|
+
*/
|
|
32
|
+
import { hydrateDataTableSnapshot, } from '@happyvertical/smrt-ui/data';
|
|
33
|
+
import { CONTENT_LIST_SURFACE_ID } from './content-list-controller.js';
|
|
34
|
+
import { sanitizeContentListViewState, } from './content-list-url-state.js';
|
|
35
|
+
/**
|
|
36
|
+
* Envelope version for a stored view. Bumped only when the envelope around the
|
|
37
|
+
* snapshot changes; the snapshot carries its own `version` for the state.
|
|
38
|
+
*/
|
|
39
|
+
export const CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION = 1;
|
|
40
|
+
/** Prefix of the `localStorage` key a saved-view store owns. */
|
|
41
|
+
export const CONTENT_LIST_SAVED_VIEW_STORAGE_PREFIX = 'smrt:content-list:saved-views';
|
|
42
|
+
function storageKeyFor(options) {
|
|
43
|
+
if (options.storageKey)
|
|
44
|
+
return options.storageKey;
|
|
45
|
+
const surfaceId = options.surfaceId ?? CONTENT_LIST_SURFACE_ID;
|
|
46
|
+
return `${CONTENT_LIST_SAVED_VIEW_STORAGE_PREFIX}:v${CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION}:${surfaceId}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the ambient `localStorage`, or `null` when it is unreachable.
|
|
50
|
+
*
|
|
51
|
+
* Reading the property itself throws in some browsers when site data is
|
|
52
|
+
* blocked, and Safari's private mode throws on the first `setItem` rather than
|
|
53
|
+
* on access — so availability is probed with a real write, not assumed.
|
|
54
|
+
*/
|
|
55
|
+
function resolveAmbientStorage() {
|
|
56
|
+
try {
|
|
57
|
+
const storage = globalThis.localStorage;
|
|
58
|
+
if (!storage)
|
|
59
|
+
return null;
|
|
60
|
+
const probe = `${CONTENT_LIST_SAVED_VIEW_STORAGE_PREFIX}:probe`;
|
|
61
|
+
storage.setItem(probe, '1');
|
|
62
|
+
storage.removeItem(probe);
|
|
63
|
+
return storage;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function createMemoryStorage() {
|
|
70
|
+
const entries = new Map();
|
|
71
|
+
return {
|
|
72
|
+
getItem: (key) => entries.get(key) ?? null,
|
|
73
|
+
setItem: (key, value) => {
|
|
74
|
+
entries.set(key, value);
|
|
75
|
+
},
|
|
76
|
+
removeItem: (key) => {
|
|
77
|
+
entries.delete(key);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function defaultCreateId() {
|
|
82
|
+
try {
|
|
83
|
+
const cryptoRef = globalThis
|
|
84
|
+
.crypto;
|
|
85
|
+
if (typeof cryptoRef?.randomUUID === 'function') {
|
|
86
|
+
return cryptoRef.randomUUID();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Fall through to the non-cryptographic id below.
|
|
91
|
+
}
|
|
92
|
+
return `view-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
93
|
+
}
|
|
94
|
+
function isPlainObject(value) {
|
|
95
|
+
return (typeof value === 'object' &&
|
|
96
|
+
value !== null &&
|
|
97
|
+
!Array.isArray(value) &&
|
|
98
|
+
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
99
|
+
Object.getPrototypeOf(value) === null));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parses one stored entry, or returns `null` when it is unusable.
|
|
103
|
+
*
|
|
104
|
+
* Every stored record goes through `hydrateDataTableSnapshot`, so a hand-edited
|
|
105
|
+
* or truncated entry is discarded on read rather than handed to the controller.
|
|
106
|
+
*/
|
|
107
|
+
function parseStoredView(value) {
|
|
108
|
+
if (!isPlainObject(value))
|
|
109
|
+
return null;
|
|
110
|
+
const { id, name, snapshot, createdAt, updatedAt, schemaVersion } = value;
|
|
111
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
if (typeof name !== 'string')
|
|
114
|
+
return null;
|
|
115
|
+
if (schemaVersion !== undefined &&
|
|
116
|
+
schemaVersion !== CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
let hydrated;
|
|
120
|
+
try {
|
|
121
|
+
hydrated = hydrateDataTableSnapshot(snapshot);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const created = typeof createdAt === 'string' ? createdAt : '';
|
|
127
|
+
return {
|
|
128
|
+
id,
|
|
129
|
+
name,
|
|
130
|
+
schemaVersion: CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION,
|
|
131
|
+
snapshot: hydrated,
|
|
132
|
+
createdAt: created,
|
|
133
|
+
updatedAt: typeof updatedAt === 'string' ? updatedAt : created,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function sortViews(views) {
|
|
137
|
+
return views.sort((left, right) => {
|
|
138
|
+
if (left.updatedAt === right.updatedAt) {
|
|
139
|
+
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
140
|
+
}
|
|
141
|
+
return left.updatedAt < right.updatedAt ? 1 : -1;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Creates the default saved-view store.
|
|
146
|
+
*
|
|
147
|
+
* Backed by `localStorage` when it is reachable and writable, and by an
|
|
148
|
+
* in-memory map otherwise — a server render, a private window, or a browser
|
|
149
|
+
* with site data blocked degrades to a store that works for the session
|
|
150
|
+
* instead of throwing on the first save.
|
|
151
|
+
*/
|
|
152
|
+
export function createContentListSavedViewStore(options = {}) {
|
|
153
|
+
const key = storageKeyFor(options);
|
|
154
|
+
const now = options.now ?? (() => new Date());
|
|
155
|
+
const createId = options.createId ?? defaultCreateId;
|
|
156
|
+
const requested = options.storage === undefined ? resolveAmbientStorage() : options.storage;
|
|
157
|
+
let storage = requested ?? createMemoryStorage();
|
|
158
|
+
let persistent = requested !== null && requested !== undefined;
|
|
159
|
+
/** Demotes to memory the first time the backing storage misbehaves. */
|
|
160
|
+
function degrade() {
|
|
161
|
+
if (!persistent)
|
|
162
|
+
return;
|
|
163
|
+
persistent = false;
|
|
164
|
+
storage = createMemoryStorage();
|
|
165
|
+
}
|
|
166
|
+
function readAll() {
|
|
167
|
+
let raw;
|
|
168
|
+
try {
|
|
169
|
+
raw = storage.getItem(key);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
degrade();
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
if (!raw)
|
|
176
|
+
return [];
|
|
177
|
+
let parsed;
|
|
178
|
+
try {
|
|
179
|
+
parsed = JSON.parse(raw);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
if (!Array.isArray(parsed))
|
|
185
|
+
return [];
|
|
186
|
+
const views = [];
|
|
187
|
+
for (const entry of parsed) {
|
|
188
|
+
const view = parseStoredView(entry);
|
|
189
|
+
if (view)
|
|
190
|
+
views.push(view);
|
|
191
|
+
}
|
|
192
|
+
return views;
|
|
193
|
+
}
|
|
194
|
+
function writeAll(views) {
|
|
195
|
+
try {
|
|
196
|
+
storage.setItem(key, JSON.stringify(views));
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// A quota or private-mode failure must not lose the operator's view:
|
|
200
|
+
// demote to memory and keep the write.
|
|
201
|
+
degrade();
|
|
202
|
+
try {
|
|
203
|
+
storage.setItem(key, JSON.stringify(views));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// The in-memory fallback cannot fail; nothing further to do.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
isPersistent: () => persistent,
|
|
212
|
+
async list() {
|
|
213
|
+
return sortViews(readAll());
|
|
214
|
+
},
|
|
215
|
+
async get(id) {
|
|
216
|
+
return readAll().find((view) => view.id === id) ?? null;
|
|
217
|
+
},
|
|
218
|
+
async save(input) {
|
|
219
|
+
if (!input || typeof input.name !== 'string' || !input.name.trim()) {
|
|
220
|
+
throw new TypeError('A content list saved view requires a name');
|
|
221
|
+
}
|
|
222
|
+
// Hydrating on write keeps a malformed payload out of storage entirely,
|
|
223
|
+
// so the only tampered payloads the read path has to survive are ones
|
|
224
|
+
// written around this API.
|
|
225
|
+
const snapshot = hydrateDataTableSnapshot(input.snapshot);
|
|
226
|
+
const timestamp = now().toISOString();
|
|
227
|
+
const views = readAll();
|
|
228
|
+
const existing = input.id
|
|
229
|
+
? views.find((view) => view.id === input.id)
|
|
230
|
+
: undefined;
|
|
231
|
+
const view = {
|
|
232
|
+
id: existing?.id ?? input.id ?? createId(),
|
|
233
|
+
name: input.name.trim(),
|
|
234
|
+
schemaVersion: CONTENT_LIST_SAVED_VIEW_SCHEMA_VERSION,
|
|
235
|
+
snapshot,
|
|
236
|
+
createdAt: existing?.createdAt || timestamp,
|
|
237
|
+
updatedAt: timestamp,
|
|
238
|
+
};
|
|
239
|
+
writeAll([...views.filter((entry) => entry.id !== view.id), view]);
|
|
240
|
+
return view;
|
|
241
|
+
},
|
|
242
|
+
async delete(id) {
|
|
243
|
+
const views = readAll();
|
|
244
|
+
const next = views.filter((view) => view.id !== id);
|
|
245
|
+
if (next.length === views.length)
|
|
246
|
+
return false;
|
|
247
|
+
writeAll(next);
|
|
248
|
+
return true;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* An explicitly non-persistent store, for server rendering and for tests that
|
|
254
|
+
* must not touch ambient storage.
|
|
255
|
+
*/
|
|
256
|
+
export function createContentListMemorySavedViewStore(options = {}) {
|
|
257
|
+
// `null` is the documented "force memory" signal, so the store also reports
|
|
258
|
+
// itself as non-persistent rather than claiming durability it does not have.
|
|
259
|
+
return createContentListSavedViewStore({ ...options, storage: null });
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Validates a stored payload and returns the patch to apply.
|
|
263
|
+
*
|
|
264
|
+
* Accepts either a whole `ContentListSavedView` or a bare snapshot. Throws
|
|
265
|
+
* `TypeError` — via `hydrateDataTableSnapshot` — when the payload is not a
|
|
266
|
+
* supported snapshot at all; column-level problems are reported in `dropped`
|
|
267
|
+
* so a stale view still opens.
|
|
268
|
+
*/
|
|
269
|
+
export function restoreContentListSavedView(value, options = {}) {
|
|
270
|
+
const payload = isPlainObject(value) && Object.hasOwn(value, 'snapshot')
|
|
271
|
+
? value.snapshot
|
|
272
|
+
: value;
|
|
273
|
+
const hydrated = hydrateDataTableSnapshot(payload);
|
|
274
|
+
const { state, dropped } = sanitizeContentListViewState(hydrated.state, options);
|
|
275
|
+
return { state, dropped };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Builds the input for `store.save` from a controller snapshot.
|
|
279
|
+
*
|
|
280
|
+
* Selection and expansion are stripped before the snapshot is stored: a saved
|
|
281
|
+
* view names a query, and persisting the rows someone had checked would restore
|
|
282
|
+
* a selection into a list whose rows have since changed.
|
|
283
|
+
*/
|
|
284
|
+
export function toContentListSavedViewInput(name, snapshot, options = {}) {
|
|
285
|
+
return {
|
|
286
|
+
...(options.id ? { id: options.id } : {}),
|
|
287
|
+
name,
|
|
288
|
+
snapshot: {
|
|
289
|
+
...snapshot,
|
|
290
|
+
state: {
|
|
291
|
+
...snapshot.state,
|
|
292
|
+
selection: { scope: 'explicit', rowIds: [] },
|
|
293
|
+
selectedRowIds: [],
|
|
294
|
+
expandedRowIds: [],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|