@svadmin/ui 0.78.1 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/app.css +187 -1
  2. package/dist/app.theme.css +187 -1
  3. package/dist/components/AutoTable.svelte +183 -962
  4. package/dist/components/content/AuditContent.svelte +33 -0
  5. package/dist/components/content/AuditContent.svelte.d.ts +11 -0
  6. package/dist/components/content/AuditSection.svelte +46 -0
  7. package/dist/components/content/AuditSection.svelte.d.ts +18 -0
  8. package/dist/components/content/ChartContainer.svelte +32 -0
  9. package/dist/components/content/ChartContainer.svelte.d.ts +15 -0
  10. package/dist/components/content/CoverageMatrix.svelte +49 -0
  11. package/dist/components/content/CoverageMatrix.svelte.d.ts +25 -0
  12. package/dist/components/content/EvidenceCard.svelte +35 -0
  13. package/dist/components/content/EvidenceCard.svelte.d.ts +14 -0
  14. package/dist/components/content/FilterToolbar.svelte +24 -4
  15. package/dist/components/content/FilterToolbar.svelte.d.ts +1 -1
  16. package/dist/components/content/StatusPill.svelte +16 -0
  17. package/dist/components/content/StatusPill.svelte.d.ts +9 -0
  18. package/dist/components/content/audit-state.d.ts +1 -0
  19. package/dist/components/content/audit-state.js +1 -0
  20. package/dist/components/content/index.d.ts +9 -0
  21. package/dist/components/content/index.js +5 -0
  22. package/dist/components/list-column-preferences.svelte.d.ts +29 -0
  23. package/dist/components/list-column-preferences.svelte.js +92 -0
  24. package/dist/components/list-deletion.svelte.d.ts +38 -0
  25. package/dist/components/list-deletion.svelte.js +111 -0
  26. package/dist/components/list-selection.svelte.d.ts +31 -0
  27. package/dist/components/list-selection.svelte.js +100 -0
  28. package/dist/components/list-state.svelte.d.ts +49 -0
  29. package/dist/components/list-state.svelte.js +225 -0
  30. package/dist/components/saved-list-views.svelte.d.ts +42 -0
  31. package/dist/components/saved-list-views.svelte.js +405 -0
  32. package/dist/index.d.ts +9 -0
  33. package/dist/index.js +5 -0
  34. package/package.json +1 -1
@@ -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/dist/index.d.ts CHANGED
@@ -330,6 +330,15 @@ export type { RowActionItem } from './components/RowActions.svelte';
330
330
  export { default as DetailDrawer } from './components/DetailDrawer.svelte';
331
331
  export { default as AuditTimeline } from './components/content/AuditTimeline.svelte';
332
332
  export type { TimelineItem } from './components/content/AuditTimeline.svelte';
333
+ export { default as AuditSection } from './components/content/AuditSection.svelte';
334
+ export type { AuditAccent } from './components/content/AuditSection.svelte';
335
+ export type { AuditDataState } from './components/content/audit-state.js';
336
+ export { default as ChartContainer } from './components/content/ChartContainer.svelte';
337
+ export { default as CoverageMatrix } from './components/content/CoverageMatrix.svelte';
338
+ export type { CoverageMatrixColumn, CoverageMatrixRow } from './components/content/CoverageMatrix.svelte';
339
+ export { default as EvidenceCard } from './components/content/EvidenceCard.svelte';
340
+ export { default as StatusPill } from './components/content/StatusPill.svelte';
341
+ export type { StatusPillTone } from './components/content/StatusPill.svelte';
333
342
  export type { TagTone } from './components/fields/TagField.svelte';
334
343
  export type { BooleanTone } from './components/fields/BooleanField.svelte';
335
344
  export type { DateFieldFormat } from './components/fields/DateField.svelte';
package/dist/index.js CHANGED
@@ -279,6 +279,11 @@ export { referenceDemoData } from './reference-data.js';
279
279
  export { default as RowActions } from './components/RowActions.svelte';
280
280
  export { default as DetailDrawer } from './components/DetailDrawer.svelte';
281
281
  export { default as AuditTimeline } from './components/content/AuditTimeline.svelte';
282
+ export { default as AuditSection } from './components/content/AuditSection.svelte';
283
+ export { default as ChartContainer } from './components/content/ChartContainer.svelte';
284
+ export { default as CoverageMatrix } from './components/content/CoverageMatrix.svelte';
285
+ export { default as EvidenceCard } from './components/content/EvidenceCard.svelte';
286
+ export { default as StatusPill } from './components/content/StatusPill.svelte';
282
287
  export { default as FilterDropdown } from './components/FilterDropdown.svelte';
283
288
  export { default as BatchActionBar } from './components/BatchActionBar.svelte';
284
289
  export { default as MediaThumbnail } from './components/content/MediaThumbnail.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.78.1",
3
+ "version": "0.79.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [