@svadmin/ui 0.88.1 → 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
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
let { headingLevel = 'h1' }: { headingLevel?: 'h1' | 'h2' } = $props();
|
|
20
20
|
const i18n = useTranslation();
|
|
21
21
|
const adminContext = captureAdminContext();
|
|
22
|
+
const controlId = $props.id();
|
|
22
23
|
|
|
23
24
|
const identity = useGetIdentity();
|
|
24
25
|
const notification = useNotification();
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
let profileSaving = $state(false);
|
|
34
35
|
let profileError = $state('');
|
|
35
36
|
let avatarUploading = $state(false);
|
|
37
|
+
let avatarError = $state('');
|
|
36
38
|
let fileInput = $state<HTMLInputElement>();
|
|
37
39
|
let currentPassword = $state('');
|
|
38
40
|
let newPassword = $state('');
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
profileSaving = false;
|
|
47
49
|
profileError = '';
|
|
48
50
|
avatarUploading = false;
|
|
51
|
+
avatarError = '';
|
|
49
52
|
currentPassword = '';
|
|
50
53
|
newPassword = '';
|
|
51
54
|
confirmPassword = '';
|
|
@@ -62,6 +65,10 @@
|
|
|
62
65
|
&& scopedTenantIdentity === tenantIdentity;
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
function failureMessage(): string {
|
|
69
|
+
return i18n.t('common.operationFailed');
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
$effect(() => {
|
|
66
73
|
void authProvider;
|
|
67
74
|
void tenantIdentity;
|
|
@@ -93,11 +100,11 @@
|
|
|
93
100
|
identity.refetch();
|
|
94
101
|
notification.success(i18n.t('common.updateSuccess'), 3000);
|
|
95
102
|
} else {
|
|
96
|
-
profileError =
|
|
103
|
+
profileError = failureMessage();
|
|
97
104
|
}
|
|
98
|
-
} catch
|
|
105
|
+
} catch {
|
|
99
106
|
if (isActiveScope(epoch, provider, scopedTenantIdentity)) {
|
|
100
|
-
profileError =
|
|
107
|
+
profileError = failureMessage();
|
|
101
108
|
}
|
|
102
109
|
} finally {
|
|
103
110
|
if (isActiveScope(epoch, provider, scopedTenantIdentity)) profileSaving = false;
|
|
@@ -105,15 +112,22 @@
|
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
async function handleAvatarChange(e: Event) {
|
|
108
|
-
const
|
|
115
|
+
const input = e.target as HTMLInputElement;
|
|
116
|
+
const file = input.files?.[0];
|
|
109
117
|
const provider = authProvider;
|
|
110
|
-
|
|
118
|
+
input.value = '';
|
|
119
|
+
if (!file || avatarUploading || !provider?.updateProfile) return;
|
|
111
120
|
const epoch = scopeEpoch;
|
|
112
121
|
const scopedTenantIdentity = tenantIdentity;
|
|
113
122
|
avatarUploading = true;
|
|
123
|
+
avatarError = '';
|
|
114
124
|
try {
|
|
115
125
|
const result = await provider.updateProfile({ avatar: file });
|
|
116
|
-
if (isActiveScope(epoch, provider, scopedTenantIdentity)
|
|
126
|
+
if (!isActiveScope(epoch, provider, scopedTenantIdentity)) return;
|
|
127
|
+
if (result.success) identity.refetch();
|
|
128
|
+
else avatarError = failureMessage();
|
|
129
|
+
} catch {
|
|
130
|
+
if (isActiveScope(epoch, provider, scopedTenantIdentity)) avatarError = failureMessage();
|
|
117
131
|
} finally {
|
|
118
132
|
if (isActiveScope(epoch, provider, scopedTenantIdentity)) avatarUploading = false;
|
|
119
133
|
}
|
|
@@ -140,7 +154,7 @@
|
|
|
140
154
|
newPassword = '';
|
|
141
155
|
confirmPassword = '';
|
|
142
156
|
} else {
|
|
143
|
-
pwError =
|
|
157
|
+
pwError = failureMessage();
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
|
|
@@ -168,6 +182,14 @@
|
|
|
168
182
|
<Loader2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-afbdd13a380e" />
|
|
169
183
|
{i18n.t('common.loading')}
|
|
170
184
|
</div>
|
|
185
|
+
{:else if identity.error}
|
|
186
|
+
<Alert.Root variant="destructive">
|
|
187
|
+
<AlertCircle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
188
|
+
<Alert.Description class="svadmin-u-60fbb7713999 svadmin-u-77a2a20e90d4">
|
|
189
|
+
<span>{failureMessage()}</span>
|
|
190
|
+
<Button size="sm" variant="outline" onclick={() => { void identity.refetch(); }}>{i18n.t('common.retry')}</Button>
|
|
191
|
+
</Alert.Description>
|
|
192
|
+
</Alert.Root>
|
|
171
193
|
{:else if identity.data}
|
|
172
194
|
<div class="svadmin-u-60fbb7713999 svadmin-u-60541e1e26f8 svadmin-u-0d304f904cb0">
|
|
173
195
|
<!-- Avatar -->
|
|
@@ -188,6 +210,8 @@
|
|
|
188
210
|
class="svadmin-u-da4dbfbc4fdc svadmin-u-7b7df0449b80 svadmin-u-5f22e64f2282 svadmin-u-53bb3a280599 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-7065497e1ca0 svadmin-u-181f3d6c9821 svadmin-u-67d6184a0024 svadmin-u-34516836730d"
|
|
189
211
|
onclick={() => fileInput?.click()}
|
|
190
212
|
disabled={avatarUploading}
|
|
213
|
+
aria-label={i18n.t('account.uploadAvatar')}
|
|
214
|
+
title={i18n.t('account.uploadAvatar')}
|
|
191
215
|
>
|
|
192
216
|
{#if avatarUploading}
|
|
193
217
|
<Loader2 class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e svadmin-u-72a4c7cdeef7 svadmin-u-afbdd13a380e" />
|
|
@@ -207,11 +231,12 @@
|
|
|
207
231
|
|
|
208
232
|
<!-- Info -->
|
|
209
233
|
<div class="svadmin-u-6ed543e2fbbb svadmin-u-36e579c0b41c svadmin-u-7e0b7cdf1a94">
|
|
234
|
+
{#if avatarError}<p role="alert">{avatarError}</p>{/if}
|
|
210
235
|
{#if editingProfile}
|
|
211
236
|
<div class="svadmin-u-6ed543e2fbbb svadmin-u-2472e9b81a97">
|
|
212
237
|
<div>
|
|
213
|
-
<Label for=
|
|
214
|
-
<Input id=
|
|
238
|
+
<Label for={`${controlId}-edit-name`} class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">{i18n.t('profile.name')}</Label>
|
|
239
|
+
<Input id={`${controlId}-edit-name`} bind:value={editName} class="svadmin-u-b6b02c0ebef6" />
|
|
215
240
|
</div>
|
|
216
241
|
{#if profileError}
|
|
217
242
|
<p class="svadmin-u-359090c2d529 svadmin-u-811148b13d1e">{profileError}</p>
|
|
@@ -281,22 +306,22 @@
|
|
|
281
306
|
{/if}
|
|
282
307
|
|
|
283
308
|
<PasswordInput
|
|
284
|
-
id=
|
|
309
|
+
id={`${controlId}-current-password`}
|
|
285
310
|
label={i18n.t('profile.currentPassword')}
|
|
286
311
|
bind:value={currentPassword}
|
|
287
312
|
autocomplete="current-password"
|
|
288
313
|
/>
|
|
289
314
|
|
|
290
315
|
<PasswordInput
|
|
291
|
-
id=
|
|
292
|
-
label={i18n.t('
|
|
316
|
+
id={`${controlId}-new-password`}
|
|
317
|
+
label={i18n.t('profile.newPassword')}
|
|
293
318
|
bind:value={newPassword}
|
|
294
319
|
autocomplete="new-password"
|
|
295
320
|
showStrength
|
|
296
321
|
/>
|
|
297
322
|
|
|
298
323
|
<PasswordInput
|
|
299
|
-
id=
|
|
324
|
+
id={`${controlId}-confirm-new-password`}
|
|
300
325
|
label={i18n.t('auth.confirmPassword')}
|
|
301
326
|
bind:value={confirmPassword}
|
|
302
327
|
autocomplete="new-password"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends string">
|
|
2
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
2
3
|
import { cn } from '../utils.js';
|
|
3
4
|
|
|
4
5
|
export interface RadioGroupOption<T extends string = string> {
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
interface Props<T extends string = string> {
|
|
12
13
|
options: readonly RadioGroupOption<T>[];
|
|
13
14
|
value?: T | null;
|
|
14
|
-
name?: string;
|
|
15
|
+
name?: string | undefined;
|
|
15
16
|
legend?: string;
|
|
16
17
|
ariaLabel?: string;
|
|
17
18
|
disabled?: boolean;
|
|
@@ -35,18 +36,214 @@
|
|
|
35
36
|
class: className = '',
|
|
36
37
|
onchange,
|
|
37
38
|
}: Props<T> = $props();
|
|
39
|
+
const instanceId = $props.id();
|
|
40
|
+
const i18n = useTranslation();
|
|
41
|
+
let groupRoot: HTMLFieldSetElement;
|
|
42
|
+
// 原生 reset 恢复的是初始选中值,而不是最后一次用户选择。值一旦变化就
|
|
43
|
+
// 无法再推断初始状态,因此只在挂载后的首个值快照一次;初始 null 表示
|
|
44
|
+
// 恢复为未选。
|
|
45
|
+
let resetValue: T | null = null;
|
|
46
|
+
let resetCaptured = false;
|
|
47
|
+
let pendingNativeReset: { event: Event; form: HTMLFormElement } | undefined;
|
|
48
|
+
const enabledOptions = $derived(options.filter(option => !option.disabled));
|
|
49
|
+
const tabValue = $derived(enabledOptions.find(option => option.value === value)?.value ?? enabledOptions[0]?.value);
|
|
50
|
+
|
|
51
|
+
function syncChecked(): void {
|
|
52
|
+
if (!groupRoot) return;
|
|
53
|
+
for (const input of groupRoot.querySelectorAll<HTMLInputElement>('input[type="radio"]')) {
|
|
54
|
+
input.checked = input.value === value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function flushNativeReset(): void {
|
|
59
|
+
const reset = pendingNativeReset;
|
|
60
|
+
if (!reset || reset.event.eventPhase !== Event.NONE) return;
|
|
61
|
+
pendingNativeReset = undefined;
|
|
62
|
+
if (groupRoot?.form !== reset.form) return;
|
|
63
|
+
// 与 FileUpload 保持同一原生 reset 约定:先等事件分发结束,再读取
|
|
64
|
+
// defaultPrevented。被取消时保留当前值,并修复 happy-dom 在取消前
|
|
65
|
+
// 已清空 checked 的模拟差异;真实浏览器此处是无副作用的重复赋值。
|
|
66
|
+
if (reset.event.defaultPrevented) {
|
|
67
|
+
syncChecked();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
value = options.some(option => option.value === resetValue) ? resetValue : null;
|
|
71
|
+
syncChecked();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$effect(() => {
|
|
75
|
+
const form = groupRoot?.form;
|
|
76
|
+
if (!form) return;
|
|
77
|
+
const reset = (event: Event) => {
|
|
78
|
+
flushNativeReset();
|
|
79
|
+
pendingNativeReset = { event, form };
|
|
80
|
+
// reset 事件先于默认动作;等默认动作完成后才能读取最终取消状态。
|
|
81
|
+
setTimeout(flushNativeReset, 0);
|
|
82
|
+
};
|
|
83
|
+
form.addEventListener('reset', reset);
|
|
84
|
+
return () => {
|
|
85
|
+
form.removeEventListener('reset', reset);
|
|
86
|
+
if (pendingNativeReset?.form === form) pendingNativeReset = undefined;
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Svelte 的 checked 只写 DOM property,不会留下 checked 内容属性,因此
|
|
91
|
+
// 浏览器 reset 默认会清空全部选中项。显式同步 defaultChecked,让原生
|
|
92
|
+
// 默认动作与上面缓存的 resetValue 保持一致;后续用户选择不改变该基线。
|
|
93
|
+
$effect(() => {
|
|
94
|
+
const current = value;
|
|
95
|
+
void options;
|
|
96
|
+
if (!groupRoot) return;
|
|
97
|
+
if (!resetCaptured) {
|
|
98
|
+
resetCaptured = true;
|
|
99
|
+
resetValue = current;
|
|
100
|
+
}
|
|
101
|
+
for (const input of groupRoot.querySelectorAll<HTMLInputElement>('input[type="radio"]')) {
|
|
102
|
+
input.defaultChecked = resetValue !== null && input.value === resetValue;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 空 name 的原生 radio 在同一表单内仍属于同一个浏览器分组,不能靠逐个
|
|
107
|
+
// required 隔离实例。未命名组改用组件内的 custom validity,且不生成字段名。
|
|
108
|
+
// 每次重新同步所有输入,避免切换 name/required/disabled、替换选项或父级
|
|
109
|
+
// 清空后残留旧错误;提示复用现有 i18n 的 validation.required。
|
|
110
|
+
$effect(() => {
|
|
111
|
+
// 先读取全部依赖再分支,避免某次提前返回后漏跟踪 required/disabled/
|
|
112
|
+
// options/value,导致后续状态变化不再清除或重建 custom validity。
|
|
113
|
+
const currentName = name;
|
|
114
|
+
const isRequired = required;
|
|
115
|
+
const isDisabled = disabled;
|
|
116
|
+
const currentValue = value;
|
|
117
|
+
const currentOptions = options;
|
|
118
|
+
const message = i18n.t('validation.required');
|
|
119
|
+
const inputs = groupRoot
|
|
120
|
+
? [...groupRoot.querySelectorAll<HTMLInputElement>('input[type="radio"]')]
|
|
121
|
+
: [];
|
|
122
|
+
const hasSelection = currentOptions.some(option => !option.disabled && option.value === currentValue);
|
|
123
|
+
const firstEnabledIndex = currentOptions.findIndex(option => !option.disabled);
|
|
124
|
+
for (const input of inputs) input.setCustomValidity('');
|
|
125
|
+
if (!groupRoot || currentName || !isRequired || isDisabled || hasSelection) return;
|
|
126
|
+
if (firstEnabledIndex >= 0) inputs[firstEnabledIndex]?.setCustomValidity(message);
|
|
127
|
+
});
|
|
38
128
|
|
|
39
129
|
function select(option: RadioGroupOption<T>): void {
|
|
40
130
|
if (disabled || option.disabled) return;
|
|
131
|
+
// reset 的最终状态要等事件分发结束才能判定;若同一任务内用户已经重新
|
|
132
|
+
// 选择,则最后一次选择优先,作废尚未冲刷的 pending reset。即使新选择
|
|
133
|
+
// 与 reset 前的值相同,也必须先作废并同步 DOM,否则延迟冲刷会用初始值
|
|
134
|
+
// 覆盖它,reset 的默认动作也可能留下与绑定值不一致的 checked。
|
|
135
|
+
const hadPendingReset = pendingNativeReset !== undefined;
|
|
136
|
+
pendingNativeReset = undefined;
|
|
41
137
|
if (value !== option.value) {
|
|
42
138
|
value = option.value;
|
|
43
139
|
onchange?.(option.value);
|
|
44
140
|
}
|
|
141
|
+
if (hadPendingReset) syncChecked();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const focusableSelector = [
|
|
145
|
+
'a[href]',
|
|
146
|
+
'button',
|
|
147
|
+
'input',
|
|
148
|
+
'select',
|
|
149
|
+
'textarea',
|
|
150
|
+
'summary',
|
|
151
|
+
'[tabindex]',
|
|
152
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
153
|
+
].join(',');
|
|
154
|
+
|
|
155
|
+
function isContentEditableHost(element: HTMLElement): boolean {
|
|
156
|
+
const attribute = element.getAttribute('contenteditable');
|
|
157
|
+
const editable = element.isContentEditable || (attribute !== null && attribute.toLowerCase() !== 'false');
|
|
158
|
+
return editable && !element.parentElement?.isContentEditable;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 与现有 UI focus helper 保持同一判定:排除原生禁用、inert/隐藏区域和
|
|
162
|
+
// 不可见元素,并把 contenteditable 宿主视为真实 Tab 停靠点。aria-disabled
|
|
163
|
+
// 和 aria-hidden 不等于原生不可聚焦,不能据此跳过宿主真实的 Tab 停靠点。
|
|
164
|
+
function isTabbable(element: HTMLElement): boolean {
|
|
165
|
+
if (element.matches(':disabled')) return false;
|
|
166
|
+
if (element.closest('[hidden], [inert]')) return false;
|
|
167
|
+
if (element.hasAttribute('tabindex') && element.tabIndex < 0) return false;
|
|
168
|
+
if (!element.hasAttribute('tabindex') && !isContentEditableHost(element) && element.tabIndex < 0) return false;
|
|
169
|
+
return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 只有非空 name 才参与原生命名分组;name="" 与省略 name 语义一致。
|
|
173
|
+
function hasNonEmptyName(input: HTMLInputElement): boolean {
|
|
174
|
+
return input.name !== '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function tabbableSequence(form: HTMLFormElement): HTMLElement[] {
|
|
178
|
+
// 只取零序 Tab 停靠点。正 tabindex 元素已在文档起点先访问,不能当作
|
|
179
|
+
// 当前组之后的下一个停靠点;contenteditable 宿主即使 tabIndex 报告 -1
|
|
180
|
+
// 也仍在浏览器 Tab 顺序中。范围收敛到当前表单,不重写整页焦点顺序。
|
|
181
|
+
return [...form.querySelectorAll<HTMLElement>(focusableSelector)]
|
|
182
|
+
.filter(element => (element.tabIndex === 0
|
|
183
|
+
|| (!element.hasAttribute('tabindex') && isContentEditableHost(element)))
|
|
184
|
+
&& isTabbable(element));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleGroupTab(event: KeyboardEvent): void {
|
|
188
|
+
// WebKit 会把同一表单内空 name 的 radio 视为同一原生组,并在 Tab 时
|
|
189
|
+
// 跳过后续未命名 RadioGroup。只有同表单存在多个未命名实例时才接管,
|
|
190
|
+
// 且只接管确实落在本组件未命名组上的目标;普通原生 radio、contenteditable、
|
|
191
|
+
// inert 边界交还浏览器。先聚焦成功再取消默认行为,避免半途困住键盘。
|
|
192
|
+
const form = groupRoot.form ?? groupRoot.closest('form');
|
|
193
|
+
const current = document.activeElement;
|
|
194
|
+
if (!form || !(current instanceof HTMLInputElement) || !groupRoot.contains(current)) return;
|
|
195
|
+
const unnamedGroups = [...form.querySelectorAll<HTMLElement>('[data-svadmin-radio-group]')]
|
|
196
|
+
.filter(group => ![...group.querySelectorAll<HTMLInputElement>('input[type="radio"]')].some(hasNonEmptyName));
|
|
197
|
+
if (unnamedGroups.length < 2) return;
|
|
198
|
+
const sequence = tabbableSequence(form);
|
|
199
|
+
const contained = sequence.filter(element => groupRoot.contains(element));
|
|
200
|
+
if (!contained.length) return;
|
|
201
|
+
const firstContained = contained[0];
|
|
202
|
+
const lastContained = contained[contained.length - 1];
|
|
203
|
+
if (!firstContained || !lastContained) return;
|
|
204
|
+
const first = sequence.indexOf(firstContained);
|
|
205
|
+
const last = sequence.indexOf(lastContained);
|
|
206
|
+
if (first < 0 || last < 0) return;
|
|
207
|
+
const target = event.shiftKey ? sequence[first - 1] : sequence[last + 1];
|
|
208
|
+
if (!(target instanceof HTMLInputElement) || target.type !== 'radio') return;
|
|
209
|
+
const targetGroup = target.closest<HTMLElement>('[data-svadmin-radio-group]');
|
|
210
|
+
if (!targetGroup || targetGroup === groupRoot) return;
|
|
211
|
+
if ([...targetGroup.querySelectorAll<HTMLInputElement>('input[type="radio"]')].some(hasNonEmptyName)) return;
|
|
212
|
+
// 保留平台默认的滚动行为,让窄屏上的键盘焦点进入视口。
|
|
213
|
+
target.focus();
|
|
214
|
+
if (document.activeElement === target) event.preventDefault();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
218
|
+
const current = event.currentTarget;
|
|
219
|
+
if (name || disabled || event.altKey || event.ctrlKey || event.metaKey
|
|
220
|
+
|| !(current instanceof HTMLInputElement) || current.matches(':disabled')) return;
|
|
221
|
+
if (event.key === 'Tab') {
|
|
222
|
+
handleGroupTab(event);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!['ArrowRight', 'ArrowDown', 'ArrowLeft', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
226
|
+
|
|
227
|
+
// 未命名 radio 的原生方向键可能跨越 fieldset;只在当前组件内选择。
|
|
228
|
+
const inputs = [...groupRoot.querySelectorAll<HTMLInputElement>('input[type="radio"]')]
|
|
229
|
+
.filter(input => !input.matches(':disabled'));
|
|
230
|
+
const index = inputs.indexOf(current);
|
|
231
|
+
if (index < 0) return;
|
|
232
|
+
const rtl = getComputedStyle(groupRoot).direction === 'rtl';
|
|
233
|
+
const forward = event.key === 'ArrowDown' || event.key === (rtl ? 'ArrowLeft' : 'ArrowRight');
|
|
234
|
+
const nextIndex = event.key === 'Home' ? 0 : event.key === 'End' ? inputs.length - 1
|
|
235
|
+
: (index + (forward ? 1 : -1) + inputs.length) % inputs.length;
|
|
236
|
+
const next = inputs[nextIndex];
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
next?.focus();
|
|
239
|
+
next?.click();
|
|
45
240
|
}
|
|
46
241
|
</script>
|
|
47
242
|
|
|
48
243
|
<fieldset
|
|
244
|
+
bind:this={groupRoot}
|
|
49
245
|
class={cn('svadmin-radio-group', className)}
|
|
246
|
+
data-svadmin-radio-group
|
|
50
247
|
disabled={disabled}
|
|
51
248
|
data-invalid={invalid ? 'true' : undefined}
|
|
52
249
|
aria-describedby={describedby}
|
|
@@ -54,8 +251,8 @@
|
|
|
54
251
|
>
|
|
55
252
|
{#if legend}<legend>{legend}</legend>{/if}
|
|
56
253
|
<div class="svadmin-radio-group__options">
|
|
57
|
-
{#each options as option (option.value)}
|
|
58
|
-
{@const inputId = `${
|
|
254
|
+
{#each options as option, index (option.value)}
|
|
255
|
+
{@const inputId = `${instanceId}-radio-${index}`}
|
|
59
256
|
<label class="svadmin-radio-group__option" class:svadmin-radio-group__option--disabled={disabled || option.disabled} for={inputId}>
|
|
60
257
|
<input
|
|
61
258
|
id={inputId}
|
|
@@ -64,8 +261,10 @@
|
|
|
64
261
|
value={option.value}
|
|
65
262
|
checked={value === option.value}
|
|
66
263
|
disabled={disabled || option.disabled}
|
|
67
|
-
required={required}
|
|
264
|
+
required={required && Boolean(name)}
|
|
68
265
|
aria-describedby={describedby}
|
|
266
|
+
tabindex={name ? undefined : !disabled && !option.disabled && option.value === tabValue ? 0 : -1}
|
|
267
|
+
onkeydown={handleKeydown}
|
|
69
268
|
onchange={() => select(option)}
|
|
70
269
|
/>
|
|
71
270
|
<span class="svadmin-radio-group__copy">
|
|
@@ -7,7 +7,7 @@ export interface RadioGroupOption<T extends string = string> {
|
|
|
7
7
|
interface Props<T extends string = string> {
|
|
8
8
|
options: readonly RadioGroupOption<T>[];
|
|
9
9
|
value?: T | null;
|
|
10
|
-
name?: string;
|
|
10
|
+
name?: string | undefined;
|
|
11
11
|
legend?: string;
|
|
12
12
|
ariaLabel?: string;
|
|
13
13
|
disabled?: boolean;
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
value?: number;
|
|
7
7
|
count?: number;
|
|
8
8
|
allowClear?: boolean;
|
|
9
|
-
name?: string;
|
|
9
|
+
name?: string | undefined;
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
required?: boolean;
|
|
12
12
|
invalid?: boolean;
|
|
13
|
-
describedby?: string;
|
|
13
|
+
describedby?: string | undefined;
|
|
14
14
|
ariaLabel?: string;
|
|
15
15
|
class?: string;
|
|
16
16
|
onchange?: (value: number) => void;
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
aria-describedby={describedby}
|
|
67
67
|
data-invalid={invalid ? 'true' : undefined}
|
|
68
68
|
>
|
|
69
|
-
{#if name}<input type="hidden" {name} value={normalizedValue} required={required} />{/if}
|
|
69
|
+
{#if name}<input type="hidden" {name} value={normalizedValue} required={required} {disabled} />{/if}
|
|
70
70
|
{#each Array(normalizedCount) as _, index (index)}
|
|
71
71
|
{@const rating = index + 1}
|
|
72
72
|
<button
|
|
@@ -2,11 +2,11 @@ interface Props {
|
|
|
2
2
|
value?: number;
|
|
3
3
|
count?: number;
|
|
4
4
|
allowClear?: boolean;
|
|
5
|
-
name?: string;
|
|
5
|
+
name?: string | undefined;
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
required?: boolean;
|
|
8
8
|
invalid?: boolean;
|
|
9
|
-
describedby?: string;
|
|
9
|
+
describedby?: string | undefined;
|
|
10
10
|
ariaLabel?: string;
|
|
11
11
|
class?: string;
|
|
12
12
|
onchange?: (value: number) => void;
|
|
@@ -6,7 +6,7 @@ type $$ComponentProps = {
|
|
|
6
6
|
open?: boolean;
|
|
7
7
|
recordId?: string | number;
|
|
8
8
|
onClose?: () => void;
|
|
9
|
-
extraSections?: Snippet;
|
|
9
|
+
extraSections?: Snippet | undefined;
|
|
10
10
|
};
|
|
11
11
|
declare const RecordDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
12
12
|
type RecordDetailDrawer = ReturnType<typeof RecordDetailDrawer>;
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
const scope = $derived({
|
|
70
70
|
resource,
|
|
71
71
|
contract: binding.resource,
|
|
72
|
+
providerName: binding.dataProviderName,
|
|
72
73
|
provider: context.providers?.[binding.dataProviderName],
|
|
73
74
|
meta: JSON.stringify(binding.meta),
|
|
74
75
|
access: context.accessControlProvider,
|
|
@@ -80,6 +81,24 @@
|
|
|
80
81
|
filters,
|
|
81
82
|
sorters,
|
|
82
83
|
});
|
|
84
|
+
function sameScope(left: typeof scope | undefined, right: typeof scope | undefined): boolean {
|
|
85
|
+
if (!left || !right) return left === right;
|
|
86
|
+
return left.resource === right.resource
|
|
87
|
+
&& left.contract === right.contract
|
|
88
|
+
&& left.providerName === right.providerName
|
|
89
|
+
&& left.provider === right.provider
|
|
90
|
+
&& left.meta === right.meta
|
|
91
|
+
&& left.access === right.access
|
|
92
|
+
&& left.authProvider === right.authProvider
|
|
93
|
+
&& left.tenant === right.tenant
|
|
94
|
+
&& left.auth === right.auth
|
|
95
|
+
&& left.optionLabel === right.optionLabel
|
|
96
|
+
&& left.normalizedFetchSize === right.normalizedFetchSize
|
|
97
|
+
&& left.filters === right.filters
|
|
98
|
+
&& left.sorters === right.sorters;
|
|
99
|
+
}
|
|
100
|
+
let observedScope = $state.raw(untrack(() => scope));
|
|
101
|
+
let observedSearchable = $state(untrack(() => searchable));
|
|
83
102
|
let valueInvalidated = $state(false);
|
|
84
103
|
const selectedValues = $derived(allowed && !valueInvalidated ? normalizedValues(value) : []);
|
|
85
104
|
const select = useSelect(definedReactiveOptions({
|
|
@@ -98,12 +117,14 @@
|
|
|
98
117
|
get sorters() { return sorters; },
|
|
99
118
|
get filters() { return [...filters, ...searchFilters]; },
|
|
100
119
|
get defaultValue() { return selectedValues; },
|
|
101
|
-
|
|
120
|
+
// 查询会先于清理 effect 重算,旧草稿不得先发送给新作用域。
|
|
121
|
+
get queryOptions() {
|
|
122
|
+
return { enabled: allowed && sameScope(observedScope, scope) && observedSearchable === searchable };
|
|
123
|
+
},
|
|
102
124
|
get projectionKey() { return `relation-picker:${String(optionLabel)}:id`; },
|
|
103
125
|
}));
|
|
104
126
|
|
|
105
127
|
let loaded = $state.raw<{ scope: typeof scope; search: string; options: RelationPickerOption[] }>();
|
|
106
|
-
let observedScope = untrack(() => scope);
|
|
107
128
|
let observedValue = untrack(() => value);
|
|
108
129
|
let observedAllowed = untrack(() => allowed);
|
|
109
130
|
let draftScope = $state.raw<typeof scope>();
|
|
@@ -123,7 +144,7 @@
|
|
|
123
144
|
const currentScope = scope;
|
|
124
145
|
const previous = untrack(() => loaded);
|
|
125
146
|
const options = new Map<string | number, RelationPickerOption>(
|
|
126
|
-
page > 1 && previous
|
|
147
|
+
page > 1 && previous && sameScope(previous.scope, currentScope) && previous.search === appliedSearch
|
|
127
148
|
? previous.options.map(option => [option.value, option]) : [],
|
|
128
149
|
);
|
|
129
150
|
for (const option of data.options) options.set(option.value, option);
|
|
@@ -133,7 +154,7 @@
|
|
|
133
154
|
const availableOptions = $derived.by(() => {
|
|
134
155
|
if (!allowed || select.isError) return [];
|
|
135
156
|
const options = new Map<string | number, RelationPickerOption>();
|
|
136
|
-
if (loaded
|
|
157
|
+
if (loaded && sameScope(loaded.scope, scope) && loaded.search === appliedSearch) {
|
|
137
158
|
for (const option of loaded.options) options.set(option.value, option);
|
|
138
159
|
}
|
|
139
160
|
for (const option of select.options) options.set(option.value, option);
|
|
@@ -152,7 +173,7 @@
|
|
|
152
173
|
}
|
|
153
174
|
|
|
154
175
|
function toggle(item: string | number): void {
|
|
155
|
-
if (disabled || !allowed || !open || draftScope
|
|
176
|
+
if (disabled || !allowed || !open || !sameScope(draftScope, scope) || select.isError
|
|
156
177
|
|| select.isFetching || searchText !== appliedSearch) return;
|
|
157
178
|
if (!multiple) {
|
|
158
179
|
draftValues = [item];
|
|
@@ -165,16 +186,17 @@
|
|
|
165
186
|
}
|
|
166
187
|
|
|
167
188
|
function commit(): void {
|
|
168
|
-
if (disabled || !allowed || !open || draftScope
|
|
189
|
+
if (disabled || !allowed || !open || !sameScope(draftScope, scope) || select.isError
|
|
169
190
|
|| select.isFetching || searchText !== appliedSearch) return;
|
|
170
191
|
const next = [...draftValues];
|
|
171
192
|
onchange?.(multiple ? next : next[0] ?? null);
|
|
172
|
-
|
|
173
|
-
resetSearch();
|
|
193
|
+
cancel();
|
|
174
194
|
}
|
|
175
195
|
|
|
176
196
|
function cancel(): void {
|
|
177
197
|
open = false;
|
|
198
|
+
draftValues = [];
|
|
199
|
+
draftScope = undefined;
|
|
178
200
|
resetSearch();
|
|
179
201
|
}
|
|
180
202
|
|
|
@@ -198,7 +220,7 @@
|
|
|
198
220
|
clearTimeout(timer);
|
|
199
221
|
const origin = scope;
|
|
200
222
|
timer = setTimeout(() => {
|
|
201
|
-
if (origin
|
|
223
|
+
if (!sameScope(origin, scope) || disabled || !allowed || !open) return;
|
|
202
224
|
appliedSearch = searchText;
|
|
203
225
|
page = 1;
|
|
204
226
|
loaded = undefined;
|
|
@@ -210,13 +232,19 @@
|
|
|
210
232
|
&& searchText === appliedSearch) page += 1;
|
|
211
233
|
}
|
|
212
234
|
|
|
235
|
+
$effect.pre(() => {
|
|
236
|
+
if (observedSearchable === searchable) return;
|
|
237
|
+
observedSearchable = searchable;
|
|
238
|
+
resetSearch();
|
|
239
|
+
});
|
|
240
|
+
|
|
213
241
|
$effect.pre(() => {
|
|
214
242
|
const currentScope = scope;
|
|
215
243
|
const currentValue = value;
|
|
216
244
|
const currentAllowed = allowed;
|
|
217
245
|
const currentDisabled = disabled;
|
|
218
246
|
untrack(() => {
|
|
219
|
-
if (observedScope
|
|
247
|
+
if (!sameScope(observedScope, currentScope) || (observedAllowed && !currentAllowed)) {
|
|
220
248
|
observedScope = currentScope;
|
|
221
249
|
valueInvalidated = true;
|
|
222
250
|
draftValues = [];
|
|
@@ -261,7 +289,7 @@
|
|
|
261
289
|
{/if}
|
|
262
290
|
</div>
|
|
263
291
|
|
|
264
|
-
<Dialog.Root bind:open>
|
|
292
|
+
<Dialog.Root bind:open onOpenChange={(next) => { if (!next) cancel(); }}>
|
|
265
293
|
<Dialog.Content class="svadmin-relation-picker__dialog" aria-label={title ?? i18n.t('common.selectOption')}>
|
|
266
294
|
<Dialog.Header>
|
|
267
295
|
<Dialog.Title>{title ?? i18n.t('common.selectOption')}</Dialog.Title>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Action } from '@svadmin/core';
|
|
5
5
|
import { captureAdminContext, useCan } from '@svadmin/core';
|
|
6
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
7
|
import type { Snippet } from 'svelte';
|
|
7
8
|
import DataState from './content/DataState.svelte';
|
|
8
9
|
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
}: Props = $props();
|
|
24
25
|
|
|
25
26
|
const adminContext = captureAdminContext();
|
|
27
|
+
const i18n = useTranslation();
|
|
26
28
|
const resource = $derived(adminContext.getResource(resourceName));
|
|
27
29
|
const accessControlEnabled = $derived(Boolean(adminContext.accessControlProvider));
|
|
28
30
|
const actionAllowedByResource = $derived(
|
|
@@ -58,7 +60,10 @@
|
|
|
58
60
|
&& (!accessControlEnabled || permission.allowed)
|
|
59
61
|
&& (!requireSourceRead || !accessControlEnabled || sourceReadPermission.allowed),
|
|
60
62
|
);
|
|
61
|
-
const
|
|
63
|
+
const permissionError = $derived(permission.isError || sourceReadPermission.isError);
|
|
64
|
+
const denialDescription = $derived(
|
|
65
|
+
permissionError ? i18n.t('common.permissionUnavailable') : i18n.t('common.permissionDenied'),
|
|
66
|
+
);
|
|
62
67
|
</script>
|
|
63
68
|
|
|
64
69
|
{#if permissionPending}
|
|
@@ -66,5 +71,5 @@
|
|
|
66
71
|
{:else if allowed}
|
|
67
72
|
{@render children()}
|
|
68
73
|
{:else}
|
|
69
|
-
<DataState state=
|
|
74
|
+
<DataState state={permissionError ? 'error' : 'forbidden'} description={denialDescription} />
|
|
70
75
|
{/if}
|