@svadmin/ui 0.78.0 → 0.78.2

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.
@@ -0,0 +1,42 @@
1
+ import { SvelteMap } from 'svelte/reactivity';
2
+ import { type ListPreferenceScope, type SavedListView, type SavedListViewAccess, type SavedListViewProvider, type SavedListViewState, type SavedListViewSubject } from './saved-list-views.js';
3
+ interface SavedListViewsOptions {
4
+ scope: () => ListPreferenceScope;
5
+ columns: () => Set<string>;
6
+ provider: () => SavedListViewProvider | undefined;
7
+ isScopeLoaded: () => boolean;
8
+ readPreference: (scope: ListPreferenceScope, key: string, legacyKey: string) => string | null;
9
+ hasExplicitURLState: boolean;
10
+ canApplyRemoteDefault: () => boolean;
11
+ captureState: () => SavedListViewState;
12
+ applyState: (state: SavedListViewState) => void;
13
+ }
14
+ export declare function createSavedListViews(options: SavedListViewsOptions): {
15
+ initialSavedView: SavedListView | undefined;
16
+ readonly availableSavedViews: SavedListView[];
17
+ readonly activeSavedViewId: string | undefined;
18
+ readonly activeSavedViewName: string | undefined;
19
+ savedViewName: string;
20
+ savedViewsOpen: boolean;
21
+ savedViewSource: "local" | "team" | "system";
22
+ readonly remoteViewsLoading: boolean;
23
+ readonly remoteViewsFailed: boolean;
24
+ readonly savedViewMutationPending: boolean;
25
+ readonly savedViewMutationError: "failure" | "conflict" | undefined;
26
+ accessDrafts: SvelteMap<string, SavedListViewAccess>;
27
+ accessSubjects: SvelteMap<string, SavedListViewSubject[]>;
28
+ accessSubjectsLoading: SvelteMap<string, boolean>;
29
+ accessSubjectsFailed: SvelteMap<string, boolean>;
30
+ accessSubjectQueries: SvelteMap<string, string>;
31
+ restoreScope: (scope: ListPreferenceScope) => SavedListView | undefined;
32
+ markSavedViewDirty: () => void;
33
+ clearActiveSavedView: () => void;
34
+ applySavedView: (view: SavedListView) => void;
35
+ saveCurrentView: () => void;
36
+ deleteSavedView: (id: string) => void;
37
+ setRemoteDefault: (view: SavedListView) => void;
38
+ saveRemoteAccess: (view: SavedListView) => void;
39
+ loadAccessSubjects: (view: SavedListView, query?: string) => void;
40
+ reload(): void;
41
+ };
42
+ export {};
@@ -0,0 +1,405 @@
1
+ import { untrack } from 'svelte';
2
+ import { SvelteMap } from 'svelte/reactivity';
3
+ import { activeSavedListViewStorageKey, cloneSavedListViewState, decodeRemoteSavedListViews, decodeSavedListViewAccess, decodeSavedListViewMutationResult, decodeSavedListViewRemoveResult, decodeSavedListViewSubjects, legacyActiveSavedListViewStorageKey, legacySavedListViewsStorageKey, listPreferenceScopeId, readSavedListViews, savedListViewsStorageKey, serializeSavedListViews, } from './saved-list-views.js';
4
+ export function createSavedListViews(options) {
5
+ const listPreferenceScope = $derived(options.scope());
6
+ const savedViewColumnIds = $derived(options.columns());
7
+ const savedViewProvider = $derived(options.provider());
8
+ const preferenceScopeIsLoaded = options.isScopeLoaded;
9
+ const hasExplicitURLState = options.hasExplicitURLState;
10
+ function readPreferences(scope) {
11
+ const savedViews = readSavedListViews(options.readPreference(scope, savedListViewsStorageKey(scope), legacySavedListViewsStorageKey(scope.resourceName)), savedViewColumnIds);
12
+ const candidate = options.readPreference(scope, activeSavedListViewStorageKey(scope), legacyActiveSavedListViewStorageKey(scope.resourceName));
13
+ const activeSavedView = savedViews.find(view => view.id === candidate);
14
+ return { savedViews, activeSavedView };
15
+ }
16
+ const initial = untrack(() => readPreferences(listPreferenceScope));
17
+ const initialSavedView = hasExplicitURLState ? undefined : initial.activeSavedView;
18
+ let savedViews = $state(initial.savedViews);
19
+ let activeSavedViewId = $state(initialSavedView?.id);
20
+ let savedViewName = $state('');
21
+ let savedViewsOpen = $state(false);
22
+ let savedViewSource = $state('local');
23
+ let remoteSavedViews = $state([]);
24
+ let remoteViewsLoading = $state(false);
25
+ let remoteViewsFailed = $state(false);
26
+ let remoteViewsReload = $state(0);
27
+ let remoteDefaultEligible = true;
28
+ let savedViewMutationPending = $state(false);
29
+ let savedViewMutationError = $state();
30
+ let savedViewMutationEpoch = 0;
31
+ let remoteViewEpoch = 0;
32
+ const accessDrafts = new SvelteMap();
33
+ const accessSubjects = new SvelteMap();
34
+ const accessSubjectsLoading = new SvelteMap();
35
+ const accessSubjectsFailed = new SvelteMap();
36
+ const accessSubjectQueries = new SvelteMap();
37
+ const accessSubjectRequests = new Map();
38
+ const availableSavedViews = $derived.by(() => {
39
+ const ids = new Set();
40
+ return [...remoteSavedViews, ...savedViews].filter(view => {
41
+ if (ids.has(view.id))
42
+ return false;
43
+ ids.add(view.id);
44
+ return true;
45
+ });
46
+ });
47
+ const activeSavedViewName = $derived(availableSavedViews.find(view => view.id === activeSavedViewId)?.name);
48
+ function persistSavedViews() {
49
+ if (typeof window === 'undefined' || !preferenceScopeIsLoaded())
50
+ return;
51
+ try {
52
+ localStorage.setItem(savedListViewsStorageKey(listPreferenceScope), serializeSavedListViews(savedViews));
53
+ }
54
+ catch { /* ignore quota errors */ }
55
+ }
56
+ function persistActiveSavedView() {
57
+ if (typeof window === 'undefined' || !preferenceScopeIsLoaded())
58
+ return;
59
+ try {
60
+ const key = activeSavedListViewStorageKey(listPreferenceScope);
61
+ if (activeSavedViewId)
62
+ localStorage.setItem(key, activeSavedViewId);
63
+ else
64
+ localStorage.removeItem(key);
65
+ }
66
+ catch { /* ignore quota errors */ }
67
+ }
68
+ function markSavedViewDirty() {
69
+ remoteDefaultEligible = false;
70
+ if (!activeSavedViewId)
71
+ return;
72
+ clearActiveSavedView();
73
+ }
74
+ function clearActiveSavedView() {
75
+ remoteDefaultEligible = false;
76
+ activeSavedViewId = undefined;
77
+ persistActiveSavedView();
78
+ }
79
+ function applySavedView(view) {
80
+ remoteDefaultEligible = false;
81
+ const state = cloneSavedListViewState(view.state);
82
+ activeSavedViewId = view.id;
83
+ persistActiveSavedView();
84
+ options.applyState(state);
85
+ }
86
+ // 表格完成列偏好和查询恢复后才打开持久化门,避免把旧租户状态写入新作用域。
87
+ function restoreScope(scope) {
88
+ remoteDefaultEligible = true;
89
+ const preferences = readPreferences(scope);
90
+ savedViewName = '';
91
+ savedViewsOpen = false;
92
+ savedViews = preferences.savedViews;
93
+ remoteSavedViews = [];
94
+ activeSavedViewId = preferences.activeSavedView?.id;
95
+ return preferences.activeSavedView;
96
+ }
97
+ $effect(() => {
98
+ const provider = savedViewProvider;
99
+ const scope = listPreferenceScope;
100
+ const columns = savedViewColumnIds;
101
+ void remoteViewsReload;
102
+ remoteSavedViews = [];
103
+ remoteViewsLoading = false;
104
+ remoteViewsFailed = false;
105
+ accessDrafts.clear();
106
+ accessSubjects.clear();
107
+ accessSubjectsLoading.clear();
108
+ accessSubjectsFailed.clear();
109
+ accessSubjectQueries.clear();
110
+ accessSubjectRequests.clear();
111
+ savedViewMutationEpoch += 1;
112
+ savedViewMutationPending = false;
113
+ savedViewMutationError = undefined;
114
+ savedViewSource = 'local';
115
+ if (!provider || !preferenceScopeIsLoaded())
116
+ return;
117
+ const epoch = ++remoteViewEpoch;
118
+ remoteViewsLoading = true;
119
+ void Promise.resolve().then(() => provider.list({ ...scope })).then(value => {
120
+ if (epoch !== remoteViewEpoch || !preferenceScopeIsLoaded())
121
+ return;
122
+ remoteSavedViews = decodeRemoteSavedListViews(value, columns, !!(provider.save || provider.remove || provider.setDefault || provider.updateAccess));
123
+ if (options.canApplyRemoteDefault() && remoteDefaultEligible) {
124
+ const defaults = remoteSavedViews.filter(view => view.default === true);
125
+ if (!hasExplicitURLState && !activeSavedViewId && defaults.length === 1 && defaults[0]) {
126
+ applySavedView(defaults[0]);
127
+ remoteDefaultEligible = false;
128
+ }
129
+ }
130
+ }).catch(() => {
131
+ if (epoch === remoteViewEpoch) {
132
+ remoteSavedViews = [];
133
+ remoteViewsFailed = true;
134
+ }
135
+ }).finally(() => {
136
+ if (epoch === remoteViewEpoch)
137
+ remoteViewsLoading = false;
138
+ });
139
+ return () => {
140
+ remoteViewEpoch += 1;
141
+ savedViewMutationEpoch += 1;
142
+ accessSubjectRequests.clear();
143
+ };
144
+ });
145
+ function saveCurrentView() {
146
+ if (!preferenceScopeIsLoaded() || savedViewMutationPending)
147
+ return;
148
+ remoteDefaultEligible = false;
149
+ const name = savedViewName.trim().slice(0, 60);
150
+ if (!name)
151
+ return;
152
+ const state = options.captureState();
153
+ if (savedViewSource !== 'local') {
154
+ const existing = remoteSavedViews.find(view => view.source === savedViewSource
155
+ && view.name.toLocaleLowerCase() === name.toLocaleLowerCase());
156
+ if (!savedViewProvider?.save || (existing && (existing.readOnly || !existing.version)))
157
+ return;
158
+ void mutateRemoteView({
159
+ id: existing?.id ?? crypto.randomUUID(), name, state,
160
+ source: savedViewSource, expectedVersion: existing?.version ?? null,
161
+ });
162
+ return;
163
+ }
164
+ const existing = savedViews.find(view => view.name.toLocaleLowerCase() === name.toLocaleLowerCase());
165
+ const view = {
166
+ id: existing?.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
167
+ name, state,
168
+ };
169
+ savedViews = existing
170
+ ? savedViews.map(candidate => candidate.id === existing.id ? view : candidate)
171
+ : [view, ...savedViews].slice(0, 25);
172
+ activeSavedViewId = view.id;
173
+ savedViewName = '';
174
+ persistSavedViews();
175
+ persistActiveSavedView();
176
+ }
177
+ function deleteSavedView(id) {
178
+ if (!preferenceScopeIsLoaded() || savedViewMutationPending)
179
+ return;
180
+ const remote = remoteSavedViews.find(view => view.id === id);
181
+ if (remote) {
182
+ if (!savedViewProvider?.remove || remote.readOnly || !remote.version)
183
+ return;
184
+ void mutateRemoteView({
185
+ id, expectedVersion: remote.version, source: remote.source === 'system' ? 'system' : 'team',
186
+ name: remote.name, state: remote.state,
187
+ }, true);
188
+ return;
189
+ }
190
+ savedViews = savedViews.filter(view => view.id !== id);
191
+ if (activeSavedViewId === id)
192
+ activeSavedViewId = undefined;
193
+ persistSavedViews();
194
+ persistActiveSavedView();
195
+ }
196
+ async function mutateRemoteView(mutation, remove = false) {
197
+ const provider = savedViewProvider;
198
+ if (!provider || savedViewMutationPending || !preferenceScopeIsLoaded()
199
+ || (remove && (mutation.expectedVersion === null || !provider.remove))
200
+ || (!remove && !provider.save))
201
+ return;
202
+ const scope = { ...listPreferenceScope };
203
+ const scopeId = listPreferenceScopeId(scope);
204
+ const columns = savedViewColumnIds;
205
+ const epoch = ++savedViewMutationEpoch;
206
+ const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
207
+ && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
208
+ savedViewMutationPending = true;
209
+ savedViewMutationError = undefined;
210
+ try {
211
+ const result = remove
212
+ ? await provider.remove?.(scope, { id: mutation.id, expectedVersion: mutation.expectedVersion ?? 0 })
213
+ : await provider.save?.(scope, { ...mutation, state: cloneSavedListViewState(mutation.state) });
214
+ if (!current())
215
+ return;
216
+ if (remove && mutation.expectedVersion !== null && decodeSavedListViewRemoveResult(result, {
217
+ id: mutation.id, expectedVersion: mutation.expectedVersion,
218
+ })) {
219
+ remoteSavedViews = remoteSavedViews.filter(view => view.id !== mutation.id);
220
+ if (activeSavedViewId === mutation.id)
221
+ activeSavedViewId = undefined;
222
+ persistActiveSavedView();
223
+ return;
224
+ }
225
+ const decoded = decodeSavedListViewMutationResult(result, columns, mutation);
226
+ if (!decoded || (remove && decoded.ok)) {
227
+ savedViewMutationError = 'failure';
228
+ return;
229
+ }
230
+ if (!decoded.ok) {
231
+ savedViewMutationError = 'conflict';
232
+ // 保留本地查询;用户必须重新读取后再决定如何修改。
233
+ return;
234
+ }
235
+ const view = { ...decoded.view, readOnly: decoded.view.readOnly !== false };
236
+ remoteSavedViews = [view, ...remoteSavedViews.filter(candidate => candidate.id !== view.id)];
237
+ savedViewName = '';
238
+ }
239
+ catch {
240
+ if (current())
241
+ savedViewMutationError = 'failure';
242
+ }
243
+ finally {
244
+ if (current())
245
+ savedViewMutationPending = false;
246
+ }
247
+ }
248
+ function setRemoteDefault(view) {
249
+ const provider = savedViewProvider;
250
+ if (!provider?.setDefault || savedViewMutationPending || view.readOnly || !view.version
251
+ || savedViewMutationError === 'conflict' || !preferenceScopeIsLoaded()
252
+ || (view.source !== 'team' && view.source !== 'system'))
253
+ return;
254
+ const source = view.source;
255
+ const expectedVersion = view.version;
256
+ const defaultValue = view.default !== true;
257
+ remoteDefaultEligible = false;
258
+ const scope = { ...listPreferenceScope };
259
+ const scopeId = listPreferenceScopeId(scope);
260
+ const epoch = ++savedViewMutationEpoch;
261
+ const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
262
+ && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
263
+ savedViewMutationPending = true;
264
+ savedViewMutationError = undefined;
265
+ void Promise.resolve().then(() => provider.setDefault?.(scope, {
266
+ id: view.id, source, expectedVersion, default: defaultValue,
267
+ })).then(result => {
268
+ if (!current())
269
+ return;
270
+ const decoded = decodeSavedListViewMutationResult(result, savedViewColumnIds, {
271
+ id: view.id, source, expectedVersion,
272
+ });
273
+ if (!decoded) {
274
+ savedViewMutationError = 'failure';
275
+ }
276
+ else if (!decoded.ok) {
277
+ savedViewMutationError = 'conflict';
278
+ }
279
+ else if (decoded.view.default !== defaultValue) {
280
+ savedViewMutationError = 'failure';
281
+ }
282
+ else {
283
+ // 默认标记会同时改变同一作用域的其他视图,成功后重新读取完整集合。
284
+ remoteViewsReload += 1;
285
+ }
286
+ }).catch(() => {
287
+ if (current())
288
+ savedViewMutationError = 'failure';
289
+ }).finally(() => {
290
+ if (current())
291
+ savedViewMutationPending = false;
292
+ });
293
+ }
294
+ function saveRemoteAccess(view) {
295
+ const provider = savedViewProvider;
296
+ const draft = accessDrafts.get(view.id);
297
+ if (!provider?.updateAccess || !draft || view.readOnly !== false || !view.version
298
+ || savedViewMutationPending || savedViewMutationError === 'conflict' || !preferenceScopeIsLoaded()
299
+ || (view.source !== 'team' && view.source !== 'system'))
300
+ return;
301
+ const access = decodeSavedListViewAccess({
302
+ mode: draft.mode,
303
+ subjectIds: draft.mode === 'restricted' ? draft.subjectIds : [],
304
+ });
305
+ if (!access) {
306
+ savedViewMutationError = 'failure';
307
+ return;
308
+ }
309
+ const source = view.source;
310
+ const expectedVersion = view.version;
311
+ const scope = { ...listPreferenceScope };
312
+ const scopeId = listPreferenceScopeId(scope);
313
+ const epoch = ++savedViewMutationEpoch;
314
+ const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
315
+ && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
316
+ remoteDefaultEligible = false;
317
+ savedViewMutationPending = true;
318
+ savedViewMutationError = undefined;
319
+ void Promise.resolve().then(() => {
320
+ if (!current())
321
+ return;
322
+ return provider.updateAccess?.(scope, {
323
+ id: view.id, source, expectedVersion, access: { ...access, subjectIds: [...access.subjectIds] },
324
+ });
325
+ }).then(result => {
326
+ if (!current())
327
+ return;
328
+ const decoded = decodeSavedListViewMutationResult(result, savedViewColumnIds, {
329
+ id: view.id, source, expectedVersion,
330
+ });
331
+ if (!decoded)
332
+ savedViewMutationError = 'failure';
333
+ else if (!decoded.ok)
334
+ savedViewMutationError = 'conflict';
335
+ else if (decoded.view.access?.mode !== access.mode
336
+ || decoded.view.access.subjectIds.length !== access.subjectIds.length
337
+ || !decoded.view.access.subjectIds.every(id => access.subjectIds.includes(id))) {
338
+ savedViewMutationError = 'failure';
339
+ }
340
+ else
341
+ remoteViewsReload += 1;
342
+ }).catch(() => {
343
+ if (current())
344
+ savedViewMutationError = 'failure';
345
+ }).finally(() => {
346
+ if (current())
347
+ savedViewMutationPending = false;
348
+ });
349
+ }
350
+ function loadAccessSubjects(view, query = '') {
351
+ const provider = savedViewProvider;
352
+ if (!provider?.listAccessSubjects || view.readOnly !== false
353
+ || view.source === 'local' || !preferenceScopeIsLoaded())
354
+ return;
355
+ const scope = { ...listPreferenceScope };
356
+ const scopeId = listPreferenceScopeId(scope);
357
+ const request = {};
358
+ accessSubjectRequests.set(view.id, request);
359
+ const current = () => accessSubjectRequests.get(view.id) === request
360
+ && scopeId === listPreferenceScopeId(listPreferenceScope)
361
+ && provider === savedViewProvider && preferenceScopeIsLoaded();
362
+ accessSubjectsLoading.set(view.id, true);
363
+ accessSubjectsFailed.delete(view.id);
364
+ accessSubjects.delete(view.id);
365
+ void Promise.resolve().then(() => {
366
+ if (current())
367
+ return provider.listAccessSubjects?.(scope, { query: query.trim(), limit: 50 });
368
+ })
369
+ .then(result => {
370
+ if (!current())
371
+ return;
372
+ const subjects = decodeSavedListViewSubjects(result);
373
+ if (!subjects)
374
+ accessSubjectsFailed.set(view.id, true);
375
+ else
376
+ accessSubjects.set(view.id, subjects);
377
+ })
378
+ .catch(() => {
379
+ if (current())
380
+ accessSubjectsFailed.set(view.id, true);
381
+ })
382
+ .finally(() => { if (current())
383
+ accessSubjectsLoading.delete(view.id); });
384
+ }
385
+ return {
386
+ initialSavedView,
387
+ get availableSavedViews() { return availableSavedViews; },
388
+ get activeSavedViewId() { return activeSavedViewId; },
389
+ get activeSavedViewName() { return activeSavedViewName; },
390
+ get savedViewName() { return savedViewName; },
391
+ set savedViewName(value) { savedViewName = value; },
392
+ get savedViewsOpen() { return savedViewsOpen; },
393
+ set savedViewsOpen(value) { savedViewsOpen = value; },
394
+ get savedViewSource() { return savedViewSource; },
395
+ set savedViewSource(value) { savedViewSource = value; },
396
+ get remoteViewsLoading() { return remoteViewsLoading; },
397
+ get remoteViewsFailed() { return remoteViewsFailed; },
398
+ get savedViewMutationPending() { return savedViewMutationPending; },
399
+ get savedViewMutationError() { return savedViewMutationError; },
400
+ accessDrafts, accessSubjects, accessSubjectsLoading, accessSubjectsFailed, accessSubjectQueries,
401
+ restoreScope, markSavedViewDirty, clearActiveSavedView, applySavedView, saveCurrentView, deleteSavedView,
402
+ setRemoteDefault, saveRemoteAccess, loadAccessSubjects,
403
+ reload() { remoteViewsReload += 1; },
404
+ };
405
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.78.0",
3
+ "version": "0.78.2",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -88,7 +88,7 @@
88
88
  "@codemirror/lang-json": "^6.0.0",
89
89
  "@codemirror/lang-markdown": "^6.0.0",
90
90
  "@codemirror/lang-sql": "^6.0.0",
91
- "@svadmin/core": "^0.57.0",
91
+ "@svadmin/core": "^0.58.0",
92
92
  "@tanstack/svelte-query": "^6.1.48",
93
93
  "codemirror": "^6.0.0",
94
94
  "isomorphic-dompurify": "^3.23.0",
@@ -175,7 +175,7 @@
175
175
  "@codemirror/lang-markdown": "^6.5.2",
176
176
  "@codemirror/lang-sql": "^6.10.0",
177
177
  "@happy-dom/global-registrator": "^20.11.12",
178
- "@svadmin/ai-elements": "^0.10.2",
178
+ "@svadmin/ai-elements": "^0.10.3",
179
179
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
180
180
  "@tailwindcss/cli": "4.3.3",
181
181
  "@testing-library/dom": "^10.4.1",