@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
|
@@ -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.retry')}
|
|
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>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
4
|
import { captureAdminContext } from '@svadmin/core';
|
|
4
5
|
import type { AuthProvider } from '@svadmin/core';
|
|
@@ -6,6 +7,8 @@
|
|
|
6
7
|
import type { RoleInfo, ResourceInfo, ActionInfo } from '../types.js';
|
|
7
8
|
import { toast } from '@svadmin/core/toast';
|
|
8
9
|
import { AlertCircle } from '@lucide/svelte';
|
|
10
|
+
import { Button } from './ui/button/index.js';
|
|
11
|
+
import DataState from './content/DataState.svelte';
|
|
9
12
|
|
|
10
13
|
const i18n = useTranslation();
|
|
11
14
|
const adminContext = captureAdminContext();
|
|
@@ -22,7 +25,8 @@
|
|
|
22
25
|
let loadingRoles = $state(true);
|
|
23
26
|
let loadingPermissions = $state(false);
|
|
24
27
|
let saving = $state(false);
|
|
25
|
-
let error = $state<
|
|
28
|
+
let error = $state<'roles' | 'unsupported' | null>(null);
|
|
29
|
+
let permissionError = $state(false);
|
|
26
30
|
let roleRequestEpoch = 0;
|
|
27
31
|
let permissionRequestEpoch = 0;
|
|
28
32
|
let mutationRequestEpoch = 0;
|
|
@@ -35,7 +39,7 @@
|
|
|
35
39
|
}))
|
|
36
40
|
);
|
|
37
41
|
|
|
38
|
-
let matrixActions = $
|
|
42
|
+
let matrixActions = $derived<ActionInfo[]>([
|
|
39
43
|
{ code: 'create', name: i18n.t('common.create') ?? 'Create' },
|
|
40
44
|
{ code: 'read', name: i18n.t('common.detail') ?? 'Read' },
|
|
41
45
|
{ code: 'update', name: i18n.t('common.edit') ?? 'Update' },
|
|
@@ -45,7 +49,7 @@
|
|
|
45
49
|
async function loadRoles(scopedProvider: AuthProvider | null, epoch: number) {
|
|
46
50
|
if (!scopedProvider?.getRoles) {
|
|
47
51
|
if (epoch !== roleRequestEpoch) return;
|
|
48
|
-
error =
|
|
52
|
+
error = 'unsupported';
|
|
49
53
|
loadingRoles = false;
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
@@ -54,8 +58,8 @@
|
|
|
54
58
|
if (epoch !== roleRequestEpoch) return;
|
|
55
59
|
roles = fetchedRoles.map(r => ({ code: r.id, ...r }));
|
|
56
60
|
selectedRoleCode = roles[0]?.code ?? '';
|
|
57
|
-
} catch
|
|
58
|
-
if (epoch === roleRequestEpoch) error =
|
|
61
|
+
} catch {
|
|
62
|
+
if (epoch === roleRequestEpoch) error = 'roles';
|
|
59
63
|
} finally {
|
|
60
64
|
if (epoch === roleRequestEpoch) loadingRoles = false;
|
|
61
65
|
}
|
|
@@ -63,15 +67,20 @@
|
|
|
63
67
|
|
|
64
68
|
async function loadPermissionsForRole(roleId: string) {
|
|
65
69
|
const scopedProvider = authProvider;
|
|
66
|
-
const epoch = permissionRequestEpoch;
|
|
67
|
-
if (
|
|
70
|
+
const epoch = ++permissionRequestEpoch;
|
|
71
|
+
if (permissionsCache[roleId]) return;
|
|
72
|
+
if (!scopedProvider?.getRolePermissions) {
|
|
73
|
+
permissionError = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
68
76
|
|
|
69
77
|
loadingPermissions = true;
|
|
78
|
+
permissionError = false;
|
|
70
79
|
try {
|
|
71
80
|
const perms = await scopedProvider.getRolePermissions(roleId);
|
|
72
81
|
if (epoch === permissionRequestEpoch) permissionsCache[roleId] = perms;
|
|
73
|
-
} catch
|
|
74
|
-
if (epoch === permissionRequestEpoch)
|
|
82
|
+
} catch {
|
|
83
|
+
if (epoch === permissionRequestEpoch) permissionError = true;
|
|
75
84
|
} finally {
|
|
76
85
|
if (epoch === permissionRequestEpoch) loadingPermissions = false;
|
|
77
86
|
}
|
|
@@ -79,9 +88,13 @@
|
|
|
79
88
|
|
|
80
89
|
// Load permissions whenever the selected role changes
|
|
81
90
|
$effect(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
const roleId = selectedRoleCode;
|
|
92
|
+
untrack(() => {
|
|
93
|
+
permissionRequestEpoch += 1;
|
|
94
|
+
loadingPermissions = false;
|
|
95
|
+
permissionError = false;
|
|
96
|
+
if (roleId) void loadPermissionsForRole(roleId);
|
|
97
|
+
});
|
|
85
98
|
});
|
|
86
99
|
|
|
87
100
|
$effect(() => {
|
|
@@ -94,6 +107,7 @@
|
|
|
94
107
|
selectedRoleCode = '';
|
|
95
108
|
permissionsCache = {};
|
|
96
109
|
error = null;
|
|
110
|
+
permissionError = false;
|
|
97
111
|
loadingRoles = true;
|
|
98
112
|
loadingPermissions = false;
|
|
99
113
|
saving = false;
|
|
@@ -115,7 +129,8 @@
|
|
|
115
129
|
|
|
116
130
|
async function handleToggle(roleCode: string, resourceCode: string, actionCode: string, grant: boolean) {
|
|
117
131
|
const scopedProvider = authProvider;
|
|
118
|
-
if (!scopedProvider?.updateRolePermissions
|
|
132
|
+
if (!scopedProvider?.updateRolePermissions || saving || loadingRoles || loadingPermissions
|
|
133
|
+
|| permissionError || roleCode !== selectedRoleCode || !permissionsCache[roleCode]) return;
|
|
119
134
|
const mutationEpoch = ++mutationRequestEpoch;
|
|
120
135
|
|
|
121
136
|
// Copy state
|
|
@@ -141,14 +156,14 @@
|
|
|
141
156
|
if (result.success) {
|
|
142
157
|
// Success silently
|
|
143
158
|
} else {
|
|
144
|
-
throw new Error(
|
|
159
|
+
throw new Error(i18n.t('common.operationFailed'));
|
|
145
160
|
}
|
|
146
|
-
} catch
|
|
161
|
+
} catch {
|
|
147
162
|
if (mutationEpoch !== mutationRequestEpoch) return;
|
|
148
|
-
toast.error((
|
|
163
|
+
toast.error(i18n.t('common.operationFailed'));
|
|
149
164
|
// Rollback on fail
|
|
150
165
|
delete permissionsCache[roleCode]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
|
|
151
|
-
void loadPermissionsForRole(roleCode);
|
|
166
|
+
if (selectedRoleCode === roleCode) void loadPermissionsForRole(roleCode);
|
|
152
167
|
} finally {
|
|
153
168
|
if (mutationEpoch === mutationRequestEpoch) saving = false;
|
|
154
169
|
}
|
|
@@ -156,21 +171,52 @@
|
|
|
156
171
|
</script>
|
|
157
172
|
|
|
158
173
|
{#if error}
|
|
159
|
-
<div class="svadmin-u-668b21aa5409 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227">
|
|
174
|
+
<div role="alert" class="svadmin-u-668b21aa5409 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227">
|
|
160
175
|
<div class="svadmin-u-9794ab45094d svadmin-u-0478c89a150f svadmin-u-ca6bcd4b6f3f svadmin-u-f0c1e65bd6f2 svadmin-u-43928fcc832f svadmin-u-811148b13d1e svadmin-u-5f22e64f2282 svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-3960ffc248d9 svadmin-u-1004c0c3954c svadmin-u-ca6bf63030aa">
|
|
161
176
|
<AlertCircle class="svadmin-u-ed8a5df7b2fb svadmin-u-2bbcfc3b5179" />
|
|
162
177
|
<h3 class="svadmin-u-69450ef1487e svadmin-u-42536e69e639">{i18n.t('common.error') ?? 'Error'}</h3>
|
|
163
|
-
<p>{error}</p>
|
|
178
|
+
<p>{error === 'unsupported' ? i18n.t('settings.rbacNotSupported') : i18n.t('common.operationFailed')}</p>
|
|
179
|
+
{#if authProvider?.getRoles}<Button size="sm" variant="outline" onclick={() => {
|
|
180
|
+
const provider = authProvider;
|
|
181
|
+
const epoch = ++roleRequestEpoch;
|
|
182
|
+
error = null;
|
|
183
|
+
loadingRoles = true;
|
|
184
|
+
roles = [];
|
|
185
|
+
selectedRoleCode = '';
|
|
186
|
+
permissionsCache = {};
|
|
187
|
+
permissionError = false;
|
|
188
|
+
void loadRoles(provider, epoch);
|
|
189
|
+
}}>{i18n.t('common.retry')}</Button>{/if}
|
|
164
190
|
</div>
|
|
165
191
|
</div>
|
|
166
192
|
{:else}
|
|
167
|
-
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
{#if loadingRoles}
|
|
194
|
+
<DataState state="loading" loadingLabel={i18n.t('common.loading')} />
|
|
195
|
+
{/if}
|
|
196
|
+
{#snippet permissionsTableContent()}
|
|
197
|
+
{#if !permissionError}
|
|
198
|
+
<DataState state="loading" loadingLabel={i18n.t('common.loading')} />
|
|
199
|
+
{:else if permissionError}
|
|
200
|
+
<div role="alert" class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-811148b13d1e">
|
|
201
|
+
<span>{i18n.t(authProvider?.getRolePermissions ? 'common.operationFailed' : 'settings.rbacNotSupported')}</span>
|
|
202
|
+
{#if authProvider?.getRolePermissions}
|
|
203
|
+
<Button size="sm" variant="outline" onclick={() => { if (selectedRoleCode) void loadPermissionsForRole(selectedRoleCode); }}>{i18n.t('common.retry')}</Button>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
{/snippet}
|
|
208
|
+
{#if !loadingRoles}
|
|
209
|
+
<div aria-busy={saving}>
|
|
210
|
+
<PermissionMatrix
|
|
211
|
+
{roles}
|
|
212
|
+
resources={matrixResources}
|
|
213
|
+
actions={matrixActions}
|
|
214
|
+
isGranted={checkGrant}
|
|
215
|
+
bind:selectedRole={selectedRoleCode}
|
|
216
|
+
tableContent={loadingPermissions || permissionError || (selectedRoleCode && !permissionsCache[selectedRoleCode]) ? permissionsTableContent : undefined}
|
|
217
|
+
loading={loadingRoles || loadingPermissions || saving || permissionError || !permissionsCache[selectedRoleCode] || !authProvider?.updateRolePermissions}
|
|
218
|
+
onToggle={handleToggle}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
{/if}
|
|
176
222
|
{/if}
|
|
@@ -21,50 +21,53 @@
|
|
|
21
21
|
const sessionProvider = $derived(adminContext.sessionProvider);
|
|
22
22
|
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
23
23
|
|
|
24
|
-
let is2faEnabled = $state(
|
|
24
|
+
let is2faEnabled = $state<boolean | null>(null);
|
|
25
25
|
let sessions = $state.raw<SessionInfo[]>([]);
|
|
26
26
|
let loadingSessions = $state(false);
|
|
27
|
-
let sessionError = $state
|
|
27
|
+
let sessionError = $state(false);
|
|
28
28
|
let updatingMfa = $state(false);
|
|
29
29
|
let revoking = $state<string | null>(null);
|
|
30
30
|
let currentPassword = $state('');
|
|
31
31
|
let newPassword = $state('');
|
|
32
32
|
let confirmPassword = $state('');
|
|
33
33
|
let passwordErrorMessage = $state('');
|
|
34
|
+
let passwordChangedRecently = $state(false);
|
|
35
|
+
let passwordRequestFailed = $state(false);
|
|
34
36
|
let changingPassword = $state(false);
|
|
35
|
-
let
|
|
37
|
+
let scopeId = 0;
|
|
38
|
+
let securityReadId = 0;
|
|
36
39
|
let pendingDanger = $state<{ label: string; run: () => Promise<void> } | null>(null);
|
|
37
40
|
function requestDanger(label: string, run: () => Promise<void>): void {
|
|
38
|
-
const epoch =
|
|
39
|
-
pendingDanger = { label, run: async () => { if (epoch ===
|
|
41
|
+
const epoch = scopeId;
|
|
42
|
+
pendingDanger = { label, run: async () => { if (epoch === scopeId) await run(); } };
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
function
|
|
43
|
-
return
|
|
45
|
+
function failureMessage(): string {
|
|
46
|
+
return i18n.t('common.operationFailed');
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
async function loadSecurity(): Promise<void> {
|
|
47
|
-
const currentRequest = ++
|
|
50
|
+
const currentRequest = ++securityReadId;
|
|
48
51
|
const provider = sessionProvider;
|
|
49
52
|
const context = adminContext.enterpriseRequestContext;
|
|
50
53
|
sessions = [];
|
|
51
|
-
is2faEnabled =
|
|
52
|
-
sessionError =
|
|
54
|
+
is2faEnabled = null;
|
|
55
|
+
sessionError = false;
|
|
53
56
|
loadingSessions = false;
|
|
54
57
|
if (!provider) return;
|
|
55
58
|
loadingSessions = true;
|
|
56
59
|
try {
|
|
57
60
|
const [nextSessions, mfa] = await Promise.all([
|
|
58
61
|
provider.listSessions(context),
|
|
59
|
-
provider.getMfaState?.(context) ??
|
|
62
|
+
provider.getMfaState?.(context) ?? null,
|
|
60
63
|
]);
|
|
61
|
-
if (currentRequest !==
|
|
64
|
+
if (currentRequest !== securityReadId) return;
|
|
62
65
|
sessions = nextSessions;
|
|
63
|
-
is2faEnabled = mfa.enabled;
|
|
64
|
-
} catch
|
|
65
|
-
if (currentRequest ===
|
|
66
|
+
is2faEnabled = typeof mfa?.enabled === 'boolean' ? mfa.enabled : null;
|
|
67
|
+
} catch {
|
|
68
|
+
if (currentRequest === securityReadId) sessionError = true;
|
|
66
69
|
} finally {
|
|
67
|
-
if (currentRequest ===
|
|
70
|
+
if (currentRequest === securityReadId) loadingSessions = false;
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -76,41 +79,49 @@
|
|
|
76
79
|
newPassword = '';
|
|
77
80
|
confirmPassword = '';
|
|
78
81
|
passwordErrorMessage = '';
|
|
82
|
+
passwordChangedRecently = false;
|
|
83
|
+
passwordRequestFailed = false;
|
|
79
84
|
changingPassword = false;
|
|
80
85
|
updatingMfa = false;
|
|
81
86
|
revoking = null;
|
|
82
87
|
pendingDanger = null;
|
|
83
88
|
void loadSecurity();
|
|
84
|
-
return () => {
|
|
89
|
+
return () => {
|
|
90
|
+
scopeId += 1;
|
|
91
|
+
securityReadId += 1;
|
|
92
|
+
};
|
|
85
93
|
});
|
|
86
94
|
|
|
87
95
|
async function handleMfaChange(enabled: boolean, confirmed = false): Promise<void> {
|
|
96
|
+
if (is2faEnabled === null || loadingSessions || sessionError || updatingMfa) return;
|
|
88
97
|
if (!enabled && !confirmed) {
|
|
89
98
|
requestDanger(isZh ? '关闭两步验证' : 'Disable two-factor authentication', () => handleMfaChange(false, true));
|
|
90
99
|
return;
|
|
91
100
|
}
|
|
92
101
|
const provider = sessionProvider;
|
|
93
|
-
const currentRequest =
|
|
102
|
+
const currentRequest = scopeId;
|
|
94
103
|
const context = adminContext.enterpriseRequestContext;
|
|
95
104
|
if (!provider?.setMfaEnabled) return;
|
|
96
105
|
updatingMfa = true;
|
|
97
106
|
try {
|
|
98
107
|
const result = await provider.setMfaEnabled(enabled, context);
|
|
99
|
-
if (currentRequest !==
|
|
108
|
+
if (currentRequest !== scopeId) return;
|
|
100
109
|
is2faEnabled = result.enabled;
|
|
101
|
-
} catch
|
|
102
|
-
if (currentRequest !==
|
|
103
|
-
notifyWithProvider({ type: 'error', message:
|
|
110
|
+
} catch {
|
|
111
|
+
if (currentRequest !== scopeId) return;
|
|
112
|
+
notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
|
|
104
113
|
} finally {
|
|
105
|
-
if (currentRequest ===
|
|
114
|
+
if (currentRequest === scopeId) updatingMfa = false;
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
async function handlePasswordChange(event: SubmitEvent): Promise<void> {
|
|
110
119
|
event.preventDefault();
|
|
120
|
+
if (changingPassword) return;
|
|
111
121
|
passwordErrorMessage = '';
|
|
122
|
+
passwordRequestFailed = false;
|
|
112
123
|
const provider = authProvider;
|
|
113
|
-
const currentRequest =
|
|
124
|
+
const currentRequest = scopeId;
|
|
114
125
|
if (!provider?.updatePassword) return;
|
|
115
126
|
if (!currentPassword) passwordErrorMessage = i18n.t('profile.currentPassword');
|
|
116
127
|
else if (newPassword.length < 8) passwordErrorMessage = i18n.t('validation.minLength', { min: 8 });
|
|
@@ -120,63 +131,66 @@
|
|
|
120
131
|
changingPassword = true;
|
|
121
132
|
try {
|
|
122
133
|
const result = await provider.updatePassword({ currentPassword, password: newPassword, confirmPassword });
|
|
123
|
-
if (!result.success) throw new Error(
|
|
124
|
-
if (currentRequest !==
|
|
134
|
+
if (!result.success) throw new Error('Password update failed');
|
|
135
|
+
if (currentRequest !== scopeId) return;
|
|
125
136
|
notifyWithProvider({ type: 'success', message: i18n.t('profile.passwordChanged') }, adminContext.notificationProvider);
|
|
137
|
+
passwordChangedRecently = true;
|
|
126
138
|
currentPassword = '';
|
|
127
139
|
newPassword = '';
|
|
128
140
|
confirmPassword = '';
|
|
129
|
-
} catch
|
|
130
|
-
if (currentRequest !==
|
|
131
|
-
|
|
141
|
+
} catch {
|
|
142
|
+
if (currentRequest !== scopeId) return;
|
|
143
|
+
passwordRequestFailed = true;
|
|
132
144
|
} finally {
|
|
133
|
-
if (currentRequest ===
|
|
145
|
+
if (currentRequest === scopeId) changingPassword = false;
|
|
134
146
|
}
|
|
135
147
|
}
|
|
136
148
|
|
|
137
149
|
async function revokeSession(id: string, confirmed = false): Promise<void> {
|
|
150
|
+
if (revoking !== null) return;
|
|
138
151
|
if (!confirmed) {
|
|
139
152
|
requestDanger(`${i18n.t('security.revokeSession')}: ${id}`, () => revokeSession(id, true));
|
|
140
153
|
return;
|
|
141
154
|
}
|
|
142
155
|
const provider = sessionProvider;
|
|
143
|
-
const currentRequest =
|
|
156
|
+
const currentRequest = scopeId;
|
|
144
157
|
const context = adminContext.enterpriseRequestContext;
|
|
145
158
|
if (!provider) return;
|
|
146
159
|
revoking = id;
|
|
147
160
|
try {
|
|
148
161
|
const result = await provider.revokeSession(id, context);
|
|
149
|
-
if (!result.success) throw new Error(
|
|
150
|
-
if (currentRequest !==
|
|
162
|
+
if (!result.success) throw new Error('Session revocation failed');
|
|
163
|
+
if (currentRequest !== scopeId) return;
|
|
151
164
|
sessions = sessions.filter((session) => session.id !== id);
|
|
152
|
-
} catch
|
|
153
|
-
if (currentRequest !==
|
|
154
|
-
notifyWithProvider({ type: 'error', message:
|
|
165
|
+
} catch {
|
|
166
|
+
if (currentRequest !== scopeId) return;
|
|
167
|
+
notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
|
|
155
168
|
} finally {
|
|
156
|
-
if (currentRequest ===
|
|
169
|
+
if (currentRequest === scopeId) revoking = null;
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
async function revokeAllOthers(confirmed = false): Promise<void> {
|
|
174
|
+
if (revoking !== null) return;
|
|
161
175
|
if (!confirmed) {
|
|
162
176
|
requestDanger(i18n.t('security.revokeOthers'), () => revokeAllOthers(true));
|
|
163
177
|
return;
|
|
164
178
|
}
|
|
165
179
|
const provider = sessionProvider;
|
|
166
|
-
const currentRequest =
|
|
180
|
+
const currentRequest = scopeId;
|
|
167
181
|
const context = adminContext.enterpriseRequestContext;
|
|
168
182
|
if (!provider) return;
|
|
169
183
|
revoking = '*';
|
|
170
184
|
try {
|
|
171
185
|
const result = await provider.revokeOtherSessions(context);
|
|
172
|
-
if (!result.success) throw new Error(
|
|
173
|
-
if (currentRequest !==
|
|
186
|
+
if (!result.success) throw new Error('Session revocation failed');
|
|
187
|
+
if (currentRequest !== scopeId) return;
|
|
174
188
|
sessions = sessions.filter((session) => session.current);
|
|
175
|
-
} catch
|
|
176
|
-
if (currentRequest !==
|
|
177
|
-
notifyWithProvider({ type: 'error', message:
|
|
189
|
+
} catch {
|
|
190
|
+
if (currentRequest !== scopeId) return;
|
|
191
|
+
notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
|
|
178
192
|
} finally {
|
|
179
|
-
if (currentRequest ===
|
|
193
|
+
if (currentRequest === scopeId) revoking = null;
|
|
180
194
|
}
|
|
181
195
|
}
|
|
182
196
|
</script>
|
|
@@ -187,17 +201,18 @@
|
|
|
187
201
|
<FeedbackNotice tone="warning" message={isZh ? '未配置 SessionProvider。会话和 MFA 设置不会使用模拟数据。' : 'SessionProvider is not configured. Session and MFA controls do not use simulated data.'} />
|
|
188
202
|
{/if}
|
|
189
203
|
<SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
|
|
190
|
-
<SettingsFieldRow controlId={`${controlId}-mfa`} label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
|
|
191
|
-
{#snippet control()}<Switch id={`${controlId}-mfa`} bind:checked={() => is2faEnabled, (enabled) => { void handleMfaChange(enabled); }} disabled={!sessionProvider?.setMfaEnabled || updatingMfa} />{/snippet}
|
|
204
|
+
<SettingsFieldRow controlId={`${controlId}-mfa`} label={i18n.t('security.enable2fa')} description={is2faEnabled === null ? i18n.t('security.twoFactorUnknown') : (is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive'))}>
|
|
205
|
+
{#snippet control()}<Switch id={`${controlId}-mfa`} bind:checked={() => is2faEnabled === true, (enabled) => { void handleMfaChange(enabled); }} disabled={!sessionProvider?.setMfaEnabled || is2faEnabled === null || updatingMfa} />{/snippet}
|
|
192
206
|
</SettingsFieldRow>
|
|
193
207
|
</SettingsGroup>
|
|
194
208
|
<SettingsGroup title={i18n.t('profile.changePassword')} description={isZh ? '密码修改由 AuthProvider 持久化。' : 'Password changes are persisted by AuthProvider.'} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
|
|
195
209
|
{#if !authProvider?.updatePassword}<FeedbackNotice tone="warning" message={isZh ? '当前 AuthProvider 不支持修改密码。' : 'The current AuthProvider does not support password updates.'} />{/if}
|
|
196
210
|
<form onsubmit={handlePasswordChange} class="svadmin-u-6199866f612f svadmin-u-3e7ce58d64fa">
|
|
197
|
-
{#if passwordErrorMessage}<Alert.Root variant="destructive"><AlertTriangle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /><Alert.Description>{passwordErrorMessage}</Alert.Description></Alert.Root>{/if}
|
|
198
|
-
<PasswordInput id=
|
|
199
|
-
<PasswordInput id=
|
|
200
|
-
<PasswordInput id=
|
|
211
|
+
{#if passwordRequestFailed || passwordErrorMessage}<Alert.Root variant="destructive"><AlertTriangle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /><Alert.Description>{passwordRequestFailed ? failureMessage() : passwordErrorMessage}</Alert.Description></Alert.Root>{/if}
|
|
212
|
+
<PasswordInput id={`${controlId}-security-current-password`} label={i18n.t('profile.currentPassword')} bind:value={currentPassword} autocomplete="current-password" disabled={!authProvider?.updatePassword || changingPassword} />
|
|
213
|
+
<PasswordInput id={`${controlId}-security-new-password`} label={i18n.t('profile.newPassword')} bind:value={newPassword} autocomplete="new-password" showStrength disabled={!authProvider?.updatePassword || changingPassword} />
|
|
214
|
+
<PasswordInput id={`${controlId}-security-confirm-password`} label={i18n.t('auth.confirmPassword')} bind:value={confirmPassword} autocomplete="new-password" disabled={!authProvider?.updatePassword || changingPassword} />
|
|
215
|
+
{#if passwordChangedRecently}<p class="text-xs text-muted-foreground">{i18n.t('profile.passwordChangedRecently')}</p>{/if}
|
|
201
216
|
<Button type="submit" disabled={!authProvider?.updatePassword || changingPassword}>{changingPassword ? (isZh ? '更新中...' : 'Updating...') : i18n.t('profile.updatePassword')}</Button>
|
|
202
217
|
</form>
|
|
203
218
|
</SettingsGroup>
|
|
@@ -206,7 +221,7 @@
|
|
|
206
221
|
{#if loadingSessions}
|
|
207
222
|
<DataState state="loading" />
|
|
208
223
|
{:else if sessionError}
|
|
209
|
-
<DataState state="error" description={
|
|
224
|
+
<DataState state="error" description={failureMessage()} retry={loadSecurity} />
|
|
210
225
|
{:else if sessions.length === 0}
|
|
211
226
|
<DataState state="empty" title={i18n.t('security.noSessions')} description={sessionProvider ? (isZh ? 'Provider 未返回活跃会话。' : 'The provider returned no active sessions.') : (isZh ? '配置 SessionProvider 后可管理真实会话。' : 'Configure SessionProvider to manage real sessions.')} />
|
|
212
227
|
{:else}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
interface Props<T extends string = string> {
|
|
11
11
|
options: readonly SegmentedControlOption<T>[];
|
|
12
|
-
value?: T;
|
|
12
|
+
value?: T | undefined;
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
ariaLabel: string;
|
|
15
15
|
dir?: 'ltr' | 'rtl';
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function handleKeydown(event: KeyboardEvent, option: SegmentedControlOption<T>, index: number): void {
|
|
55
|
-
|
|
55
|
+
const current = event.currentTarget;
|
|
56
|
+
if (disabled || option.disabled || event.altKey || event.ctrlKey || event.metaKey
|
|
57
|
+
|| !(current instanceof HTMLButtonElement) || current.matches(':disabled')) return;
|
|
56
58
|
const rtl = getComputedStyle(root).direction === 'rtl' || dir === 'rtl';
|
|
57
59
|
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
58
60
|
event.preventDefault();
|
|
@@ -5,7 +5,7 @@ export interface SegmentedControlOption<T extends string = string> {
|
|
|
5
5
|
}
|
|
6
6
|
interface Props<T extends string = string> {
|
|
7
7
|
options: readonly SegmentedControlOption<T>[];
|
|
8
|
-
value?: T;
|
|
8
|
+
value?: T | undefined;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
ariaLabel: string;
|
|
11
11
|
dir?: 'ltr' | 'rtl';
|
|
@@ -463,7 +463,7 @@
|
|
|
463
463
|
/>
|
|
464
464
|
<div class="svadmin-u-36e579c0b41c svadmin-u-7e0b7cdf1a94">
|
|
465
465
|
<p class="svadmin-u-f283ea9bea0e svadmin-u-a14daebf7748 svadmin-u-2689f3958069 svadmin-u-a7a63217e098">{identity.name}</p>
|
|
466
|
-
<p class="svadmin-u-f283ea9bea0e svadmin-u-d058ca6de60f svadmin-u-5f1ff8fe8768">{((identity as Record<string, unknown>)['role'] || (identity as Record<string, unknown>)['roleName']) ?? '
|
|
466
|
+
<p class="svadmin-u-f283ea9bea0e svadmin-u-d058ca6de60f svadmin-u-5f1ff8fe8768">{((identity as Record<string, unknown>)['role'] || (identity as Record<string, unknown>)['roleName']) ?? i18n.t('common.user')}</p>
|
|
467
467
|
</div>
|
|
468
468
|
<button
|
|
469
469
|
class="svadmin-u-d0a52b312f7d svadmin-u-cbbf90f9a828 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-3e3534b4c5df svadmin-u-646e10356266 svadmin-u-0b48b877be2a svadmin-u-ceb69a6b0e5f"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import FieldRenderer from './FieldRenderer.svelte';
|
|
16
16
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
17
17
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
18
|
+
import { withoutArrayFieldMetadata } from './fields/array-field-metadata.js';
|
|
18
19
|
|
|
19
20
|
const i18n = useTranslation();
|
|
20
21
|
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
|
|
26
27
|
interface Props {
|
|
27
28
|
resourceName: string;
|
|
28
|
-
id?: string | number;
|
|
29
|
+
id?: string | number | undefined;
|
|
29
30
|
mode?: 'create' | 'edit';
|
|
30
31
|
steps: StepDef[];
|
|
31
32
|
onSuccess?: () => void;
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
let element = $state.raw<HTMLFormElement>();
|
|
103
104
|
let submitError = $state<string | null>(null);
|
|
104
105
|
let submission = $state.raw<object>();
|
|
106
|
+
const instanceId = $props.id();
|
|
105
107
|
const scope = $derived({
|
|
106
108
|
contract: binding.resource, id, mode, allowed, configurationValid,
|
|
107
109
|
steps: JSON.stringify(steps), provider: adminContext.providers?.[binding.dataProviderName],
|
|
@@ -110,6 +112,14 @@
|
|
|
110
112
|
session: captureAuthSession(adminContext.authProvider),
|
|
111
113
|
});
|
|
112
114
|
const rendered = $derived({ scope, step: form.steps.currentStep });
|
|
115
|
+
const authoritativeDraft = $derived({ scope, values: form.values });
|
|
116
|
+
let acceptedDraft = $state.raw<{
|
|
117
|
+
snapshot: typeof authoritativeDraft;
|
|
118
|
+
values: Record<string, unknown>;
|
|
119
|
+
}>();
|
|
120
|
+
// 仅回显本次写入已确认的 UI 身份;新的 Core 快照使缓存立即失效。
|
|
121
|
+
const renderedValues = $derived(acceptedDraft?.snapshot === authoritativeDraft
|
|
122
|
+
? acceptedDraft.values : authoritativeDraft.values);
|
|
113
123
|
let mounted = true;
|
|
114
124
|
let confirmation = $state.raw<typeof rendered>();
|
|
115
125
|
const currentRendered = (origin: typeof rendered) => mounted && origin === rendered &&
|
|
@@ -122,11 +132,35 @@
|
|
|
122
132
|
confirmation = undefined;
|
|
123
133
|
});
|
|
124
134
|
$effect(() => () => { mounted = false; submission = undefined; confirmation = undefined; });
|
|
125
|
-
function errorId(field: string): string { return `${resourceName}-step-${field}-error`; }
|
|
135
|
+
function errorId(field: string): string { return `${instanceId}-${resourceName}-step-${field}-error`; }
|
|
136
|
+
const focusableFieldSelector = [
|
|
137
|
+
'input:not([type="hidden"])', 'textarea', 'select', 'button', '[tabindex]',
|
|
138
|
+
].join(', ');
|
|
139
|
+
function insideDisabledFieldset(candidate: HTMLElement): boolean {
|
|
140
|
+
for (let parent = candidate.parentElement; parent; parent = parent.parentElement) {
|
|
141
|
+
if (!(parent instanceof HTMLFieldSetElement) || !parent.disabled) continue;
|
|
142
|
+
const legend = Array.from(parent.children).find(child => child instanceof HTMLLegendElement);
|
|
143
|
+
if (!legend?.contains(candidate)) return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
function focusFirstActionableControl(field: HTMLElement): boolean {
|
|
148
|
+
for (const candidate of field.querySelectorAll<HTMLElement>(focusableFieldSelector)) {
|
|
149
|
+
if (candidate.matches(':disabled, [disabled], input[type="hidden"]')) continue;
|
|
150
|
+
if (candidate.closest('[hidden], [inert], [aria-disabled="true"]')) continue;
|
|
151
|
+
if (insideDisabledFieldset(candidate)) continue;
|
|
152
|
+
candidate.focus();
|
|
153
|
+
if (document.activeElement === candidate) return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
126
157
|
async function focusInvalid(origin: typeof scope) {
|
|
127
158
|
await tick();
|
|
128
|
-
if (mounted
|
|
129
|
-
|
|
159
|
+
if (!mounted || scope !== origin || !origin.session.isCurrent() || !element?.isConnected) return;
|
|
160
|
+
// 嵌套数组的子字段键不属于当前步骤的顶层错误。
|
|
161
|
+
for (const field of element.querySelectorAll<HTMLElement>(':scope > [data-svadmin-field]')) {
|
|
162
|
+
const key = field.getAttribute('data-svadmin-field-key');
|
|
163
|
+
if (key && form.errors[key] && focusFirstActionableControl(field)) return;
|
|
130
164
|
}
|
|
131
165
|
}
|
|
132
166
|
const changeStep = $derived.by(() => {
|
|
@@ -140,7 +174,15 @@
|
|
|
140
174
|
const fieldChange = $derived.by(() => {
|
|
141
175
|
const origin = rendered;
|
|
142
176
|
return (key: string) => (value: unknown) => {
|
|
143
|
-
if (currentRendered(origin))
|
|
177
|
+
if (!currentRendered(origin)) return;
|
|
178
|
+
const previous = renderedValues;
|
|
179
|
+
form.setFieldValue(key, withoutArrayFieldMetadata(value));
|
|
180
|
+
if (currentRendered(origin)) {
|
|
181
|
+
acceptedDraft = {
|
|
182
|
+
snapshot: authoritativeDraft,
|
|
183
|
+
values: { ...previous, [key]: value },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
144
186
|
};
|
|
145
187
|
});
|
|
146
188
|
function leave(origin: typeof rendered) {
|
|
@@ -176,6 +218,16 @@
|
|
|
176
218
|
};
|
|
177
219
|
});
|
|
178
220
|
|
|
221
|
+
// 显式 reset 入口:宿主通过 bind:this 调用,恢复 Core 草稿、步骤导航和本实例
|
|
222
|
+
// 提交提示;不触发导航,也不改变未授权状态。
|
|
223
|
+
export function reset(): void {
|
|
224
|
+
if (!allowed) return;
|
|
225
|
+
submitError = null;
|
|
226
|
+
submission = undefined;
|
|
227
|
+
confirmation = undefined;
|
|
228
|
+
form.reset();
|
|
229
|
+
}
|
|
230
|
+
|
|
179
231
|
const handleSubmit = $derived.by(() => {
|
|
180
232
|
const origin = rendered;
|
|
181
233
|
return async () => {
|
|
@@ -267,7 +319,7 @@
|
|
|
267
319
|
{#each currentFields as field (field.key)}
|
|
268
320
|
<FieldRenderer
|
|
269
321
|
{field}
|
|
270
|
-
value={
|
|
322
|
+
value={renderedValues[field.key]}
|
|
271
323
|
onchange={fieldChange(field.key)}
|
|
272
324
|
invalid={!!form.errors[field.key]}
|
|
273
325
|
{...definedOptions({ errorId: form.errors[field.key] ? errorId(field.key) : undefined })}
|
|
@@ -306,7 +358,5 @@
|
|
|
306
358
|
{/if}
|
|
307
359
|
</div>
|
|
308
360
|
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
onconfirm={confirmLeave} oncancel={cancelLeave} />
|
|
312
|
-
{/key}
|
|
361
|
+
<ConfirmDialog open={confirmOpen} message={i18n.t('common.unsavedChanges')}
|
|
362
|
+
onconfirm={confirmLeave} oncancel={cancelLeave} />
|