@svadmin/ui 0.88.0 → 0.91.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.
- package/dist/app.css +4 -0
- package/dist/app.theme.css +4 -0
- package/dist/component-registry.svelte.d.ts +2 -2
- package/dist/components/AboutSettings.svelte +23 -4
- package/dist/components/AdminApp.svelte +6 -4
- package/dist/components/AdminApp.svelte.d.ts +3 -3
- package/dist/components/ApiSettings.svelte +43 -27
- package/dist/components/ApprovalActionCard.svelte +8 -9
- package/dist/components/ApprovalCenter.svelte +24 -5
- package/dist/components/AuditLogViewer.svelte +2 -2
- package/dist/components/AutoComplete.svelte +610 -0
- package/dist/components/AutoComplete.svelte.d.ts +25 -0
- package/dist/components/AutoForm.svelte +263 -18
- package/dist/components/AutoForm.svelte.d.ts +19 -2
- package/dist/components/AutoTable.svelte +135 -35
- package/dist/components/AutoTable.svelte.d.ts +8 -0
- package/dist/components/BatchActionBar.svelte +42 -14
- package/dist/components/BatchActionBar.svelte.d.ts +5 -0
- package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
- package/dist/components/CancelTaskButton.svelte +14 -1
- package/dist/components/Cascader.svelte +412 -53
- package/dist/components/Cascader.svelte.d.ts +7 -1
- package/dist/components/ColorPicker.svelte +1 -1
- package/dist/components/ColorPicker.svelte.d.ts +1 -1
- package/dist/components/ComboboxField.svelte +270 -34
- package/dist/components/ComboboxField.svelte.d.ts +4 -0
- package/dist/components/CommandPalette.svelte +4 -4
- package/dist/components/DashboardView.svelte +73 -7
- package/dist/components/DashboardView.svelte.d.ts +3 -1
- package/dist/components/DateRangeInput.svelte +8 -1
- package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
- package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
- package/dist/components/DetailWorkspaceSections.svelte +233 -0
- package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
- package/dist/components/DrawerForm.svelte +78 -9
- package/dist/components/DrawerForm.svelte.d.ts +3 -3
- package/dist/components/ErrorBoundary.svelte +1 -1
- package/dist/components/ErrorComponent.svelte +19 -6
- package/dist/components/ErrorComponent.svelte.d.ts +1 -0
- package/dist/components/ErrorSummary.svelte +9 -4
- package/dist/components/FieldRenderer.svelte +203 -90
- package/dist/components/FieldRenderer.svelte.d.ts +16 -1
- package/dist/components/FileUpload.svelte +116 -51
- package/dist/components/FileUpload.svelte.d.ts +7 -27
- package/dist/components/FileUploadField.svelte +170 -0
- package/dist/components/FileUploadField.svelte.d.ts +21 -0
- package/dist/components/FilterBuilder.svelte +13 -7
- package/dist/components/FilterCollectionInput.svelte +9 -7
- package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
- package/dist/components/ForgotPasswordPage.svelte +1 -1
- package/dist/components/Header.svelte +1 -1
- package/dist/components/ImportWizard.svelte +197 -28
- package/dist/components/ImportWizard.svelte.d.ts +5 -1
- package/dist/components/InferencerPanel.svelte +4 -2
- package/dist/components/InfiniteList.svelte +52 -9
- package/dist/components/JsonSchemaForm.svelte +237 -42
- package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
- package/dist/components/KanbanBoard.svelte +43 -7
- package/dist/components/KanbanBoard.svelte.d.ts +2 -2
- package/dist/components/Layout.svelte +1 -1
- package/dist/components/LoginPage.svelte +1 -1
- package/dist/components/MetricStrip.svelte +8 -3
- package/dist/components/MetricStrip.svelte.d.ts +1 -0
- package/dist/components/ModalForm.svelte +85 -9
- package/dist/components/ModalForm.svelte.d.ts +3 -3
- package/dist/components/MoneyInput.svelte +5 -3
- package/dist/components/MoneyInput.svelte.d.ts +4 -2
- package/dist/components/MultiTabKeepAlive.svelte +502 -43
- package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
- package/dist/components/NotificationsSettings.svelte +10 -10
- package/dist/components/NumberInput.svelte +5 -3
- package/dist/components/NumberInput.svelte.d.ts +4 -2
- package/dist/components/NumericInput.svelte +277 -25
- package/dist/components/NumericInput.svelte.d.ts +3 -1
- package/dist/components/PercentInput.svelte +5 -3
- package/dist/components/PercentInput.svelte.d.ts +4 -2
- package/dist/components/PermissionMatrix.svelte +44 -8
- package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
- package/dist/components/ProfilePage.svelte +38 -13
- package/dist/components/RadioGroup.svelte +203 -4
- package/dist/components/RadioGroup.svelte.d.ts +1 -1
- package/dist/components/Rate.svelte +3 -3
- package/dist/components/Rate.svelte.d.ts +2 -2
- package/dist/components/RecordDetailDrawer.svelte +1 -1
- package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
- package/dist/components/RegisterPage.svelte +1 -1
- package/dist/components/RelationPicker.svelte +39 -11
- package/dist/components/ResourceAccessGuard.svelte +7 -2
- package/dist/components/RetryTaskButton.svelte +14 -1
- package/dist/components/RolesSettings.svelte +74 -28
- package/dist/components/SecuritySettings.svelte +66 -51
- package/dist/components/SegmentedControl.svelte +4 -2
- package/dist/components/SegmentedControl.svelte.d.ts +1 -1
- package/dist/components/Sidebar.svelte +1 -1
- package/dist/components/StepsForm.svelte +60 -10
- package/dist/components/StepsForm.svelte.d.ts +4 -2
- package/dist/components/SvadminLogo.svelte +1 -0
- package/dist/components/SvarDataGrid.svelte +42 -24
- package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
- package/dist/components/SvarResourceRead.svelte +2 -0
- package/dist/components/SvarResourceTableView.svelte +4 -2
- package/dist/components/TagsInput.svelte +3 -3
- package/dist/components/TagsInput.svelte.d.ts +2 -2
- package/dist/components/TaskDetails.svelte +22 -16
- package/dist/components/TaskDetails.svelte.d.ts +1 -1
- package/dist/components/TaskList.svelte +57 -15
- package/dist/components/TaskQueueDrawer.svelte +54 -34
- package/dist/components/Transfer.svelte +67 -5
- package/dist/components/Transfer.svelte.d.ts +4 -0
- package/dist/components/TreeSelect.svelte +337 -77
- package/dist/components/TreeSelect.svelte.d.ts +4 -0
- package/dist/components/TreeTable.svelte +6 -1
- package/dist/components/TwoFactorAuthPage.svelte +8 -4
- package/dist/components/UpdatePasswordPage.svelte +1 -1
- package/dist/components/VersionDiffViewer.svelte +9 -7
- package/dist/components/account/CompanyProfilePage.svelte +13 -12
- package/dist/components/account/MemberDirectory.svelte +14 -10
- package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
- package/dist/components/content/AuditTimeline.svelte +8 -6
- package/dist/components/content/DataState.svelte +1 -1
- package/dist/components/content/FileList.svelte +44 -13
- package/dist/components/content/FileList.svelte.d.ts +4 -2
- package/dist/components/content/FileListRow.svelte +87 -0
- package/dist/components/content/FileListRow.svelte.d.ts +11 -0
- package/dist/components/content/ImportDropzone.svelte +109 -12
- package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
- package/dist/components/content/IntegrationCard.svelte +3 -3
- package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
- package/dist/components/content/MediaThumbnail.svelte +6 -2
- package/dist/components/detail-workspace.d.ts +60 -0
- package/dist/components/detail-workspace.js +3 -0
- package/dist/components/enterprise/json-schema-form.d.ts +1 -1
- package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
- package/dist/components/enterprise/recursive-schema-form.js +288 -14
- package/dist/components/enterprise/schema-form-references.d.ts +14 -0
- package/dist/components/enterprise/schema-form-references.js +316 -0
- package/dist/components/fields/ArrayField.svelte +178 -14
- package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
- package/dist/components/fields/array-field-metadata.d.ts +4 -0
- package/dist/components/fields/array-field-metadata.js +36 -0
- package/dist/components/file-upload-reference.d.ts +11 -0
- package/dist/components/file-upload-reference.js +50 -0
- package/dist/components/file-upload-types.d.ts +32 -0
- package/dist/components/file-upload-types.js +1 -0
- package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
- package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
- package/dist/components/json-schema-form-state.d.ts +19 -0
- package/dist/components/list-deletion.svelte.js +6 -1
- package/dist/components/list-state.svelte.d.ts +12 -1
- package/dist/components/list-state.svelte.js +56 -16
- package/dist/components/numeric-format.d.ts +44 -0
- package/dist/components/numeric-format.js +198 -0
- package/dist/components/record-detail.svelte.js +2 -0
- package/dist/components/saved-list-views.d.ts +9 -2
- package/dist/components/saved-list-views.js +42 -11
- package/dist/components/saved-list-views.svelte.d.ts +3 -0
- package/dist/components/saved-list-views.svelte.js +56 -7
- package/dist/components/task-action.svelte.d.ts +1 -0
- package/dist/components/task-action.svelte.js +55 -3
- package/dist/components/task-utils.d.ts +1 -0
- package/dist/components/task-utils.js +18 -0
- package/dist/components/ui/input/input.svelte +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/sheet/sheet.svelte +4 -1
- package/dist/components/version-history.d.ts +14 -0
- package/dist/components/version-history.js +32 -0
- package/dist/components/workspace-tab-state.d.ts +78 -0
- package/dist/components/workspace-tab-state.js +587 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +4 -0
- package/package.json +5 -5
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
} from '@tanstack/svelte-table';
|
|
21
21
|
import type { Column, Header, Row, TableFeatures } from '@tanstack/table-core';
|
|
22
22
|
import { createAtom, useSelector } from '@tanstack/svelte-store';
|
|
23
|
+
import { useQueryClient } from '@tanstack/svelte-query';
|
|
23
24
|
import {
|
|
24
25
|
column_getCanSort,
|
|
25
26
|
column_getSize,
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
cell_getValue,
|
|
38
39
|
} from '@tanstack/table-core/static-functions';
|
|
39
40
|
|
|
40
|
-
import { captureAdminContext, captureAuthSession, useGetIdentity, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData, type ExportFormat, type TaskProvider } from '@svadmin/core';
|
|
41
|
+
import { captureAdminContext, captureAuthSession, useGetIdentity, getContractFormFields, useNavigation, useParsed, useResourceContract, useList, useDeleteMany, downloadData, type ExportFormat, type TaskProvider, parseQueryKey } from '@svadmin/core';
|
|
41
42
|
import { snapshotPlainData, parseContractRouteId, formatContractRouteId } from '@svadmin/core/schema';
|
|
42
43
|
import {
|
|
43
44
|
checkedTableRows,
|
|
@@ -56,7 +57,9 @@
|
|
|
56
57
|
import { readURLState, writeURLState } from '@svadmin/core';
|
|
57
58
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
58
59
|
import { createSavedListViews } from './saved-list-views.svelte.js';
|
|
59
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
captureSavedListViewState, cloneListStateSnapshot, createListState, type ListStateSnapshot,
|
|
62
|
+
} from './list-state.svelte.js';
|
|
60
63
|
import { createListSelection } from './list-selection.svelte.js';
|
|
61
64
|
import { createListColumnPreferences } from './list-column-preferences.svelte.js';
|
|
62
65
|
import { createListDeletion } from './list-deletion.svelte.js';
|
|
@@ -87,7 +90,10 @@
|
|
|
87
90
|
Eye, Copy, RefreshCw, Rows, X, Bookmark, Check
|
|
88
91
|
} from '@lucide/svelte';
|
|
89
92
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
93
|
+
import BatchActionBar from './BatchActionBar.svelte';
|
|
90
94
|
import RecordDetailDrawer from './RecordDetailDrawer.svelte';
|
|
95
|
+
import DetailWorkspaceSections from './DetailWorkspaceSections.svelte';
|
|
96
|
+
import type { DetailWorkspaceSectionsConfig } from './detail-workspace.js';
|
|
91
97
|
import QuickEditDrawer from './QuickEditDrawer.svelte';
|
|
92
98
|
import RecordRowActions from './RecordRowActions.svelte';
|
|
93
99
|
import CanAccess from './CanAccess.svelte';
|
|
@@ -136,6 +142,10 @@
|
|
|
136
142
|
pagination?: { current: number; pageSize: number };
|
|
137
143
|
/** Externally controlled sorters */
|
|
138
144
|
sorters?: Sort[];
|
|
145
|
+
/** 初次挂载时恢复的查询快照;优先级低于受控分页/排序和已启用的 URL 状态。 */
|
|
146
|
+
initialListState?: ListStateSnapshot | undefined;
|
|
147
|
+
/** 查询快照变化回调;搜索在 debounce 应用后才触发。 */
|
|
148
|
+
onListStateChange?: ((state: ListStateSnapshot) => void) | undefined;
|
|
139
149
|
/** 批量操作必须按 selection.scope 区分明确 ID 与全部查询结果。 */
|
|
140
150
|
batchActions?: Snippet<[{ selectedIds: (string | number)[]; selection: BatchSelection }]>;
|
|
141
151
|
/** Summary row rendered at table footer */
|
|
@@ -151,6 +161,8 @@
|
|
|
151
161
|
exportTaskIdempotencyKey?: string;
|
|
152
162
|
exportFormat?: ExportFormat;
|
|
153
163
|
exportMaxItemCount?: number;
|
|
164
|
+
/** Optional record-scoped detail workspace sections. Audit uses the existing AuditLogProvider. */
|
|
165
|
+
detailWorkspace?: DetailWorkspaceSectionsConfig;
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
let {
|
|
@@ -176,6 +188,8 @@
|
|
|
176
188
|
summary,
|
|
177
189
|
pagination: externalPagination,
|
|
178
190
|
sorters: externalSorters,
|
|
191
|
+
initialListState,
|
|
192
|
+
onListStateChange,
|
|
179
193
|
deleteVariables,
|
|
180
194
|
savedViewProvider,
|
|
181
195
|
applyRemoteDefaultView = false,
|
|
@@ -184,11 +198,13 @@
|
|
|
184
198
|
exportTaskIdempotencyKey,
|
|
185
199
|
exportFormat = 'csv',
|
|
186
200
|
exportMaxItemCount,
|
|
201
|
+
detailWorkspace,
|
|
187
202
|
}: Props = $props();
|
|
188
203
|
|
|
189
204
|
let densityOverride = $state<'compact' | 'comfortable' | undefined>(undefined);
|
|
190
205
|
const currentDensity = $derived(densityOverride ?? density);
|
|
191
206
|
const adminContext = captureAdminContext();
|
|
207
|
+
const queryClient = useQueryClient();
|
|
192
208
|
const parsed = useParsed();
|
|
193
209
|
const navigation = useNavigation();
|
|
194
210
|
|
|
@@ -251,6 +267,26 @@
|
|
|
251
267
|
|| urlState.search !== undefined
|
|
252
268
|
|| urlState.filters !== undefined
|
|
253
269
|
));
|
|
270
|
+
const initialListStateSnapshot = untrack(() => initialListState
|
|
271
|
+
? cloneListStateSnapshot(initialListState)
|
|
272
|
+
: undefined);
|
|
273
|
+
const initialPreferenceScope: ListPreferenceScope = untrack(() => listPreferenceScope);
|
|
274
|
+
const initialPreferenceSessionKey = untrack(() => preferenceSession.cacheKey);
|
|
275
|
+
let initialListStateRetired = !initialListStateSnapshot;
|
|
276
|
+
let initialIdentityHydrationPending = !!initialListStateSnapshot
|
|
277
|
+
&& initialPreferenceScope.identityKey?.startsWith('unresolved:') === true;
|
|
278
|
+
|
|
279
|
+
function sameScopeExceptIdentity(left: ListPreferenceScope, right: ListPreferenceScope): boolean {
|
|
280
|
+
return left.resourceName === right.resourceName
|
|
281
|
+
&& left.providerName === right.providerName
|
|
282
|
+
&& left.tenantIdentity === right.tenantIdentity;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function initialListStateApplies(scope: ListPreferenceScope): boolean {
|
|
286
|
+
return !!initialListStateSnapshot && !initialListStateRetired
|
|
287
|
+
&& sameScopeExceptIdentity(scope, initialPreferenceScope);
|
|
288
|
+
}
|
|
289
|
+
|
|
254
290
|
const savedViewModel = createSavedListViews({
|
|
255
291
|
scope: () => listPreferenceScope,
|
|
256
292
|
columns: () => savedViewColumnIds,
|
|
@@ -258,7 +294,9 @@
|
|
|
258
294
|
isScopeLoaded: preferenceScopeIsLoaded,
|
|
259
295
|
readPreference: readScopedPreference,
|
|
260
296
|
hasExplicitURLState,
|
|
261
|
-
|
|
297
|
+
initialStateOverridesSavedView: !!initialListStateSnapshot,
|
|
298
|
+
canApplyRemoteDefault: () => applyRemoteDefaultView && !externalPagination && !externalSorters
|
|
299
|
+
&& !initialListStateApplies(listPreferenceScope),
|
|
262
300
|
captureState: () => {
|
|
263
301
|
listState.flushSearch();
|
|
264
302
|
return getCurrentSavedViewState();
|
|
@@ -275,6 +313,7 @@
|
|
|
275
313
|
// The table can stay mounted while the surrounding tenant/provider changes.
|
|
276
314
|
// Keep persistence effects paused until the new scope has been loaded.
|
|
277
315
|
let loadedPreferenceScopeId = $state(untrack(() => listPreferenceScopeId(listPreferenceScope)));
|
|
316
|
+
const queryScopeReady = $derived(loadedPreferenceScopeId === listPreferenceScopeId(listPreferenceScope));
|
|
278
317
|
|
|
279
318
|
// Snapshot resource values for initial state (untrack to avoid reactive tracking)
|
|
280
319
|
const storedPageSize = parseInt(readLocalPreference('svadmin-default-page-size') ?? '', 10);
|
|
@@ -286,10 +325,12 @@
|
|
|
286
325
|
externalPagination: () => externalPagination,
|
|
287
326
|
externalSorters: () => externalSorters,
|
|
288
327
|
initialURLState: urlState,
|
|
328
|
+
initialListState: initialListStateSnapshot,
|
|
289
329
|
initialViewState,
|
|
290
330
|
syncWithLocation: () => syncWithLocation,
|
|
291
331
|
writeURLState: (state) => writeURLState(state, adminContext),
|
|
292
332
|
onDirty: markSavedViewDirty,
|
|
333
|
+
onStateChange: (state) => onListStateChange?.(state),
|
|
293
334
|
});
|
|
294
335
|
const pagination = $derived(listState.pagination);
|
|
295
336
|
const sorters = $derived(listState.sorters);
|
|
@@ -325,7 +366,9 @@
|
|
|
325
366
|
const listResult = useList({
|
|
326
367
|
get resource() { void activeRendering; return binding.resource; },
|
|
327
368
|
get dataProviderName() { return binding.dataProviderName; },
|
|
328
|
-
|
|
369
|
+
errorNotification: false,
|
|
370
|
+
// 作用域恢复完成前不发送查询,避免把旧作用域快照作为新作用域的首帧请求。
|
|
371
|
+
get queryOptions() { return { enabled: canRead && queryScopeReady }; },
|
|
329
372
|
get pagination() { return queryPagination; },
|
|
330
373
|
get sorters() { return querySorters; },
|
|
331
374
|
get filters() { return queryFilters; },
|
|
@@ -500,15 +543,14 @@
|
|
|
500
543
|
});
|
|
501
544
|
|
|
502
545
|
function getCurrentSavedViewState(): SavedListViewState {
|
|
503
|
-
return
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
};
|
|
546
|
+
return captureSavedListViewState(
|
|
547
|
+
listState.captureState(), columnPreferences.captureState(), initPageSize,
|
|
548
|
+
);
|
|
507
549
|
}
|
|
508
550
|
|
|
509
551
|
function applySavedViewState(state: SavedListViewState): void {
|
|
510
552
|
listState.applyState(state);
|
|
511
|
-
sortingAtom.set(
|
|
553
|
+
sortingAtom.set(listState.sorters.map((sorter) => ({ id: sorter.field, desc: sorter.order === 'desc' })));
|
|
512
554
|
columnPreferences.applyState(state);
|
|
513
555
|
selectionModel.reset();
|
|
514
556
|
}
|
|
@@ -525,10 +567,30 @@
|
|
|
525
567
|
const scopeId = listPreferenceScopeId(scope);
|
|
526
568
|
if (scopeId === loadedPreferenceScopeId) return;
|
|
527
569
|
|
|
570
|
+
if (initialListStateSnapshot) {
|
|
571
|
+
const identityHydration = initialIdentityHydrationPending
|
|
572
|
+
&& sameScopeExceptIdentity(scope, initialPreferenceScope)
|
|
573
|
+
&& preferenceSession.cacheKey === initialPreferenceSessionKey
|
|
574
|
+
&& scope.identityKey !== undefined
|
|
575
|
+
&& !scope.identityKey.startsWith('unresolved:');
|
|
576
|
+
if (identityHydration) {
|
|
577
|
+
initialIdentityHydrationPending = false;
|
|
578
|
+
} else {
|
|
579
|
+
initialListStateRetired = true;
|
|
580
|
+
initialIdentityHydrationPending = false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
528
584
|
const activeSavedView = savedViewModel.restoreScope(scope);
|
|
529
585
|
const restoredColumns = columnPreferences.readScope(scope);
|
|
530
586
|
|
|
531
|
-
if (!hasExplicitURLState &&
|
|
587
|
+
if (!hasExplicitURLState && initialListStateApplies(scope)) {
|
|
588
|
+
// 挂载快照已由 createListState 应用;首次身份 hydrate 只保留用户编辑,不重放快照。
|
|
589
|
+
// 初始快照优先于旧活动视图时取消活动标记,但不删除保存视图或写入其他作用域。
|
|
590
|
+
savedViewModel.clearActiveSavedView();
|
|
591
|
+
columnPreferences.applyState(restoredColumns);
|
|
592
|
+
selectionModel.reset();
|
|
593
|
+
} else if (!hasExplicitURLState && activeSavedView) {
|
|
532
594
|
applySavedView(activeSavedView);
|
|
533
595
|
} else {
|
|
534
596
|
if (!hasExplicitURLState) resetToDefaultListState();
|
|
@@ -749,6 +811,8 @@
|
|
|
749
811
|
// ─── Confirm dialog ───────────────────────────────────────────
|
|
750
812
|
let operationError = $state<string | null>(null);
|
|
751
813
|
let detailOpenedInHistory = $state(false);
|
|
814
|
+
let detailOpenedId = $state<string | number | undefined>();
|
|
815
|
+
let detailRouteChangedInHistory = $state(false);
|
|
752
816
|
let quickEditId = $state<string | number | undefined>();
|
|
753
817
|
const tableScope = $derived({
|
|
754
818
|
contract: binding.resource, resourceName, provider: adminContext.providers?.[binding.dataProviderName],
|
|
@@ -788,6 +852,8 @@
|
|
|
788
852
|
selectionModel.reset();
|
|
789
853
|
expandedAtom.set({});
|
|
790
854
|
detailOpenedInHistory = false;
|
|
855
|
+
detailOpenedId = undefined;
|
|
856
|
+
detailRouteChangedInHistory = false;
|
|
791
857
|
localDetailId = undefined;
|
|
792
858
|
quickEditId = undefined;
|
|
793
859
|
const queryScopeChanged = previousTableScope.contract !== scope.contract
|
|
@@ -809,16 +875,35 @@
|
|
|
809
875
|
if (!canRead || !canShow) return;
|
|
810
876
|
if (!syncWithLocation) { localDetailId = id; return; }
|
|
811
877
|
detailOpenedInHistory = true;
|
|
878
|
+
detailOpenedId = id;
|
|
879
|
+
detailRouteChangedInHistory = false;
|
|
812
880
|
writeURLState({ detailId: formatContractRouteId(binding.resource, id) }, adminContext, 'push');
|
|
813
881
|
}
|
|
882
|
+
$effect.pre(() => {
|
|
883
|
+
if (!detailOpenedInHistory || detailOpenedId == null || detailRecordId == null) return;
|
|
884
|
+
if (String(detailRecordId) !== String(detailOpenedId)) detailRouteChangedInHistory = true;
|
|
885
|
+
});
|
|
814
886
|
function closeDetail(): void {
|
|
815
887
|
if (!syncWithLocation) { localDetailId = undefined; return; }
|
|
816
888
|
if (detailOpenedInHistory && adminContext.routerProvider) {
|
|
889
|
+
const routeChanged = detailRouteChangedInHistory;
|
|
817
890
|
detailOpenedInHistory = false;
|
|
891
|
+
detailOpenedId = undefined;
|
|
892
|
+
detailRouteChangedInHistory = false;
|
|
893
|
+
// The drawer can move to another record without unmounting (next/previous).
|
|
894
|
+
// Once that happened, the top history entry may be a detail route even if
|
|
895
|
+
// the current record is the originally opened one (A -> B -> A), so a single
|
|
896
|
+
// back() could reopen another record instead of closing.
|
|
897
|
+
if (routeChanged) {
|
|
898
|
+
writeURLState({ detailId: undefined }, adminContext);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
818
901
|
adminContext.routerProvider.back();
|
|
819
902
|
return;
|
|
820
903
|
}
|
|
821
904
|
detailOpenedInHistory = false;
|
|
905
|
+
detailOpenedId = undefined;
|
|
906
|
+
detailRouteChangedInHistory = false;
|
|
822
907
|
writeURLState({ detailId: undefined }, adminContext);
|
|
823
908
|
}
|
|
824
909
|
// ─── CSV Export ───────────────────────────────────────────────
|
|
@@ -843,6 +928,15 @@
|
|
|
843
928
|
function refreshList() {
|
|
844
929
|
if (canRead && !query.isFetching) void listResult.refetch();
|
|
845
930
|
}
|
|
931
|
+
function refreshPermission() {
|
|
932
|
+
const resource = resourceName;
|
|
933
|
+
void queryClient.invalidateQueries({
|
|
934
|
+
predicate: query => {
|
|
935
|
+
const parsed = parseQueryKey(query.queryKey);
|
|
936
|
+
return parsed?.kind === 'access' && parsed.resource === resource;
|
|
937
|
+
},
|
|
938
|
+
}).catch(() => {});
|
|
939
|
+
}
|
|
846
940
|
const gridRowIndex = $derived(new Map(tableView.rows.map(row => [rowIdValue(row), row])));
|
|
847
941
|
const gridColumns = $derived.by(() => {
|
|
848
942
|
void tableColumnOrder.current; void tableColumnVisibility.current;
|
|
@@ -1108,20 +1202,16 @@
|
|
|
1108
1202
|
</div>
|
|
1109
1203
|
{/if}
|
|
1110
1204
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
<
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
<span class="svadmin-u-bfa603190748" role="status">{i18n.t("common.processing")}</span>
|
|
1122
|
-
{/if}
|
|
1123
|
-
</div>
|
|
1124
|
-
<div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
|
|
1205
|
+
{#if selectedCount > 0 || allMatchingSelected || (deleteRequest?.batch && deleteManyMutation.isPending)}
|
|
1206
|
+
<div data-svadmin-batch-toolbar>
|
|
1207
|
+
<BatchActionBar
|
|
1208
|
+
{selectedCount}
|
|
1209
|
+
selectedLabel={selectionLabel}
|
|
1210
|
+
ariaLabel={selectionLabel}
|
|
1211
|
+
forceVisible={allMatchingSelected}
|
|
1212
|
+
pending={deleteManyMutation.isPending}
|
|
1213
|
+
onclear={clearSelection}
|
|
1214
|
+
>
|
|
1125
1215
|
{#if batchActions && selectionReady && selectedCount > 0}
|
|
1126
1216
|
{@render batchActions({ selectedIds: [...selectedIds], selection: batchSelectionSnapshot() })}
|
|
1127
1217
|
{/if}
|
|
@@ -1142,16 +1232,7 @@
|
|
|
1142
1232
|
<Trash2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" data-icon="inline-start" aria-hidden="true" /> {i18n.t("common.batchDelete", { count: selectedCount })}
|
|
1143
1233
|
</Button>
|
|
1144
1234
|
{/if}
|
|
1145
|
-
|
|
1146
|
-
variant="ghost"
|
|
1147
|
-
size="sm"
|
|
1148
|
-
class="svadmin-u-ed8a5df7b2fb svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-ea7b2e9e070e"
|
|
1149
|
-
disabled={deleteManyMutation.isPending}
|
|
1150
|
-
onclick={clearSelection}
|
|
1151
|
-
>
|
|
1152
|
-
{i18n.t("common.clearSelection")}
|
|
1153
|
-
</Button>
|
|
1154
|
-
</div>
|
|
1235
|
+
</BatchActionBar>
|
|
1155
1236
|
</div>
|
|
1156
1237
|
{/if}
|
|
1157
1238
|
|
|
@@ -1305,6 +1386,9 @@
|
|
|
1305
1386
|
<div>
|
|
1306
1387
|
<h4 class="svadmin-u-2689f3958069 svadmin-u-fc7473ca09eb">{i18n.t("common.savedViews")}</h4>
|
|
1307
1388
|
<p class="svadmin-u-b6b02c0ebef6 svadmin-u-359090c2d529 svadmin-u-bfa603190748">{i18n.t("common.savedViewsHint")}</p>
|
|
1389
|
+
{#if savedViewModel.staleSavedViewColumns.length > 0}
|
|
1390
|
+
<p role="status">{i18n.t('common.savedViewStaleFields', { count: savedViewModel.staleSavedViewColumns.length })}</p>
|
|
1391
|
+
{/if}
|
|
1308
1392
|
</div>
|
|
1309
1393
|
{#if savedViewModel.remoteViewsLoading}
|
|
1310
1394
|
<p role="status">{i18n.t('common.loading')}</p>
|
|
@@ -1610,8 +1694,13 @@
|
|
|
1610
1694
|
</div>
|
|
1611
1695
|
{/each}
|
|
1612
1696
|
</div>
|
|
1697
|
+
{:else if listPermission.isError}
|
|
1698
|
+
<p role="alert">{i18n.t('common.permissionUnavailable')}</p>
|
|
1699
|
+
<Button type="button" variant="outline" onclick={refreshPermission}>
|
|
1700
|
+
{i18n.t('common.retry')}
|
|
1701
|
+
</Button>
|
|
1613
1702
|
{:else if !canRead}
|
|
1614
|
-
<p role="alert">{i18n.t('common.
|
|
1703
|
+
<p role="alert">{i18n.t('common.permissionDenied')}</p>
|
|
1615
1704
|
{:else if query.error || query.isError || !pageRecords.ok}
|
|
1616
1705
|
<DataState
|
|
1617
1706
|
state="error"
|
|
@@ -1958,6 +2047,16 @@
|
|
|
1958
2047
|
oncancel={deletionModel.cancel}
|
|
1959
2048
|
/>
|
|
1960
2049
|
|
|
2050
|
+
{#snippet workspaceSections()}
|
|
2051
|
+
{#if detailRecordId != null && detailWorkspace}
|
|
2052
|
+
<DetailWorkspaceSections
|
|
2053
|
+
{resourceName}
|
|
2054
|
+
recordId={detailRecordId}
|
|
2055
|
+
sections={detailWorkspace}
|
|
2056
|
+
/>
|
|
2057
|
+
{/if}
|
|
2058
|
+
{/snippet}
|
|
2059
|
+
|
|
1961
2060
|
{#if detailRecordId != null}
|
|
1962
2061
|
<RecordDetailDrawer
|
|
1963
2062
|
rendering={activeRendering}
|
|
@@ -1965,6 +2064,7 @@
|
|
|
1965
2064
|
open={detailOpen}
|
|
1966
2065
|
recordId={detailRecordId}
|
|
1967
2066
|
onClose={closeDetail}
|
|
2067
|
+
{...definedOptions({ extraSections: detailWorkspace ? workspaceSections : undefined })}
|
|
1968
2068
|
/>
|
|
1969
2069
|
{/if}
|
|
1970
2070
|
|
|
@@ -3,7 +3,9 @@ import type { AutoTableGridState } from './auto-table-grid.js';
|
|
|
3
3
|
import { type ExportFormat, type TaskProvider } from '@svadmin/core';
|
|
4
4
|
import { type BatchSelection } from './table-contract';
|
|
5
5
|
import type { BaseRecord, FieldDefinition, Sort } from '@svadmin/core';
|
|
6
|
+
import { type ListStateSnapshot } from './list-state.svelte.js';
|
|
6
7
|
import { type SavedListViewProvider } from './saved-list-views.js';
|
|
8
|
+
import type { DetailWorkspaceSectionsConfig } from './detail-workspace.js';
|
|
7
9
|
import type { Snippet } from 'svelte';
|
|
8
10
|
interface Props {
|
|
9
11
|
resourceName: string;
|
|
@@ -54,6 +56,10 @@ interface Props {
|
|
|
54
56
|
};
|
|
55
57
|
/** Externally controlled sorters */
|
|
56
58
|
sorters?: Sort[];
|
|
59
|
+
/** 初次挂载时恢复的查询快照;优先级低于受控分页/排序和已启用的 URL 状态。 */
|
|
60
|
+
initialListState?: ListStateSnapshot | undefined;
|
|
61
|
+
/** 查询快照变化回调;搜索在 debounce 应用后才触发。 */
|
|
62
|
+
onListStateChange?: ((state: ListStateSnapshot) => void) | undefined;
|
|
57
63
|
/** 批量操作必须按 selection.scope 区分明确 ID 与全部查询结果。 */
|
|
58
64
|
batchActions?: Snippet<[{
|
|
59
65
|
selectedIds: (string | number)[];
|
|
@@ -76,6 +82,8 @@ interface Props {
|
|
|
76
82
|
exportTaskIdempotencyKey?: string;
|
|
77
83
|
exportFormat?: ExportFormat;
|
|
78
84
|
exportMaxItemCount?: number;
|
|
85
|
+
/** Optional record-scoped detail workspace sections. Audit uses the existing AuditLogProvider. */
|
|
86
|
+
detailWorkspace?: DetailWorkspaceSectionsConfig;
|
|
79
87
|
}
|
|
80
88
|
declare const AutoTable: import("svelte").Component<Props, {}, "">;
|
|
81
89
|
type AutoTable = ReturnType<typeof AutoTable>;
|
|
@@ -4,12 +4,20 @@
|
|
|
4
4
|
import { Button } from './ui/button/index.js';
|
|
5
5
|
import { Badge } from './ui/badge/index.js';
|
|
6
6
|
import { cn } from '../utils.js';
|
|
7
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
8
|
+
|
|
9
|
+
const i18n = useTranslation();
|
|
7
10
|
|
|
8
11
|
interface Props {
|
|
9
12
|
selectedCount?: number;
|
|
10
13
|
totalCount?: number;
|
|
11
14
|
selectedLabel?: string;
|
|
12
15
|
clearLabel?: string;
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
pending?: boolean;
|
|
18
|
+
pendingLabel?: string;
|
|
19
|
+
/** 全量匹配等选择模式可在零计数时保留清除入口。 */
|
|
20
|
+
forceVisible?: boolean;
|
|
13
21
|
variant?: 'default' | 'floating' | 'subtle';
|
|
14
22
|
onclear?: () => void;
|
|
15
23
|
actions?: Snippet;
|
|
@@ -21,7 +29,11 @@
|
|
|
21
29
|
selectedCount = 0,
|
|
22
30
|
totalCount,
|
|
23
31
|
selectedLabel,
|
|
24
|
-
clearLabel
|
|
32
|
+
clearLabel,
|
|
33
|
+
ariaLabel,
|
|
34
|
+
pending = false,
|
|
35
|
+
pendingLabel,
|
|
36
|
+
forceVisible = false,
|
|
25
37
|
variant = 'default',
|
|
26
38
|
onclear,
|
|
27
39
|
actions,
|
|
@@ -29,17 +41,26 @@
|
|
|
29
41
|
class: className = '',
|
|
30
42
|
}: Props = $props();
|
|
31
43
|
|
|
32
|
-
const isVisible = $derived(selectedCount > 0);
|
|
44
|
+
const isVisible = $derived(selectedCount > 0 || pending || forceVisible);
|
|
45
|
+
const clearText = $derived(clearLabel ?? i18n.t('common.clearSelection'));
|
|
46
|
+
const regionLabel = $derived(ariaLabel ?? i18n.t('common.batchActions'));
|
|
47
|
+
const busyText = $derived(pendingLabel ?? i18n.t('common.processing'));
|
|
33
48
|
const displayText = $derived.by(() => {
|
|
34
49
|
if (selectedLabel) {
|
|
35
50
|
return selectedLabel.replace('{count}', String(selectedCount));
|
|
36
51
|
}
|
|
37
52
|
return totalCount !== undefined
|
|
38
|
-
?
|
|
39
|
-
:
|
|
53
|
+
? i18n.t('common.selectedOfTotal', { count: selectedCount, total: totalCount })
|
|
54
|
+
: i18n.t('common.selectedCount', { count: selectedCount });
|
|
40
55
|
});
|
|
56
|
+
|
|
57
|
+
function handleClear() {
|
|
58
|
+
if (!pending) onclear?.();
|
|
59
|
+
}
|
|
41
60
|
</script>
|
|
42
61
|
|
|
62
|
+
<span class="svadmin-sr-only" role="status" aria-live="polite" aria-atomic="true">{pending ? busyText : ''}</span>
|
|
63
|
+
|
|
43
64
|
{#if isVisible}
|
|
44
65
|
{#if variant === 'floating'}
|
|
45
66
|
<div
|
|
@@ -48,35 +69,39 @@
|
|
|
48
69
|
className
|
|
49
70
|
)}
|
|
50
71
|
role="region"
|
|
51
|
-
aria-label=
|
|
72
|
+
aria-label={regionLabel}
|
|
73
|
+
aria-busy={pending || undefined}
|
|
52
74
|
>
|
|
53
75
|
<div class="svadmin-batch-action-bar__selection">
|
|
54
76
|
<CheckCircle2 class="svadmin-batch-action-bar__icon" aria-hidden="true" />
|
|
55
77
|
<span class="svadmin-batch-action-bar__label">{displayText}</span>
|
|
78
|
+
{#if pending}<span aria-hidden="true">{busyText}</span>{/if}
|
|
56
79
|
</div>
|
|
57
80
|
|
|
58
81
|
{#if actions}
|
|
59
|
-
<div class="svadmin-batch-action-bar__actions">
|
|
82
|
+
<div class="svadmin-batch-action-bar__actions" inert={pending}>
|
|
60
83
|
{@render actions()}
|
|
61
84
|
</div>
|
|
62
85
|
{/if}
|
|
63
86
|
|
|
64
87
|
{#if children}
|
|
65
|
-
<div class="svadmin-batch-action-bar__actions">
|
|
88
|
+
<div class="svadmin-batch-action-bar__actions" inert={pending}>
|
|
66
89
|
{@render children()}
|
|
67
90
|
</div>
|
|
68
91
|
{/if}
|
|
69
92
|
|
|
70
93
|
{#if onclear}
|
|
71
94
|
<Button
|
|
95
|
+
type="button"
|
|
72
96
|
variant="ghost"
|
|
73
97
|
size="sm"
|
|
74
98
|
class="svadmin-batch-action-bar__clear"
|
|
75
|
-
onclick={
|
|
76
|
-
aria-label={
|
|
99
|
+
onclick={handleClear}
|
|
100
|
+
aria-label={clearText}
|
|
101
|
+
disabled={pending}
|
|
77
102
|
>
|
|
78
103
|
<X class="svadmin-batch-action-bar__clear-icon" aria-hidden="true" />
|
|
79
|
-
{
|
|
104
|
+
{clearText}
|
|
80
105
|
</Button>
|
|
81
106
|
{/if}
|
|
82
107
|
</div>
|
|
@@ -88,25 +113,28 @@
|
|
|
88
113
|
className
|
|
89
114
|
)}
|
|
90
115
|
role="region"
|
|
91
|
-
aria-label=
|
|
116
|
+
aria-label={regionLabel}
|
|
117
|
+
aria-busy={pending || undefined}
|
|
92
118
|
>
|
|
93
119
|
<div class="svadmin-batch-action-bar__selection">
|
|
94
120
|
<Badge variant="secondary" class="svadmin-batch-action-bar__count">
|
|
95
121
|
{selectedCount}
|
|
96
122
|
</Badge>
|
|
97
123
|
<span class="svadmin-batch-action-bar__label">{displayText}</span>
|
|
124
|
+
{#if pending}<span aria-hidden="true">{busyText}</span>{/if}
|
|
98
125
|
{#if onclear}
|
|
99
126
|
<button
|
|
100
127
|
type="button"
|
|
101
128
|
class="svadmin-batch-action-bar__clear"
|
|
102
|
-
onclick={
|
|
129
|
+
onclick={handleClear}
|
|
130
|
+
disabled={pending}
|
|
103
131
|
>
|
|
104
|
-
{
|
|
132
|
+
{clearText}
|
|
105
133
|
</button>
|
|
106
134
|
{/if}
|
|
107
135
|
</div>
|
|
108
136
|
|
|
109
|
-
<div class="svadmin-batch-action-bar__actions">
|
|
137
|
+
<div class="svadmin-batch-action-bar__actions" inert={pending}>
|
|
110
138
|
{#if actions}
|
|
111
139
|
{@render actions()}
|
|
112
140
|
{/if}
|
|
@@ -4,6 +4,11 @@ interface Props {
|
|
|
4
4
|
totalCount?: number;
|
|
5
5
|
selectedLabel?: string;
|
|
6
6
|
clearLabel?: string;
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
pending?: boolean;
|
|
9
|
+
pendingLabel?: string;
|
|
10
|
+
/** 全量匹配等选择模式可在零计数时保留清除入口。 */
|
|
11
|
+
forceVisible?: boolean;
|
|
7
12
|
variant?: 'default' | 'floating' | 'subtle';
|
|
8
13
|
onclear?: () => void;
|
|
9
14
|
actions?: Snippet;
|
|
@@ -153,9 +153,11 @@
|
|
|
153
153
|
{/if}
|
|
154
154
|
|
|
155
155
|
{#if extraSections && canReadRecord && query.isSuccess}
|
|
156
|
-
|
|
157
|
-
{
|
|
158
|
-
|
|
156
|
+
{#key `${resourceName}:${recordId}`}
|
|
157
|
+
<section data-svadmin-detail-extensions aria-label={i18n.t('common.additionalDetails')}>
|
|
158
|
+
{@render extraSections()}
|
|
159
|
+
</section>
|
|
160
|
+
{/key}
|
|
159
161
|
{/if}
|
|
160
162
|
|
|
161
163
|
{#snippet footer()}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { createTaskAction } from './task-action.svelte.js';
|
|
7
7
|
|
|
8
8
|
const i18n = useTranslation();
|
|
9
|
+
const instanceId = $props.id();
|
|
10
|
+
const errorId = `${instanceId}-error`;
|
|
9
11
|
|
|
10
12
|
let {
|
|
11
13
|
taskId,
|
|
@@ -37,7 +39,9 @@
|
|
|
37
39
|
});
|
|
38
40
|
</script>
|
|
39
41
|
|
|
40
|
-
<
|
|
42
|
+
<span class="task-action-feedback">
|
|
43
|
+
<Button {variant} {size} onclick={action.run} disabled={!action.available || action.pending}
|
|
44
|
+
aria-describedby={action.failed && !onError ? errorId : undefined}>
|
|
41
45
|
{#if action.pending}
|
|
42
46
|
<Loader2 class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />
|
|
43
47
|
{:else}
|
|
@@ -49,3 +53,12 @@
|
|
|
49
53
|
{i18n.t('common.cancel')}
|
|
50
54
|
{/if}
|
|
51
55
|
</Button>
|
|
56
|
+
{#if action.failed && !onError}
|
|
57
|
+
<span id={errorId} role="alert">{i18n.t('task.actionFailed')}</span>
|
|
58
|
+
{/if}
|
|
59
|
+
</span>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.task-action-feedback { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; max-width: 16rem; }
|
|
63
|
+
.task-action-feedback [role="alert"] { color: var(--destructive); font-size: 12px; line-height: 16px; white-space: normal; overflow-wrap: anywhere; }
|
|
64
|
+
</style>
|