@svadmin/ui 0.78.1 → 0.78.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AutoTable.svelte +183 -962
- package/dist/components/list-column-preferences.svelte.d.ts +29 -0
- package/dist/components/list-column-preferences.svelte.js +92 -0
- package/dist/components/list-deletion.svelte.d.ts +38 -0
- package/dist/components/list-deletion.svelte.js +111 -0
- package/dist/components/list-selection.svelte.d.ts +31 -0
- package/dist/components/list-selection.svelte.js +100 -0
- package/dist/components/list-state.svelte.d.ts +49 -0
- package/dist/components/list-state.svelte.js +225 -0
- package/dist/components/saved-list-views.svelte.d.ts +42 -0
- package/dist/components/saved-list-views.svelte.js +405 -0
- 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 {
|
|
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,
|
|
44
|
-
import {
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
);
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
784
|
-
|
|
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
|
|
796
|
-
|
|
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
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
861
|
-
const
|
|
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 &&
|
|
869
|
-
applySavedView(
|
|
529
|
+
if (!hasExplicitURLState && activeSavedView) {
|
|
530
|
+
applySavedView(activeSavedView);
|
|
870
531
|
} else {
|
|
871
532
|
if (!hasExplicitURLState) resetToDefaultListState();
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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={() =>
|
|
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={() =>
|
|
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={
|
|
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={
|
|
1946
|
+
oncancel={deletionModel.cancel}
|
|
2726
1947
|
/>
|
|
2727
1948
|
|
|
2728
1949
|
{#if detailRecordId != null}
|