@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
@@ -3,8 +3,7 @@
3
3
  import type { AutoTableGridState } from './auto-table-grid.js';
4
4
  import { definedOptions } from '@svadmin/core/options';
5
5
 
6
- import { onDestroy, tick, untrack } from 'svelte';
7
- import { SvelteMap } from 'svelte/reactivity';
6
+ import { untrack } from 'svelte';
8
7
  import { cn } from '../utils.js';
9
8
  import {
10
9
  createTable,
@@ -17,8 +16,6 @@
17
16
  rowSortingFeature,
18
17
  type ColumnDef,
19
18
  type SortingState,
20
- type RowSelectionState,
21
- type ColumnVisibilityState,
22
19
  type ExpandedState,
23
20
  } from '@tanstack/svelte-table';
24
21
  import type { Column, Header, Row, TableFeatures } from '@tanstack/table-core';
@@ -40,8 +37,8 @@
40
37
  cell_getValue,
41
38
  } from '@tanstack/table-core/static-functions';
42
39
 
43
- import { captureAdminContext, captureAuthSession, useGetIdentity, DeleteManyPartialError, getAdminOptions, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData, type ExportFormat, type TaskProvider } from '@svadmin/core';
44
- import { decodeBaseRecord, snapshotPlainData, parseContractRouteId, formatContractRouteId } from '@svadmin/core/schema';
40
+ import { captureAdminContext, captureAuthSession, useGetIdentity, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData, type ExportFormat, type TaskProvider } from '@svadmin/core';
41
+ import { snapshotPlainData, parseContractRouteId, formatContractRouteId } from '@svadmin/core/schema';
45
42
  import {
46
43
  checkedTableRows,
47
44
  copyTableRecord,
@@ -53,37 +50,21 @@
53
50
  import type {
54
51
  BaseRecord,
55
52
  FieldDefinition,
56
- Filter,
57
- LogicalFilter,
58
- Pagination as PaginationState,
59
53
  Sort,
60
54
  } from '@svadmin/core';
61
55
  import { useCan } from '@svadmin/core';
62
56
  import { readURLState, writeURLState } from '@svadmin/core';
63
57
  import { useTranslation } from '@svadmin/core/i18n';
58
+ import { createSavedListViews } from './saved-list-views.svelte.js';
59
+ import { createListState } from './list-state.svelte.js';
60
+ import { createListSelection } from './list-selection.svelte.js';
61
+ import { createListColumnPreferences } from './list-column-preferences.svelte.js';
62
+ import { createListDeletion } from './list-deletion.svelte.js';
64
63
  import {
65
- activeSavedListViewStorageKey,
66
64
  canMigrateLegacyListPreferences,
67
- cloneSavedListViewState,
68
- columnOrderStorageKey,
69
- columnVisibilityStorageKey,
70
- decodeRemoteSavedListViews,
71
- decodeSavedListViewMutationResult,
72
- decodeSavedListViewRemoveResult,
73
65
  decodeSavedListViewAccess,
74
- decodeSavedListViewSubjects,
75
- legacyActiveSavedListViewStorageKey,
76
- legacyColumnOrderStorageKey,
77
- legacyColumnVisibilityStorageKey,
78
- legacySavedListViewsStorageKey,
79
66
  listPreferenceScopeId,
80
- readSavedListViews,
81
- savedListViewsStorageKey,
82
- serializeSavedListViews,
83
67
  type ListPreferenceScope,
84
- type SavedListView,
85
- type SavedListViewAccess,
86
- type SavedListViewSubject,
87
68
  type SavedListViewState,
88
69
  type SavedListViewProvider,
89
70
  } from './saved-list-views.js';
@@ -204,8 +185,6 @@
204
185
  }: Props = $props();
205
186
 
206
187
  let densityOverride = $state<'compact' | 'comfortable' | undefined>(undefined);
207
- let allMatchingSelected = $state(false);
208
- const excludedMatchingIds = new SvelteMap<string, string | number>();
209
188
  const currentDensity = $derived(densityOverride ?? density);
210
189
  const adminContext = captureAdminContext();
211
190
  const parsed = useParsed();
@@ -263,49 +242,6 @@
263
242
  ...resource.fields.map((field) => field.key),
264
243
  '_select', '_expand', '_actions',
265
244
  ]));
266
- const storedSavedViews = untrack(() => readSavedListViews(
267
- readScopedPreference(
268
- listPreferenceScope,
269
- savedListViewsStorageKey(listPreferenceScope),
270
- legacySavedListViewsStorageKey(resourceName),
271
- ),
272
- savedViewColumnIds,
273
- ));
274
- let savedViews = $state<SavedListView[]>(storedSavedViews);
275
- let remoteSavedViews = $state<SavedListView[]>([]);
276
- let remoteViewsLoading = $state(false);
277
- let remoteViewsFailed = $state(false);
278
- let remoteViewsReload = $state(0);
279
- let remoteDefaultEligible = true;
280
- const availableSavedViews = $derived.by(() => {
281
- const ids = new Set<string>();
282
- return [...remoteSavedViews, ...savedViews].filter(view => {
283
- if (ids.has(view.id)) return false;
284
- ids.add(view.id);
285
- return true;
286
- });
287
- });
288
- const storedActiveSavedViewId = untrack(() => {
289
- const candidate = readScopedPreference(
290
- listPreferenceScope,
291
- activeSavedListViewStorageKey(listPreferenceScope),
292
- legacyActiveSavedListViewStorageKey(resourceName),
293
- );
294
- return storedSavedViews.some((view) => view.id === candidate) ? candidate ?? undefined : undefined;
295
- });
296
- let savedViewName = $state('');
297
- let savedViewsOpen = $state(false);
298
- let savedViewSource = $state<'local' | 'team' | 'system'>('local');
299
- let savedViewMutationPending = $state(false);
300
- let savedViewMutationError = $state<'failure' | 'conflict' | undefined>();
301
- const accessDrafts = new SvelteMap<string, SavedListViewAccess>();
302
- const accessSubjects = new SvelteMap<string, SavedListViewSubject[]>();
303
- const accessSubjectsLoading = new SvelteMap<string, boolean>();
304
- const accessSubjectsFailed = new SvelteMap<string, boolean>();
305
- const accessSubjectQueries = new SvelteMap<string, string>();
306
- const accessSubjectRequests = new Map<string, object>();
307
- let savedViewMutationEpoch = 0;
308
-
309
245
  const hasExplicitURLState = untrack(() => (
310
246
  urlState.page !== undefined
311
247
  || urlState.pageSize !== undefined
@@ -313,237 +249,64 @@
313
249
  || urlState.search !== undefined
314
250
  || urlState.filters !== undefined
315
251
  ));
316
- let activeSavedViewId = $state<string | undefined>(hasExplicitURLState ? undefined : storedActiveSavedViewId);
317
- const activeSavedViewName = $derived(availableSavedViews.find((view) => view.id === activeSavedViewId)?.name);
318
- const initialSavedView = untrack(() => (
319
- !hasExplicitURLState && storedActiveSavedViewId
320
- ? storedSavedViews.find((view) => view.id === storedActiveSavedViewId)
321
- : undefined
322
- ));
323
- const initialViewState = initialSavedView?.state;
252
+ const savedViewModel = createSavedListViews({
253
+ scope: () => listPreferenceScope,
254
+ columns: () => savedViewColumnIds,
255
+ provider: () => savedViewProvider,
256
+ isScopeLoaded: preferenceScopeIsLoaded,
257
+ readPreference: readScopedPreference,
258
+ hasExplicitURLState,
259
+ canApplyRemoteDefault: () => applyRemoteDefaultView && !externalPagination && !externalSorters,
260
+ captureState: () => {
261
+ listState.flushSearch();
262
+ return getCurrentSavedViewState();
263
+ },
264
+ applyState: applySavedViewState,
265
+ });
266
+ const {
267
+ markSavedViewDirty, applySavedView, saveCurrentView, deleteSavedView,
268
+ setRemoteDefault, saveRemoteAccess, loadAccessSubjects,
269
+ accessDrafts, accessSubjects, accessSubjectsLoading, accessSubjectsFailed, accessSubjectQueries,
270
+ } = savedViewModel;
271
+ const initialViewState = savedViewModel.initialSavedView?.state;
324
272
 
325
273
  // The table can stay mounted while the surrounding tenant/provider changes.
326
274
  // Keep persistence effects paused until the new scope has been loaded.
327
275
  let loadedPreferenceScopeId = $state(untrack(() => listPreferenceScopeId(listPreferenceScope)));
328
276
 
329
- function readColumnVisibilityPreference(scope: ListPreferenceScope): ColumnVisibilityState {
330
- const stored = readScopedPreference(
331
- scope,
332
- columnVisibilityStorageKey(scope),
333
- legacyColumnVisibilityStorageKey(resourceName),
334
- );
335
- if (stored) {
336
- try {
337
- const parsed: unknown = JSON.parse(stored);
338
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
339
- const visibility: ColumnVisibilityState = {};
340
- for (const [columnId, visible] of Object.entries(decodeBaseRecord(parsed))) {
341
- if (savedViewColumnIds.has(columnId) && typeof visible === 'boolean') {
342
- visibility[columnId] = visible;
343
- }
344
- }
345
- return visibility;
346
- }
347
- } catch { /* fall through to resource defaults */ }
348
- }
349
-
350
- const visibility: ColumnVisibilityState = {};
351
- for (const field of resource.fields) {
352
- if (field.showInList === false) visibility[field.key] = false;
353
- }
354
- return visibility;
355
- }
356
-
357
- function readColumnOrderPreference(scope: ListPreferenceScope): string[] {
358
- const stored = readScopedPreference(
359
- scope,
360
- columnOrderStorageKey(scope),
361
- legacyColumnOrderStorageKey(resourceName),
362
- );
363
- if (!stored) return [];
364
- try {
365
- const parsed: unknown = JSON.parse(stored);
366
- return Array.isArray(parsed)
367
- ? [...new Set(parsed.filter((columnId: unknown): columnId is string => (
368
- typeof columnId === 'string' && savedViewColumnIds.has(columnId)
369
- )))]
370
- : [];
371
- } catch {
372
- return [];
373
- }
374
- }
375
-
376
- function readScopedSavedViewPreferences(scope: ListPreferenceScope): {
377
- savedViews: SavedListView[];
378
- activeSavedViewId?: string;
379
- activeSavedView?: SavedListView;
380
- columnVisibility: ColumnVisibilityState;
381
- columnOrder: string[];
382
- } {
383
- const scopedSavedViews = readSavedListViews(
384
- readScopedPreference(
385
- scope,
386
- savedListViewsStorageKey(scope),
387
- legacySavedListViewsStorageKey(resourceName),
388
- ),
389
- savedViewColumnIds,
390
- );
391
- const candidate = readScopedPreference(
392
- scope,
393
- activeSavedListViewStorageKey(scope),
394
- legacyActiveSavedListViewStorageKey(resourceName),
395
- );
396
- const activeSavedView = scopedSavedViews.find((view) => view.id === candidate);
397
- return definedOptions({
398
- savedViews: scopedSavedViews,
399
- activeSavedViewId: activeSavedView?.id,
400
- activeSavedView,
401
- columnVisibility: readColumnVisibilityPreference(scope),
402
- columnOrder: readColumnOrderPreference(scope),
403
- });
404
- }
405
-
406
277
  // Snapshot resource values for initial state (untrack to avoid reactive tracking)
407
278
  const storedPageSize = parseInt(readLocalPreference('svadmin-default-page-size') ?? '', 10);
408
279
  const initPageSize = $derived(resource.pageSize ?? (Number.isSafeInteger(storedPageSize) && storedPageSize > 0 ? storedPageSize : 10));
409
- const initDefaultSort = $derived(resource.defaultSort);
410
-
411
- let pagination = $state<PaginationState>(untrack(() => externalPagination ?? {
412
- current: urlState.page ?? initialViewState?.pagination.current ?? 1,
413
- pageSize: urlState.pageSize ?? initialViewState?.pagination.pageSize ?? initPageSize,
414
- }));
415
- let sorters = $state<Sort[]>(untrack(() =>
416
- externalSorters ??
417
- (urlState.sortField
418
- ? [{ field: urlState.sortField, order: urlState.sortOrder ?? 'asc' }]
419
- : initialViewState?.sorters ?? (initDefaultSort ? [initDefaultSort] : [])))
420
- );
421
- const initialURLFilters = untrack(() => urlState.filters ?? initialViewState?.filters ?? []);
422
- const editableFilterKeys = $derived(new Set(resource.fields.filter((field) => field.filterable).map((field) => field.key)));
423
- const editableFilterCounts: Record<string, number> = {};
424
- for (const filter of initialURLFilters) {
425
- if ('field' in filter && filter.operator === 'contains' && typeof filter.value === 'string' && untrack(() => editableFilterKeys.has(filter.field))) {
426
- editableFilterCounts[filter.field] = (editableFilterCounts[filter.field] ?? 0) + 1;
427
- }
428
- }
429
- const initialFilterValues: Record<string, string> = {};
430
- for (const filter of initialURLFilters) {
431
- if (
432
- 'field' in filter
433
- && filter.operator === 'contains'
434
- && typeof filter.value === 'string'
435
- && editableFilterCounts[filter.field] === 1
436
- ) {
437
- initialFilterValues[filter.field] = filter.value;
438
- }
439
- }
440
- let filters = $state<Filter[]>(initialURLFilters);
441
- let searchText = $state(urlState.search ?? initialViewState?.search ?? '');
442
- let appliedSearchText = $state(untrack(() => searchText));
443
- let searchDebounceTimer: ReturnType<typeof setTimeout> | undefined;
280
+ const listState = createListState({
281
+ fields: () => resource.fields,
282
+ defaultPageSize: () => initPageSize,
283
+ defaultSort: () => resource.defaultSort,
284
+ externalPagination: () => externalPagination,
285
+ externalSorters: () => externalSorters,
286
+ initialURLState: urlState,
287
+ initialViewState,
288
+ syncWithLocation: () => syncWithLocation,
289
+ writeURLState: (state) => writeURLState(state, adminContext),
290
+ onDirty: markSavedViewDirty,
291
+ });
292
+ const pagination = $derived(listState.pagination);
293
+ const sorters = $derived(listState.sorters);
294
+ const searchText = $derived(listState.searchText);
295
+ const appliedSearchText = $derived(listState.appliedSearchText);
296
+ const searchableFields = $derived(listState.searchableFields);
297
+ const filterableFields = $derived(listState.filterableFields);
298
+ const filterValues = $derived(listState.filterValues);
299
+ const activeFilterCount = $derived(listState.activeFilterCount);
300
+ const activeFilterItems = $derived(listState.activeFilterItems);
301
+ const activeFilters = $derived(listState.activeFilters);
302
+ const queryPagination = $derived(listState.queryPagination);
303
+ const querySorters = $derived(listState.querySorters);
304
+ const queryFilters = $derived(listState.queryFilters);
305
+ const { clearFilters, setFilterValue, removeActiveFilter } = listState;
444
306
 
445
307
  function scheduleSearch(event: Event) {
446
308
  if (!(event.currentTarget instanceof HTMLInputElement)) return;
447
- markSavedViewDirty();
448
- searchText = event.currentTarget.value;
449
- if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
450
- searchDebounceTimer = setTimeout(() => {
451
- appliedSearchText = searchText;
452
- markSavedViewDirty();
453
- pagination = { ...pagination, current: 1 };
454
- searchDebounceTimer = undefined;
455
- }, 300);
456
- }
457
-
458
- onDestroy(() => {
459
- if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
460
- });
461
-
462
- // Sync external controlled state
463
- $effect(() => {
464
- if (externalPagination) {
465
- pagination = externalPagination;
466
- }
467
- });
468
-
469
- $effect(() => {
470
- if (externalSorters) {
471
- sorters = externalSorters;
472
- }
473
- });
474
-
475
- // ─── Build active filters with search ─────────────────────────
476
- const searchableFields = $derived(resource.fields.filter(f => f.searchable));
477
- const filterableFields = $derived(resource.fields.filter(f => f.filterable));
478
- let filterValues = $state<Record<string, string>>(initialFilterValues);
479
- const locationFilters = $derived<Filter[]>(filters);
480
- const activeFilterCount = $derived(locationFilters.length);
481
- const activeFilterItems = $derived.by(() => {
482
- const labels = new Map(resource.fields.map((field) => [field.key, field.label]));
483
- return locationFilters.map((filter, index) => {
484
- if ('field' in filter) {
485
- return { index, label: `${labels.get(filter.field) ?? filter.field}: ${String(filter.value)}` };
486
- }
487
- return { index, label: filter.operator };
488
- });
489
- });
490
- const activeFilters = $derived.by(() => {
491
- const result: Filter[] = [...locationFilters];
492
- if (appliedSearchText.trim() && searchableFields.length > 0) {
493
- const firstSearchableField = searchableFields[0];
494
- if (searchableFields.length === 1 && firstSearchableField) {
495
- result.push({ field: firstSearchableField.key, operator: 'contains', value: appliedSearchText });
496
- } else {
497
- const searchFilter: LogicalFilter = {
498
- operator: 'or',
499
- value: searchableFields.map(f => ({
500
- field: f.key,
501
- operator: 'contains',
502
- value: appliedSearchText
503
- }))
504
- };
505
- result.push(searchFilter);
506
- }
507
- }
508
- return result;
509
- });
510
- $effect(() => {
511
- if (!syncWithLocation) return;
512
- writeURLState({
513
- page: pagination.current,
514
- pageSize: pagination.pageSize,
515
- sortField: sorters[0]?.field,
516
- sortOrder: sorters[0]?.order,
517
- search: appliedSearchText || undefined,
518
- filters: locationFilters,
519
- }, adminContext);
520
- });
521
-
522
- function clearFilters(): void {
523
- markSavedViewDirty();
524
- if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
525
- searchDebounceTimer = undefined;
526
- searchText = '';
527
- appliedSearchText = '';
528
- filters = [];
529
- filterValues = {};
530
- pagination = { ...pagination, current: 1 };
531
- }
532
-
533
- function setFilterValue(field: string, value: string): void {
534
- markSavedViewDirty();
535
- const trimmedValue = value.trim();
536
- const nextFilter = { field, operator: 'contains' as const, value: trimmedValue };
537
- let replaced = false;
538
- filters = filters.flatMap((filter) => {
539
- if (!('field' in filter) || filter.field !== field || filter.operator !== 'contains') return [filter];
540
- if (!trimmedValue || replaced) return [];
541
- replaced = true;
542
- return [nextFilter];
543
- });
544
- if (trimmedValue && !replaced) filters = [...filters, nextFilter];
545
- filterValues[field] = value;
546
- pagination = { ...pagination, current: 1 };
309
+ listState.scheduleSearch(event.currentTarget.value);
547
310
  }
548
311
 
549
312
  function setFilterFromEvent(field: string, event: Event): void {
@@ -552,56 +315,10 @@
552
315
  }
553
316
  }
554
317
 
555
- function removeActiveFilter(index: number): void {
556
- markSavedViewDirty();
557
- const filter = locationFilters[index];
558
- if (!filter) return;
559
- const nextFilters = filters.filter((_, filterIndex) => filterIndex !== index);
560
- filters = nextFilters;
561
- if ('field' in filter && filter.operator === 'contains') {
562
- const remaining = nextFilters.filter((candidate) => (
563
- 'field' in candidate
564
- && candidate.field === filter.field
565
- && candidate.operator === 'contains'
566
- && typeof candidate.value === 'string'
567
- ));
568
- const remainingFilter = remaining[0];
569
- filterValues[filter.field] = remaining.length === 1 && remainingFilter && 'field' in remainingFilter
570
- ? String(remainingFilter.value)
571
- : '';
572
- }
573
- pagination = { ...pagination, current: 1 };
574
- }
575
-
576
318
  function clonePlainValue(value: unknown): unknown {
577
319
  return value === undefined ? undefined : snapshotPlainData(value);
578
320
  }
579
321
 
580
- function cloneFilter(filter: Filter): Filter {
581
- if ('field' in filter) {
582
- return {
583
- field: filter.field,
584
- operator: filter.operator,
585
- value: clonePlainValue(filter.value),
586
- };
587
- }
588
- return {
589
- operator: filter.operator,
590
- value: filter.value.map(cloneFilter),
591
- };
592
- }
593
-
594
- const queryPagination = $derived<PaginationState>(definedOptions({
595
- current: pagination.current,
596
- pageSize: pagination.pageSize,
597
- mode: pagination.mode,
598
- }));
599
- const querySorters = $derived<Sort[]>(sorters.map(sorter => ({
600
- field: sorter.field,
601
- order: sorter.order,
602
- })));
603
- const queryFilters = $derived<Filter[]>(activeFilters.map(cloneFilter));
604
-
605
322
  // ─── Data fetching ────────────────────────────────────────────
606
323
  const listResult = useList({
607
324
  get resource() { void activeRendering; return binding.resource; },
@@ -631,7 +348,46 @@
631
348
  }
632
349
  },
633
350
  };
634
- let deleteRequest = $state<{ ids: (string | number)[]; batch: boolean } | null>(null);
351
+ const deletionModel = createListDeletion({
352
+ canRequestSingle: () => canRead && pageRecords.ok && !query.isError && canDelete,
353
+ canRequestBatch: () => canRead && pageRecords.ok && !query.isError && canBatchDelete,
354
+ canConfirm: () => pageRecords.ok && !query.isError && deleteAllowed,
355
+ permission: () => deletePermission,
356
+ selection: () => selectionModel,
357
+ clearSelection: () => {
358
+ selectionModel.clearExplicit();
359
+ table_resetRowSelection(tbl, true);
360
+ },
361
+ captureScope: () => {
362
+ const scope = tableScope;
363
+ return () => tableScope.contract === scope.contract
364
+ && tableScope.resourceName === scope.resourceName
365
+ && tableScope.provider === scope.provider
366
+ && tableScope.meta === scope.meta
367
+ && tableScope.tenant === scope.tenant
368
+ && tableScope.auth === scope.auth
369
+ && tableScope.router === scope.router
370
+ && tableScope.permissionProvider === scope.permissionProvider;
371
+ },
372
+ mutate: (ids) => deleteManyMutation.mutateAsync({
373
+ ids,
374
+ ...definedOptions({ variables: deleteVariables, dataProviderName: binding.dataProviderName }),
375
+ }),
376
+ reportError: (message) => { operationError = message; },
377
+ messages: {
378
+ single: () => i18n.t('common.deleteConfirm'),
379
+ batch: (count) => i18n.t('common.batchDeleteConfirm', { count }),
380
+ failure: () => i18n.t('common.operationFailed'),
381
+ partial: (failed, total) => i18n.t('common.batchDeletePartialFail', { failed, total }),
382
+ },
383
+ });
384
+ const deleteRequest = $derived(deletionModel.request);
385
+ const confirmOpen = $derived(deletionModel.open);
386
+ const confirmPending = $derived(deletionModel.pending);
387
+ const confirmMessage = $derived(deletionModel.message);
388
+ const {
389
+ requestDelete: confirmDelete, requestBatchDelete: confirmBatchDelete, confirm: confirmAction,
390
+ } = deletionModel;
635
391
  const deletePermission = useCan(() => definedOptions({
636
392
  resource: resourceName, action: 'delete',
637
393
  id: deleteRequest?.batch === false ? deleteRequest.ids[0] : undefined,
@@ -666,19 +422,34 @@
666
422
  const initialSorting = untrack<SortingState>(() =>
667
423
  sorters.map(s => ({ id: s.field, desc: s.order === 'desc' }))
668
424
  );
669
- const initialColumnVisibility = untrack(() => (
670
- initialViewState?.columnVisibility ?? readColumnVisibilityPreference(listPreferenceScope)
671
- ));
672
425
  const sortingAtom = createAtom(initialSorting);
673
- const columnVisibilityAtom = createAtom(initialColumnVisibility);
674
- let rowSelection = $state<RowSelectionState>({});
426
+ const columnPreferences = createListColumnPreferences({
427
+ scope: () => listPreferenceScope,
428
+ columns: () => savedViewColumnIds,
429
+ fields: () => resource.fields,
430
+ isScopeLoaded: preferenceScopeIsLoaded,
431
+ readPreference: readScopedPreference,
432
+ initialViewState,
433
+ onDirty: markSavedViewDirty,
434
+ });
435
+ const {
436
+ visibility: tableColumnVisibility, order: tableColumnOrder,
437
+ setVisibility: setColumnVisibility, setOrder: setColumnOrder,
438
+ } = columnPreferences;
439
+ const selectionModel = createListSelection({
440
+ filters: () => queryFilters,
441
+ sorters: () => querySorters,
442
+ total: () => query.data?.total ?? 0,
443
+ selectable: () => selectable,
444
+ allowAllMatching: () => allowSelectAllMatching && !!batchActions,
445
+ onCriteriaChange: deletionModel.cancelBatchOnCriteriaChange,
446
+ });
447
+ const rowSelection = $derived(selectionModel.rowSelection);
448
+ const allMatchingSelected = $derived(selectionModel.allMatchingSelected);
449
+ const selectedIds = $derived(selectionModel.selectedIds);
450
+ const selectedCount = $derived(selectionModel.selectedCount);
451
+ const { rowIsSelected } = selectionModel;
675
452
  const expandedAtom = createAtom<ExpandedState>({});
676
- const initialColumnOrder = untrack(() => (
677
- initialViewState?.columnOrder?.length
678
- ? initialViewState.columnOrder
679
- : readColumnOrderPreference(listPreferenceScope)
680
- ));
681
- const columnOrderAtom = createAtom(initialColumnOrder);
682
453
  const features = tableFeatures({
683
454
  columnOrderingFeature,
684
455
  columnSizingFeature,
@@ -689,10 +460,7 @@
689
460
  });
690
461
 
691
462
  const tableSorting = useSelector(sortingAtom);
692
- const tableColumnVisibility = useSelector(columnVisibilityAtom);
693
463
  const tableExpanded = useSelector(expandedAtom);
694
- const tableColumnOrder = useSelector(columnOrderAtom);
695
- const selectedIdValueByKey = new SvelteMap<string, string | number>();
696
464
 
697
465
  function preferenceScopeIsLoaded(): boolean {
698
466
  return preferenceIdentityReady && loadedPreferenceScopeId === listPreferenceScopeId(listPreferenceScope);
@@ -703,10 +471,6 @@
703
471
  return expanded === true || expanded[rowId] === true;
704
472
  }
705
473
 
706
- function rowIsSelected(rowId: string): boolean {
707
- return allMatchingSelected ? !excludedMatchingIds.has(rowId) : rowSelection[rowId] === true;
708
- }
709
-
710
474
  function rowIdValue(row: Row<TableFeatures, TableRecord>): string | number {
711
475
  return row.original.id;
712
476
  }
@@ -714,16 +478,11 @@
714
478
  function toggleRowSelection(row: Row<TableFeatures, TableRecord>): void {
715
479
  if (confirmPending || !canRead) return;
716
480
  if (allMatchingSelected) {
717
- if (excludedMatchingIds.has(row.id)) excludedMatchingIds.delete(row.id);
718
- else excludedMatchingIds.set(row.id, rowIdValue(row));
481
+ selectionModel.toggleExcluded(row.id, rowIdValue(row));
719
482
  return;
720
483
  }
721
484
  const wasSelected = rowIsSelected(row.id);
722
- if (!wasSelected) {
723
- selectedIdValueByKey.set(row.id, rowIdValue(row));
724
- } else if (wasSelected) {
725
- selectedIdValueByKey.delete(row.id);
726
- }
485
+ selectionModel.trackRow(row.id, rowIdValue(row), !wasSelected);
727
486
  row_toggleSelected(row);
728
487
  }
729
488
 
@@ -734,121 +493,28 @@
734
493
  }));
735
494
 
736
495
  if (JSON.stringify(nextSorters) !== JSON.stringify(sorters)) {
737
- sorters = nextSorters;
738
- }
739
- });
740
-
741
- // Persist column visibility to localStorage
742
- $effect(() => {
743
- if (!preferenceScopeIsLoaded()) return;
744
- const storageKey = columnVisibilityStorageKey(listPreferenceScope);
745
- if (typeof window !== 'undefined') {
746
- try {
747
- localStorage.setItem(storageKey, JSON.stringify(tableColumnVisibility.current));
748
- } catch { /* ignore quota errors */ }
496
+ listState.sorters = nextSorters;
749
497
  }
750
498
  });
751
499
 
752
- $effect(() => {
753
- if (!preferenceScopeIsLoaded()) return;
754
- const ids = tableColumnOrder.current;
755
- persistColumnOrder(ids);
756
- });
757
-
758
- function persistSavedViews(): void {
759
- if (typeof window === 'undefined' || !preferenceScopeIsLoaded()) return;
760
- try {
761
- localStorage.setItem(savedListViewsStorageKey(listPreferenceScope), serializeSavedListViews(savedViews));
762
- } catch { /* ignore quota errors */ }
763
- }
764
-
765
- function persistActiveSavedView(): void {
766
- if (typeof window === 'undefined' || !preferenceScopeIsLoaded()) return;
767
- try {
768
- const key = activeSavedListViewStorageKey(listPreferenceScope);
769
- if (activeSavedViewId) localStorage.setItem(key, activeSavedViewId);
770
- else localStorage.removeItem(key);
771
- } catch { /* ignore quota errors */ }
772
- }
773
-
774
- function markSavedViewDirty(): void {
775
- remoteDefaultEligible = false;
776
- if (!activeSavedViewId) return;
777
- activeSavedViewId = undefined;
778
- persistActiveSavedView();
779
- }
780
-
781
500
  function getCurrentSavedViewState(): SavedListViewState {
782
501
  return {
783
- search: appliedSearchText,
784
- filters: locationFilters.map(cloneFilter),
785
- sorters: sorters.map((sorter) => ({ ...sorter })),
786
- pagination: {
787
- current: pagination.current ?? 1,
788
- pageSize: pagination.pageSize ?? initPageSize,
789
- },
790
- columnVisibility: { ...tableColumnVisibility.current },
791
- columnOrder: [...tableColumnOrder.current],
502
+ ...listState.captureState(),
503
+ ...columnPreferences.captureState(),
792
504
  };
793
505
  }
794
506
 
795
- function applySavedView(view: SavedListView): void {
796
- remoteDefaultEligible = false;
797
- if (searchDebounceTimer) {
798
- clearTimeout(searchDebounceTimer);
799
- searchDebounceTimer = undefined;
800
- }
801
- const state = cloneSavedListViewState(view.state);
802
- activeSavedViewId = view.id;
803
- persistActiveSavedView();
804
- searchText = state.search;
805
- appliedSearchText = state.search;
806
- filters = state.filters;
807
- const restoredFilterCounts: Record<string, number> = {};
808
- for (const filter of state.filters) {
809
- if ('field' in filter && filter.operator === 'contains' && typeof filter.value === 'string' && editableFilterKeys.has(filter.field)) {
810
- restoredFilterCounts[filter.field] = (restoredFilterCounts[filter.field] ?? 0) + 1;
811
- }
812
- }
813
- filterValues = {};
814
- for (const filter of state.filters) {
815
- if (
816
- 'field' in filter
817
- && filter.operator === 'contains'
818
- && typeof filter.value === 'string'
819
- && restoredFilterCounts[filter.field] === 1
820
- ) {
821
- filterValues[filter.field] = filter.value;
822
- }
823
- }
824
- pagination = { ...pagination, ...state.pagination };
825
- sorters = state.sorters;
507
+ function applySavedViewState(state: SavedListViewState): void {
508
+ listState.applyState(state);
826
509
  sortingAtom.set(state.sorters.map((sorter) => ({ id: sorter.field, desc: sorter.order === 'desc' })));
827
- columnVisibilityAtom.set(state.columnVisibility);
828
- columnOrderAtom.set(state.columnOrder);
829
- persistColumnOrder(state.columnOrder);
830
- rowSelection = {};
831
- allMatchingSelected = false;
832
- excludedMatchingIds.clear();
833
- selectedIdValueByKey.clear();
510
+ columnPreferences.applyState(state);
511
+ selectionModel.reset();
834
512
  }
835
513
 
836
514
  function resetToDefaultListState(): void {
837
- if (searchDebounceTimer) {
838
- clearTimeout(searchDebounceTimer);
839
- searchDebounceTimer = undefined;
840
- }
841
- searchText = '';
842
- appliedSearchText = '';
843
- filters = [];
844
- filterValues = {};
845
- if (!externalPagination) {
846
- pagination = { current: 1, pageSize: initPageSize };
847
- }
515
+ listState.resetToDefaults();
848
516
  if (!externalSorters) {
849
- const defaultSorters = initDefaultSort ? [{ ...initDefaultSort }] : [];
850
- sorters = defaultSorters;
851
- sortingAtom.set(defaultSorters.map((sorter) => ({ id: sorter.field, desc: sorter.order === 'desc' })));
517
+ sortingAtom.set(listState.sorters.map((sorter) => ({ id: sorter.field, desc: sorter.order === 'desc' })));
852
518
  }
853
519
  }
854
520
 
@@ -857,310 +523,20 @@
857
523
  const scopeId = listPreferenceScopeId(scope);
858
524
  if (scopeId === loadedPreferenceScopeId) return;
859
525
 
860
- remoteDefaultEligible = true;
861
- const preferences = readScopedSavedViewPreferences(scope);
862
- savedViewName = '';
863
- savedViewsOpen = false;
864
- savedViews = preferences.savedViews;
865
- remoteSavedViews = [];
866
- activeSavedViewId = preferences.activeSavedViewId;
526
+ const activeSavedView = savedViewModel.restoreScope(scope);
527
+ const restoredColumns = columnPreferences.readScope(scope);
867
528
 
868
- if (!hasExplicitURLState && preferences.activeSavedView) {
869
- applySavedView(preferences.activeSavedView);
529
+ if (!hasExplicitURLState && activeSavedView) {
530
+ applySavedView(activeSavedView);
870
531
  } else {
871
532
  if (!hasExplicitURLState) resetToDefaultListState();
872
- columnVisibilityAtom.set(preferences.columnVisibility);
873
- columnOrderAtom.set(preferences.columnOrder);
874
- rowSelection = {};
875
- allMatchingSelected = false;
876
- excludedMatchingIds.clear();
877
- selectedIdValueByKey.clear();
533
+ columnPreferences.applyState(restoredColumns);
534
+ selectionModel.reset();
878
535
  }
879
536
 
880
537
  loadedPreferenceScopeId = scopeId;
881
538
  });
882
539
 
883
- let remoteViewEpoch = 0;
884
- $effect(() => {
885
- const provider = savedViewProvider;
886
- const scope = listPreferenceScope;
887
- const columns = savedViewColumnIds;
888
- void remoteViewsReload;
889
- remoteSavedViews = [];
890
- remoteViewsLoading = false;
891
- remoteViewsFailed = false;
892
- accessDrafts.clear();
893
- accessSubjects.clear();
894
- accessSubjectsLoading.clear();
895
- accessSubjectsFailed.clear();
896
- accessSubjectQueries.clear();
897
- accessSubjectRequests.clear();
898
- savedViewMutationEpoch += 1;
899
- savedViewMutationPending = false;
900
- savedViewMutationError = undefined;
901
- savedViewSource = 'local';
902
- if (!provider || !preferenceScopeIsLoaded()) {
903
- remoteSavedViews = [];
904
- return;
905
- }
906
- const epoch = ++remoteViewEpoch;
907
- remoteViewsLoading = true;
908
- void Promise.resolve().then(() => provider.list({ ...scope })).then(value => {
909
- if (epoch !== remoteViewEpoch || !preferenceScopeIsLoaded()) return;
910
- remoteSavedViews = decodeRemoteSavedListViews(value, columns, !!(provider.save || provider.remove || provider.setDefault || provider.updateAccess));
911
- if (applyRemoteDefaultView && remoteDefaultEligible) {
912
- const defaults = remoteSavedViews.filter(view => view.default === true);
913
- if (!hasExplicitURLState && !activeSavedViewId && !externalPagination && !externalSorters
914
- && defaults.length === 1 && defaults[0]) {
915
- applySavedView(defaults[0]);
916
- remoteDefaultEligible = false;
917
- }
918
- }
919
- }).catch(() => {
920
- if (epoch === remoteViewEpoch) {
921
- remoteSavedViews = [];
922
- remoteViewsFailed = true;
923
- }
924
- }).finally(() => {
925
- if (epoch === remoteViewEpoch) remoteViewsLoading = false;
926
- });
927
- return () => {
928
- remoteViewEpoch += 1;
929
- savedViewMutationEpoch += 1;
930
- accessSubjectRequests.clear();
931
- };
932
- });
933
-
934
- function saveCurrentView(): void {
935
- if (!preferenceScopeIsLoaded() || savedViewMutationPending) return;
936
- remoteDefaultEligible = false;
937
- const name = savedViewName.trim().slice(0, 60);
938
- if (!name) return;
939
- if (searchDebounceTimer) {
940
- clearTimeout(searchDebounceTimer);
941
- searchDebounceTimer = undefined;
942
- appliedSearchText = searchText;
943
- }
944
- if (savedViewSource !== 'local') {
945
- const existing = remoteSavedViews.find(view => view.source === savedViewSource
946
- && view.name.toLocaleLowerCase() === name.toLocaleLowerCase());
947
- if (!savedViewProvider?.save || (existing && (existing.readOnly || !existing.version))) return;
948
- void mutateRemoteView({
949
- id: existing?.id ?? crypto.randomUUID(), name, state: getCurrentSavedViewState(),
950
- source: savedViewSource, expectedVersion: existing?.version ?? null,
951
- });
952
- return;
953
- }
954
- const existing = savedViews.find((view) => view.name.toLocaleLowerCase() === name.toLocaleLowerCase());
955
- const view: SavedListView = {
956
- id: existing?.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
957
- name,
958
- state: getCurrentSavedViewState(),
959
- };
960
- savedViews = existing
961
- ? savedViews.map((candidate) => candidate.id === existing.id ? view : candidate)
962
- : [view, ...savedViews].slice(0, 25);
963
- activeSavedViewId = view.id;
964
- savedViewName = '';
965
- persistSavedViews();
966
- persistActiveSavedView();
967
- }
968
-
969
- function deleteSavedView(id: string): void {
970
- if (!preferenceScopeIsLoaded() || savedViewMutationPending) return;
971
- const remote = remoteSavedViews.find(view => view.id === id);
972
- if (remote) {
973
- if (!savedViewProvider?.remove || remote.readOnly || !remote.version) return;
974
- void mutateRemoteView({
975
- id, expectedVersion: remote.version, source: remote.source === 'system' ? 'system' : 'team',
976
- name: remote.name, state: remote.state,
977
- }, true);
978
- return;
979
- }
980
- savedViews = savedViews.filter((view) => view.id !== id);
981
- if (activeSavedViewId === id) activeSavedViewId = undefined;
982
- persistSavedViews();
983
- persistActiveSavedView();
984
- }
985
-
986
- async function mutateRemoteView(
987
- mutation: import('./saved-list-views').SavedListViewMutation, remove = false,
988
- ): Promise<void> {
989
- const provider = savedViewProvider;
990
- if (!provider || savedViewMutationPending || !preferenceScopeIsLoaded()
991
- || (remove && (mutation.expectedVersion === null || !provider.remove))
992
- || (!remove && !provider.save)) return;
993
- const scope = { ...listPreferenceScope };
994
- const scopeId = listPreferenceScopeId(scope);
995
- const columns = savedViewColumnIds;
996
- const epoch = ++savedViewMutationEpoch;
997
- const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
998
- && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
999
- savedViewMutationPending = true;
1000
- savedViewMutationError = undefined;
1001
- try {
1002
- const result = remove
1003
- ? await provider.remove?.(scope, { id: mutation.id, expectedVersion: mutation.expectedVersion ?? 0 })
1004
- : await provider.save?.(scope, { ...mutation, state: cloneSavedListViewState(mutation.state) });
1005
- if (!current()) return;
1006
- if (remove && mutation.expectedVersion !== null && decodeSavedListViewRemoveResult(result, {
1007
- id: mutation.id, expectedVersion: mutation.expectedVersion,
1008
- })) {
1009
- remoteSavedViews = remoteSavedViews.filter(view => view.id !== mutation.id);
1010
- if (activeSavedViewId === mutation.id) activeSavedViewId = undefined;
1011
- persistActiveSavedView();
1012
- return;
1013
- }
1014
- const decoded = decodeSavedListViewMutationResult(result, columns, mutation);
1015
- if (!decoded || (remove && decoded.ok)) {
1016
- savedViewMutationError = 'failure';
1017
- return;
1018
- }
1019
- if (!decoded.ok) {
1020
- savedViewMutationError = 'conflict';
1021
- // 保留本地查询;用户必须重新读取后再决定如何修改。
1022
- return;
1023
- }
1024
- const view = { ...decoded.view, readOnly: decoded.view.readOnly !== false };
1025
- remoteSavedViews = [view, ...remoteSavedViews.filter(candidate => candidate.id !== view.id)];
1026
- savedViewName = '';
1027
- } catch {
1028
- if (current()) savedViewMutationError = 'failure';
1029
- } finally {
1030
- if (current()) savedViewMutationPending = false;
1031
- }
1032
- }
1033
-
1034
- function setRemoteDefault(view: SavedListView): void {
1035
- const provider = savedViewProvider;
1036
- if (!provider?.setDefault || savedViewMutationPending || view.readOnly || !view.version
1037
- || savedViewMutationError === 'conflict' || !preferenceScopeIsLoaded()
1038
- || (view.source !== 'team' && view.source !== 'system')) return;
1039
- const source = view.source;
1040
- const expectedVersion = view.version;
1041
- const defaultValue = view.default !== true;
1042
- remoteDefaultEligible = false;
1043
- const scope = { ...listPreferenceScope };
1044
- const scopeId = listPreferenceScopeId(scope);
1045
- const epoch = ++savedViewMutationEpoch;
1046
- const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
1047
- && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
1048
- savedViewMutationPending = true;
1049
- savedViewMutationError = undefined;
1050
- void Promise.resolve().then(() => provider.setDefault?.(scope, {
1051
- id: view.id, source, expectedVersion, default: defaultValue,
1052
- })).then(result => {
1053
- if (!current()) return;
1054
- const decoded = decodeSavedListViewMutationResult(result, savedViewColumnIds, {
1055
- id: view.id, source, expectedVersion,
1056
- });
1057
- if (!decoded) {
1058
- savedViewMutationError = 'failure';
1059
- } else if (!decoded.ok) {
1060
- savedViewMutationError = 'conflict';
1061
- } else if (decoded.view.default !== defaultValue) {
1062
- savedViewMutationError = 'failure';
1063
- } else {
1064
- // 默认标记会同时改变同一作用域的其他视图,成功后重新读取完整集合。
1065
- remoteViewsReload += 1;
1066
- }
1067
- }).catch(() => {
1068
- if (current()) savedViewMutationError = 'failure';
1069
- }).finally(() => {
1070
- if (current()) savedViewMutationPending = false;
1071
- });
1072
- }
1073
-
1074
- function saveRemoteAccess(view: SavedListView): void {
1075
- const provider = savedViewProvider;
1076
- const draft = accessDrafts.get(view.id);
1077
- if (!provider?.updateAccess || !draft || view.readOnly !== false || !view.version
1078
- || savedViewMutationPending || savedViewMutationError === 'conflict' || !preferenceScopeIsLoaded()
1079
- || (view.source !== 'team' && view.source !== 'system')) return;
1080
- const access = decodeSavedListViewAccess({
1081
- mode: draft.mode,
1082
- subjectIds: draft.mode === 'restricted' ? draft.subjectIds : [],
1083
- });
1084
- if (!access) { savedViewMutationError = 'failure'; return; }
1085
- const source = view.source;
1086
- const expectedVersion = view.version;
1087
- const scope = { ...listPreferenceScope };
1088
- const scopeId = listPreferenceScopeId(scope);
1089
- const epoch = ++savedViewMutationEpoch;
1090
- const current = () => epoch === savedViewMutationEpoch && provider === savedViewProvider
1091
- && scopeId === listPreferenceScopeId(listPreferenceScope) && preferenceScopeIsLoaded();
1092
- remoteDefaultEligible = false;
1093
- savedViewMutationPending = true;
1094
- savedViewMutationError = undefined;
1095
- void Promise.resolve().then(() => {
1096
- if (!current()) return;
1097
- return provider.updateAccess?.(scope, {
1098
- id: view.id, source, expectedVersion, access: { ...access, subjectIds: [...access.subjectIds] },
1099
- });
1100
- }).then(result => {
1101
- if (!current()) return;
1102
- const decoded = decodeSavedListViewMutationResult(result, savedViewColumnIds, {
1103
- id: view.id, source, expectedVersion,
1104
- });
1105
- if (!decoded) savedViewMutationError = 'failure';
1106
- else if (!decoded.ok) savedViewMutationError = 'conflict';
1107
- else if (decoded.view.access?.mode !== access.mode
1108
- || decoded.view.access.subjectIds.length !== access.subjectIds.length
1109
- || !decoded.view.access.subjectIds.every(id => access.subjectIds.includes(id))) {
1110
- savedViewMutationError = 'failure';
1111
- } else remoteViewsReload += 1;
1112
- }).catch(() => {
1113
- if (current()) savedViewMutationError = 'failure';
1114
- }).finally(() => {
1115
- if (current()) savedViewMutationPending = false;
1116
- });
1117
- }
1118
-
1119
- function loadAccessSubjects(view: SavedListView, query = ''): void {
1120
- const provider = savedViewProvider;
1121
- if (!provider?.listAccessSubjects || view.readOnly !== false
1122
- || view.source === 'local' || !preferenceScopeIsLoaded()) return;
1123
- const scope = { ...listPreferenceScope };
1124
- const scopeId = listPreferenceScopeId(scope);
1125
- const request = {};
1126
- accessSubjectRequests.set(view.id, request);
1127
- const current = () => accessSubjectRequests.get(view.id) === request
1128
- && scopeId === listPreferenceScopeId(listPreferenceScope)
1129
- && provider === savedViewProvider && preferenceScopeIsLoaded();
1130
- accessSubjectsLoading.set(view.id, true);
1131
- accessSubjectsFailed.delete(view.id);
1132
- accessSubjects.delete(view.id);
1133
- void Promise.resolve().then(() => {
1134
- if (current()) return provider.listAccessSubjects?.(scope, { query: query.trim(), limit: 50 });
1135
- })
1136
- .then(result => {
1137
- if (!current()) return;
1138
- const subjects = decodeSavedListViewSubjects(result);
1139
- if (!subjects) accessSubjectsFailed.set(view.id, true);
1140
- else accessSubjects.set(view.id, subjects);
1141
- })
1142
- .catch(() => {
1143
- if (current()) accessSubjectsFailed.set(view.id, true);
1144
- })
1145
- .finally(() => { if (current()) accessSubjectsLoading.delete(view.id); });
1146
- }
1147
-
1148
- function setColumnVisibility(columnId: string, visible: boolean): void {
1149
- markSavedViewDirty();
1150
- columnVisibilityAtom.set({ ...tableColumnVisibility.current, [columnId]: visible });
1151
- }
1152
-
1153
- function persistColumnOrder(ids: string[]): void {
1154
- if (typeof window === 'undefined' || !preferenceScopeIsLoaded()) return;
1155
- try { localStorage.setItem(columnOrderStorageKey(listPreferenceScope), JSON.stringify(ids)); } catch { /* ignore */ }
1156
- }
1157
-
1158
- function setColumnOrder(newOrder: Array<{ id: string }>): void {
1159
- markSavedViewDirty();
1160
- const ids = newOrder.map((column) => column.id);
1161
- columnOrderAtom.set(ids);
1162
- }
1163
-
1164
540
  function toggleColumnSort(column: Column<TableFeatures, TableRecord, unknown>): void {
1165
541
  markSavedViewDirty();
1166
542
  column_toggleSorting(column);
@@ -1170,7 +546,7 @@
1170
546
  $effect(() => {
1171
547
  if (!externalSorters) return;
1172
548
  if (JSON.stringify(externalSorters) !== JSON.stringify(sorters)) {
1173
- sorters = externalSorters;
549
+ listState.sorters = externalSorters;
1174
550
  }
1175
551
  const nextSorting = externalSorters.map(s => ({ id: s.field, desc: s.order === 'desc' }));
1176
552
  if (JSON.stringify(nextSorting) !== JSON.stringify(tableSorting.current)) {
@@ -1248,7 +624,7 @@
1248
624
  sortingAtom.set(typeof updater === 'function' ? updater(tableSorting.current) : updater);
1249
625
  },
1250
626
  onRowSelectionChange: (updater) => {
1251
- rowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
627
+ selectionModel.rowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
1252
628
  },
1253
629
  atoms: {
1254
630
  expanded: expandedAtom,
@@ -1268,26 +644,12 @@
1268
644
  }
1269
645
  const willSelect = !table_getIsAllRowsSelected(tbl);
1270
646
  for (const row of tableView.rows) {
1271
- if (willSelect) {
1272
- selectedIdValueByKey.set(row.id, rowIdValue(row));
1273
- } else if (!willSelect) {
1274
- selectedIdValueByKey.delete(row.id);
1275
- }
647
+ selectionModel.trackRow(row.id, rowIdValue(row), willSelect);
1276
648
  }
1277
649
  table_toggleAllRowsSelected(tbl);
1278
650
  }
1279
651
 
1280
- const selectedIds = $derived(
1281
- Object.keys(rowSelection).filter(key => rowSelection[key] === true)
1282
- .flatMap(key => {
1283
- const id = selectedIdValueByKey.get(key);
1284
- return id === undefined ? [] : [id];
1285
- })
1286
- );
1287
652
  const matchingTotal = $derived(query.data?.total ?? 0);
1288
- const selectedCount = $derived(allMatchingSelected
1289
- ? Math.max(0, matchingTotal - excludedMatchingIds.size)
1290
- : selectedIds.length);
1291
653
  const selectionLabel = $derived(i18n.t(
1292
654
  allMatchingSelected ? 'common.allMatchingSelected' : 'common.selectedCount',
1293
655
  { count: selectedCount },
@@ -1300,42 +662,8 @@
1300
662
  && Number.isSafeInteger(matchingTotal) && matchingTotal > 0
1301
663
  );
1302
664
  function batchSelectionSnapshot(): BatchSelection {
1303
- if (allMatchingSelected) return {
1304
- scope: 'all', filters: queryFilters.map(cloneFilter),
1305
- sorters: querySorters.map(sorter => ({ ...sorter })), total: matchingTotal,
1306
- excludedIds: [...excludedMatchingIds.values()],
1307
- };
1308
- return {
1309
- scope: 'selected', ids: [...selectedIds],
1310
- currentPageIds: tableView.rows.filter(row => rowSelection[row.id] === true).map(rowIdValue),
1311
- };
665
+ return selectionModel.snapshot(tableView.rows.map(row => ({ key: row.id, id: rowIdValue(row) })));
1312
666
  }
1313
- const selectionCriteria = $derived(JSON.stringify(queryFilters));
1314
- let previousSelectionCriteria = untrack(() => selectionCriteria);
1315
- $effect.pre(() => {
1316
- const criteria = selectionCriteria;
1317
- if (criteria !== previousSelectionCriteria) {
1318
- allMatchingSelected = false;
1319
- excludedMatchingIds.clear();
1320
- rowSelection = {};
1321
- selectedIdValueByKey.clear();
1322
- if (deleteRequest?.batch && !confirmPending) {
1323
- confirmOpen = false;
1324
- deleteRequest = null;
1325
- }
1326
- }
1327
- previousSelectionCriteria = criteria;
1328
- });
1329
- $effect.pre(() => {
1330
- if (!selectable || !allowSelectAllMatching || !batchActions) {
1331
- allMatchingSelected = false;
1332
- excludedMatchingIds.clear();
1333
- }
1334
- if (!selectable) {
1335
- rowSelection = {};
1336
- selectedIdValueByKey.clear();
1337
- }
1338
- });
1339
667
  const batchDeletePerm = useCan(() => ({
1340
668
  resource: resourceName,
1341
669
  action: 'delete',
@@ -1348,19 +676,14 @@
1348
676
 
1349
677
  function clearSelection(): void {
1350
678
  if (!deleteManyMutation.isPending) {
1351
- allMatchingSelected = false;
1352
- excludedMatchingIds.clear();
1353
- selectedIdValueByKey.clear();
679
+ selectionModel.reset();
1354
680
  table_resetRowSelection(tbl, true);
1355
681
  }
1356
682
  }
1357
683
 
1358
684
  function selectAllMatching(): void {
1359
685
  if (!canSelectAllMatching || confirmPending || confirmOpen) return;
1360
- selectedIdValueByKey.clear();
1361
- excludedMatchingIds.clear();
1362
- rowSelection = {};
1363
- allMatchingSelected = true;
686
+ selectionModel.selectAllMatching();
1364
687
  }
1365
688
 
1366
689
  function isColumnVisible(columnId: string): boolean {
@@ -1422,11 +745,7 @@
1422
745
  });
1423
746
 
1424
747
  // ─── Confirm dialog ───────────────────────────────────────────
1425
- let confirmOpen = $state(false);
1426
- let confirmMessage = $state('');
1427
- let confirmPending = $state(false);
1428
748
  let operationError = $state<string | null>(null);
1429
- let activeDelete: object | undefined;
1430
749
  let detailOpenedInHistory = $state(false);
1431
750
  let quickEditId = $state<string | number | undefined>();
1432
751
  const tableScope = $derived({
@@ -1463,111 +782,17 @@
1463
782
  $effect.pre(() => {
1464
783
  const scope = tableScope;
1465
784
  if (previousTableScope && !tableScopesEqual(previousTableScope, scope)) {
1466
- activeDelete = undefined;
1467
- confirmOpen = false;
1468
- confirmPending = false;
1469
- deleteRequest = null;
1470
- operationError = null;
1471
- allMatchingSelected = false;
1472
- excludedMatchingIds.clear();
1473
- selectedIdValueByKey.clear();
1474
- rowSelection = {};
785
+ deletionModel.reset();
786
+ selectionModel.reset();
1475
787
  expandedAtom.set({});
1476
788
  detailOpenedInHistory = false;
1477
789
  localDetailId = undefined;
1478
790
  quickEditId = undefined;
1479
- if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
1480
- searchDebounceTimer = undefined;
791
+ listState.cancelSearch();
1481
792
  if (previousTableScope.resourceName !== scope.resourceName) resetToDefaultListState();
1482
793
  }
1483
794
  previousTableScope = scope;
1484
795
  });
1485
- $effect(() => {
1486
- if (deleteRequest && !deletePermission.isLoading && !deletePermission.allowed) {
1487
- confirmOpen = false;
1488
- deleteRequest = null;
1489
- operationError = i18n.t('common.operationFailed');
1490
- }
1491
- });
1492
- onDestroy(() => { activeDelete = undefined; });
1493
-
1494
- function confirmDelete(id: string | number) {
1495
- if (!canRead || !pageRecords.ok || query.isError || !canDelete || confirmPending) return;
1496
- confirmMessage = i18n.t('common.deleteConfirm');
1497
- deleteRequest = { ids: [id], batch: false };
1498
- operationError = null;
1499
- confirmOpen = true;
1500
- }
1501
-
1502
- function confirmBatchDelete() {
1503
- const ids = [...selectedIds];
1504
- if (!canRead || !pageRecords.ok || query.isError || !canBatchDelete || ids.length === 0 || confirmPending) return;
1505
- confirmMessage = i18n.t('common.batchDeleteConfirm', { count: ids.length });
1506
- deleteRequest = { ids, batch: true };
1507
- operationError = null;
1508
- confirmOpen = true;
1509
- }
1510
-
1511
- async function confirmAction() {
1512
- if (!confirmOpen || !pageRecords.ok || query.isError || !deleteAllowed || confirmPending || !deleteRequest) return;
1513
- const request = deleteRequest;
1514
- if (request.batch && JSON.stringify(request.ids) !== JSON.stringify(selectedIds)) {
1515
- confirmOpen = false;
1516
- deleteRequest = null;
1517
- return;
1518
- }
1519
- const scope = tableScope;
1520
- const token = {};
1521
- activeDelete = token;
1522
- confirmPending = true;
1523
- confirmOpen = false;
1524
- const clearBatchSelectionBeforeMutation = request.batch && getAdminOptions().mutationMode === 'undoable';
1525
- const selectionBeforeDelete = clearBatchSelectionBeforeMutation ? new Map(selectedIdValueByKey) : undefined;
1526
- if (clearBatchSelectionBeforeMutation) {
1527
- selectedIdValueByKey.clear();
1528
- table_resetRowSelection(tbl, true);
1529
- }
1530
- const current = () => activeDelete === token &&
1531
- tableScope.contract === scope.contract &&
1532
- tableScope.resourceName === scope.resourceName &&
1533
- tableScope.provider === scope.provider &&
1534
- tableScope.meta === scope.meta &&
1535
- tableScope.tenant === scope.tenant &&
1536
- tableScope.auth === scope.auth &&
1537
- tableScope.router === scope.router &&
1538
- tableScope.permissionProvider === scope.permissionProvider;
1539
- try {
1540
- await deleteManyMutation.mutateAsync({
1541
- ids: [...request.ids],
1542
- ...definedOptions({ variables: deleteVariables, dataProviderName: binding.dataProviderName }),
1543
- });
1544
- if (!current()) return;
1545
- selectedIdValueByKey.clear();
1546
- table_resetRowSelection(tbl, true);
1547
- await tick();
1548
- confirmOpen = false;
1549
- } catch (error) {
1550
- if (!current()) return;
1551
- if (error instanceof DeleteManyPartialError) {
1552
- rowSelection = Object.fromEntries(error.failedIds.map(id => [tableRowKey(id), true as const]));
1553
- selectedIdValueByKey.clear();
1554
- for (const id of error.failedIds) selectedIdValueByKey.set(tableRowKey(id), id);
1555
- operationError = i18n.t('common.batchDeletePartialFail', { failed: error.failedIds.length, total: request.ids.length });
1556
- } else {
1557
- if (selectionBeforeDelete) {
1558
- selectedIdValueByKey.clear();
1559
- for (const [key, id] of selectionBeforeDelete) selectedIdValueByKey.set(key, id);
1560
- rowSelection = Object.fromEntries([...selectionBeforeDelete.keys()].map(key => [key, true as const]));
1561
- }
1562
- operationError = i18n.t('common.operationFailed');
1563
- }
1564
- confirmOpen = false;
1565
- } finally {
1566
- if (activeDelete === token) { activeDelete = undefined; confirmPending = false; deleteRequest = null; }
1567
- await tick();
1568
- await tick();
1569
- }
1570
- }
1571
796
  function openDetail(id: string | number): void {
1572
797
  if (!canRead || !canShow) return;
1573
798
  if (!syncWithLocation) { localDetailId = id; return; }
@@ -1601,7 +826,7 @@
1601
826
  function goToPage(page: number) {
1602
827
  if (!Number.isSafeInteger(page) || page < 1 || page > Math.max(1, totalPages)) return;
1603
828
  markSavedViewDirty();
1604
- pagination = { ...pagination, current: page };
829
+ listState.pagination = { ...pagination, current: page };
1605
830
  }
1606
831
  function refreshList() {
1607
832
  if (canRead && !query.isFetching) void listResult.refetch();
@@ -1620,9 +845,9 @@
1620
845
  if (!canRead || query.isFetching || deleteManyMutation.isPending) return;
1621
846
  markSavedViewDirty();
1622
847
  const accepted = next.filter(sort => gridColumns.some(column => column.key === sort.field && column.sortable));
1623
- sorters = accepted.map(sort => ({ ...sort }));
848
+ listState.sorters = accepted.map(sort => ({ ...sort }));
1624
849
  sortingAtom.set(accepted.map(sort => ({ id: sort.field, desc: sort.order === 'desc' })));
1625
- pagination = { ...pagination, current: 1 };
850
+ listState.pagination = { ...pagination, current: 1 };
1626
851
  }
1627
852
  </script>
1628
853
 
@@ -1979,7 +1204,7 @@
1979
1204
  </div>
1980
1205
  {/each}
1981
1206
  <div class="svadmin-u-60fbb7713999 svadmin-u-77a2a20e90d4 svadmin-u-f46b61a9b310">
1982
- <Button size="sm" class="svadmin-u-36e579c0b41c" onclick={() => { pagination = { ...pagination, current: 1 }; }}>
1207
+ <Button size="sm" class="svadmin-u-36e579c0b41c" onclick={() => { listState.pagination = { ...pagination, current: 1 }; }}>
1983
1208
  {i18n.t("common.confirm")}
1984
1209
  </Button>
1985
1210
  <Button variant="outline" size="sm" onclick={clearFilters}>
@@ -2055,11 +1280,11 @@
2055
1280
  </DropdownMenu.Root>
2056
1281
 
2057
1282
  <!-- Saved Views -->
2058
- <Popover.Root bind:open={savedViewsOpen}>
1283
+ <Popover.Root bind:open={savedViewModel.savedViewsOpen}>
2059
1284
  <Popover.Trigger>
2060
1285
  {#snippet child({ props })}
2061
1286
  <Button variant="outline" size="sm" class="svadmin-u-e7a768f922d2 svadmin-u-0b91436debbd" {...props} disabled={!preferenceIdentityReady} aria-label={i18n.t("common.savedViews")}>
2062
- <Bookmark class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {activeSavedViewName ?? i18n.t("common.savedViews")}
1287
+ <Bookmark class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {savedViewModel.activeSavedViewName ?? i18n.t("common.savedViews")}
2063
1288
  </Button>
2064
1289
  {/snippet}
2065
1290
  </Popover.Trigger>
@@ -2069,28 +1294,28 @@
2069
1294
  <h4 class="svadmin-u-2689f3958069 svadmin-u-fc7473ca09eb">{i18n.t("common.savedViews")}</h4>
2070
1295
  <p class="svadmin-u-b6b02c0ebef6 svadmin-u-359090c2d529 svadmin-u-bfa603190748">{i18n.t("common.savedViewsHint")}</p>
2071
1296
  </div>
2072
- {#if remoteViewsLoading}
1297
+ {#if savedViewModel.remoteViewsLoading}
2073
1298
  <p role="status">{i18n.t('common.loading')}</p>
2074
- {:else if remoteViewsFailed}
1299
+ {:else if savedViewModel.remoteViewsFailed}
2075
1300
  <p role="alert">{i18n.t('common.operationFailed')}</p>
2076
- <Button type="button" variant="outline" size="sm" onclick={() => remoteViewsReload += 1}>
1301
+ <Button type="button" variant="outline" size="sm" onclick={() => savedViewModel.reload()}>
2077
1302
  {i18n.t('common.retry')}
2078
1303
  </Button>
2079
1304
  {/if}
2080
- {#if savedViewMutationPending}
1305
+ {#if savedViewModel.savedViewMutationPending}
2081
1306
  <p role="status">{i18n.t('common.loading')}</p>
2082
- {:else if savedViewMutationError}
2083
- <p role="alert">{i18n.t(savedViewMutationError === 'conflict' ? 'common.viewConflict' : 'common.operationFailed')}</p>
2084
- <Button type="button" variant="outline" size="sm" onclick={() => remoteViewsReload += 1}>
1307
+ {:else if savedViewModel.savedViewMutationError}
1308
+ <p role="alert">{i18n.t(savedViewModel.savedViewMutationError === 'conflict' ? 'common.viewConflict' : 'common.operationFailed')}</p>
1309
+ <Button type="button" variant="outline" size="sm" onclick={() => savedViewModel.reload()}>
2085
1310
  {i18n.t('common.refresh')}
2086
1311
  </Button>
2087
1312
  {/if}
2088
1313
  {#if savedViewProvider?.save}
2089
- <Select.Root aria-label={i18n.t('common.viewSource')} value={savedViewSource} disabled={savedViewMutationPending}
1314
+ <Select.Root aria-label={i18n.t('common.viewSource')} value={savedViewModel.savedViewSource} disabled={savedViewModel.savedViewMutationPending}
2090
1315
  onchange={(event: Event) => {
2091
1316
  if (!(event.currentTarget instanceof HTMLSelectElement)) return;
2092
1317
  const value = event.currentTarget.value;
2093
- if (value === 'local' || value === 'team' || value === 'system') savedViewSource = value;
1318
+ if (value === 'local' || value === 'team' || value === 'system') savedViewModel.savedViewSource = value;
2094
1319
  }}>
2095
1320
  <option value="local">{i18n.t('common.personalView')}</option>
2096
1321
  <option value="team">{i18n.t('common.teamView')}</option>
@@ -2102,21 +1327,17 @@
2102
1327
  <Select.Root
2103
1328
  id="saved-list-view"
2104
1329
  class="svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741"
2105
- value={activeSavedViewId ?? ""}
1330
+ value={savedViewModel.activeSavedViewId ?? ""}
2106
1331
  onchange={(event: Event) => {
2107
1332
  if (!(event.currentTarget instanceof HTMLSelectElement)) return;
2108
1333
  const id = event.currentTarget.value;
2109
- const view = availableSavedViews.find((candidate) => candidate.id === id);
1334
+ const view = savedViewModel.availableSavedViews.find((candidate) => candidate.id === id);
2110
1335
  if (view) applySavedView(view);
2111
- else {
2112
- remoteDefaultEligible = false;
2113
- activeSavedViewId = undefined;
2114
- persistActiveSavedView();
2115
- }
1336
+ else savedViewModel.clearActiveSavedView();
2116
1337
  }}
2117
1338
  >
2118
1339
  <option value="">{i18n.t("common.currentView")}</option>
2119
- {#each availableSavedViews as view (view.id)}
1340
+ {#each savedViewModel.availableSavedViews as view (view.id)}
2120
1341
  <option value={view.id}>{view.name}</option>
2121
1342
  {/each}
2122
1343
  </Select.Root>
@@ -2126,17 +1347,17 @@
2126
1347
  aria-label={i18n.t("common.viewName")}
2127
1348
  placeholder={i18n.t("common.viewName")}
2128
1349
  maxlength={60}
2129
- bind:value={savedViewName}
2130
- disabled={savedViewMutationPending}
1350
+ bind:value={savedViewModel.savedViewName}
1351
+ disabled={savedViewModel.savedViewMutationPending}
2131
1352
  class="svadmin-u-e7a768f922d2"
2132
1353
  />
2133
- <Button size="sm" class="svadmin-u-012fbd121f37" disabled={!savedViewName.trim() || savedViewMutationPending || savedViewMutationError === 'conflict'} onclick={saveCurrentView}>
1354
+ <Button size="sm" class="svadmin-u-012fbd121f37" disabled={!savedViewModel.savedViewName.trim() || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'} onclick={saveCurrentView}>
2134
1355
  <Check class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" data-icon="inline-start" /> {i18n.t("common.saveView")}
2135
1356
  </Button>
2136
1357
  </div>
2137
- {#if availableSavedViews.length > 0}
1358
+ {#if savedViewModel.availableSavedViews.length > 0}
2138
1359
  <div class="svadmin-u-da7c36cd8867 svadmin-u-b950dda299d3 svadmin-u-18049387f0af svadmin-u-f46b61a9b310">
2139
- {#each availableSavedViews as view (view.id)}
1360
+ {#each savedViewModel.availableSavedViews as view (view.id)}
2140
1361
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-77a2a20e90d4 svadmin-u-fc7473ca09eb">
2141
1362
  <span class="svadmin-u-7e0b7cdf1a94 svadmin-u-f283ea9bea0e">{view.name}</span>
2142
1363
  {#if (view.source === 'team' || view.source === 'system') && savedViewProvider?.setDefault}
@@ -2146,7 +1367,7 @@
2146
1367
  aria-label={`${i18n.t(view.default ? 'common.unsetDefaultView' : 'common.setDefaultView')} ${view.name}`}
2147
1368
  aria-pressed={view.default === true}
2148
1369
  title={i18n.t(view.default ? 'common.unsetDefaultView' : 'common.setDefaultView')}
2149
- disabled={view.readOnly === true || !view.version || savedViewMutationPending || savedViewMutationError === 'conflict'}
1370
+ disabled={view.readOnly === true || !view.version || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'}
2150
1371
  onclick={() => setRemoteDefault(view)}
2151
1372
  >
2152
1373
  <Star class={view.default ? "svadmin-u-e83a7042bc91" : ""} fill={view.default ? "currentColor" : "none"} />
@@ -2158,7 +1379,7 @@
2158
1379
  <Select.Root
2159
1380
  aria-label={`${i18n.t('common.viewAccess')} ${view.name}`}
2160
1381
  value={accessMode}
2161
- disabled={view.readOnly !== false || !view.version || savedViewMutationPending || savedViewMutationError === 'conflict'}
1382
+ disabled={view.readOnly !== false || !view.version || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'}
2162
1383
  onchange={(event: Event) => {
2163
1384
  if (!(event.currentTarget instanceof HTMLSelectElement)) return;
2164
1385
  const mode = event.currentTarget.value;
@@ -2183,7 +1404,7 @@
2183
1404
  <Input
2184
1405
  aria-label={`${i18n.t('common.search')} ${view.name}`}
2185
1406
  value={accessSubjectQueries.get(view.id) ?? ''}
2186
- disabled={view.readOnly !== false || savedViewMutationPending}
1407
+ disabled={view.readOnly !== false || savedViewModel.savedViewMutationPending}
2187
1408
  oninput={(event: Event) => {
2188
1409
  if (!(event.currentTarget instanceof HTMLInputElement)) return;
2189
1410
  accessSubjectQueries.set(view.id, event.currentTarget.value);
@@ -2192,7 +1413,7 @@
2192
1413
  />
2193
1414
  <Button type="button" size="sm" variant="outline"
2194
1415
  aria-label={`${i18n.t('common.refresh')} ${view.name}`}
2195
- disabled={view.readOnly !== false || savedViewMutationPending}
1416
+ disabled={view.readOnly !== false || savedViewModel.savedViewMutationPending}
2196
1417
  onclick={() => loadAccessSubjects(view, accessSubjectQueries.get(view.id) ?? '')}>
2197
1418
  <RefreshCw />
2198
1419
  </Button>
@@ -2206,7 +1427,7 @@
2206
1427
  {#each selected as id (id)}
2207
1428
  <label>
2208
1429
  <input type="checkbox" checked
2209
- disabled={view.readOnly !== false || savedViewMutationPending || savedViewMutationError === 'conflict'}
1430
+ disabled={view.readOnly !== false || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'}
2210
1431
  onchange={() => accessDrafts.set(view.id, {
2211
1432
  mode: 'restricted', subjectIds: selected.filter(candidate => candidate !== id),
2212
1433
  })} />
@@ -2216,7 +1437,7 @@
2216
1437
  {#each (accessSubjects.get(view.id) ?? []).filter(subject => !selected.includes(subject.id)) as subject (subject.id)}
2217
1438
  <label>
2218
1439
  <input type="checkbox"
2219
- disabled={view.readOnly !== false || savedViewMutationPending || savedViewMutationError === 'conflict' || selected.length >= 200}
1440
+ disabled={view.readOnly !== false || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict' || selected.length >= 200}
2220
1441
  onchange={() => accessDrafts.set(view.id, {
2221
1442
  mode: 'restricted', subjectIds: [...selected, subject.id],
2222
1443
  })} />
@@ -2228,7 +1449,7 @@
2228
1449
  <Input
2229
1450
  aria-label={`${i18n.t('common.accessSubjects')} ${view.name}`}
2230
1451
  value={(draft?.subjectIds ?? view.access?.subjectIds ?? []).join(', ')}
2231
- disabled={view.readOnly !== false || !view.version || savedViewMutationPending || savedViewMutationError === 'conflict'}
1452
+ disabled={view.readOnly !== false || !view.version || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'}
2232
1453
  oninput={(event: Event) => {
2233
1454
  if (event.currentTarget instanceof HTMLInputElement) {
2234
1455
  accessDrafts.set(view.id, { mode: 'restricted', subjectIds: event.currentTarget.value.split(',').map(id => id.trim()) });
@@ -2240,7 +1461,7 @@
2240
1461
  <Button type="button" size="sm"
2241
1462
  aria-label={`${i18n.t('common.saveAccess')} ${view.name}`}
2242
1463
  disabled={!draft || view.readOnly !== false || !view.version
2243
- || savedViewMutationPending || savedViewMutationError === 'conflict'
1464
+ || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'
2244
1465
  || (draft.mode === 'restricted' && !decodeSavedListViewAccess(draft))}
2245
1466
  onclick={() => saveRemoteAccess(view)}>
2246
1467
  {i18n.t('common.saveAccess')}
@@ -2251,7 +1472,7 @@
2251
1472
  class="svadmin-u-52083e7da442 svadmin-u-cc46d0fa277d svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-bfa603190748 svadmin-u-8e551981c8d7 svadmin-u-ea7b2e9e070e svadmin-u-f10f771f87e9 svadmin-u-793c80e97ffb svadmin-u-9c1295a6914a"
2252
1473
  aria-label="{i18n.t("common.delete")} {view.name}"
2253
1474
  title="{i18n.t("common.delete")} {view.name}"
2254
- disabled={view.readOnly === true || savedViewMutationPending || savedViewMutationError === 'conflict'
1475
+ disabled={view.readOnly === true || savedViewModel.savedViewMutationPending || savedViewModel.savedViewMutationError === 'conflict'
2255
1476
  || (!!view.source && !savedViewProvider?.remove)}
2256
1477
  onclick={() => deleteSavedView(view.id)}
2257
1478
  >
@@ -2301,7 +1522,7 @@
2301
1522
  class="svadmin-u-ea7b2e9e070e svadmin-u-34516836730d svadmin-u-52083e7da442 svadmin-u-f7b5fa971871 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-36d4469299aa"
2302
1523
  aria-label="{i18n.t('common.clear')}: {i18n.t('common.search')}"
2303
1524
  title="{i18n.t('common.clear')}: {i18n.t('common.search')}"
2304
- onclick={() => { searchText = ""; appliedSearchText = ""; pagination = { ...pagination, current: 1 }; }}
1525
+ onclick={listState.clearSearch}
2305
1526
  >
2306
1527
  <X class="svadmin-u-ef2d6f74d3d0" />
2307
1528
  </button>
@@ -2671,7 +1892,7 @@
2671
1892
  const size = Number(e.currentTarget.value);
2672
1893
  if (Number.isSafeInteger(size) && size > 0) {
2673
1894
  markSavedViewDirty();
2674
- pagination = { ...pagination, pageSize: size, current: 1 };
1895
+ listState.pagination = { ...pagination, pageSize: size, current: 1 };
2675
1896
  }
2676
1897
  }}
2677
1898
  >
@@ -2722,7 +1943,7 @@
2722
1943
  confirmText={i18n.t('common.delete')}
2723
1944
  confirming={confirmPending || deletePermission.isLoading}
2724
1945
  onconfirm={confirmAction}
2725
- oncancel={() => { if (!confirmPending) { confirmOpen = false; deleteRequest = null; } }}
1946
+ oncancel={deletionModel.cancel}
2726
1947
  />
2727
1948
 
2728
1949
  {#if detailRecordId != null}