@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
|
@@ -7,6 +7,10 @@ export interface TreeSelectOption {
|
|
|
7
7
|
expanded?: boolean;
|
|
8
8
|
}
|
|
9
9
|
interface Props {
|
|
10
|
+
id?: string;
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
describedby?: string;
|
|
13
|
+
invalid?: boolean;
|
|
10
14
|
options?: TreeSelectOption[];
|
|
11
15
|
value?: string | number | (string | number)[];
|
|
12
16
|
multiple?: boolean;
|
|
@@ -151,7 +151,12 @@
|
|
|
151
151
|
&& new Set(columns.map(column => column.key)).size === columns.length
|
|
152
152
|
&& (!ordering || (columns.some(column => column.key === ordering.key) && ['asc', 'desc'].includes(ordering.direction)));
|
|
153
153
|
});
|
|
154
|
-
const
|
|
154
|
+
const hasInvalidData = $derived(!fullIndex.valid || !index.valid || !columnsValid);
|
|
155
|
+
// 宿主传入的 error 可能包含 provider/internal diagnostic,只映射为稳定本地化文案;
|
|
156
|
+
// 本地数据/列校验失败优先保留更具体、可操作的 invalidData 说明。
|
|
157
|
+
const displayError = $derived(hasInvalidData
|
|
158
|
+
? i18n.t('treeTable.invalidData')
|
|
159
|
+
: (error ? i18n.t('common.operationFailed') : ''));
|
|
155
160
|
const available = $derived(!loading && !displayError);
|
|
156
161
|
const selection = $derived(treeTableSelection(fullIndex.nodes, new Set(selectedKeys)));
|
|
157
162
|
function isExpanded(id: TreeTableId): boolean {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
let { enrollmentProvider }: { enrollmentProvider?: MfaEnrollmentProvider } = $props();
|
|
19
19
|
const i18n = useTranslation();
|
|
20
20
|
const context = captureAdminContext();
|
|
21
|
+
const controlId = $props.id();
|
|
21
22
|
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
22
23
|
let secret = $state('');
|
|
23
24
|
let code = $state('');
|
|
@@ -34,6 +35,9 @@
|
|
|
34
35
|
secret = ''; code = ''; recoveryCodes = null; pending = false; error = ''; copied = false;
|
|
35
36
|
return () => { epoch += 1; };
|
|
36
37
|
});
|
|
38
|
+
function failureMessage(): string {
|
|
39
|
+
return i18n.t('common.operationFailed');
|
|
40
|
+
}
|
|
37
41
|
async function enroll(verify: boolean) {
|
|
38
42
|
const provider = enrollmentProvider;
|
|
39
43
|
if (!provider || pending) return;
|
|
@@ -55,8 +59,8 @@
|
|
|
55
59
|
if (!result.secret) throw new Error(isZh ? '未返回登记密钥' : 'No enrollment secret returned');
|
|
56
60
|
secret = result.secret;
|
|
57
61
|
}
|
|
58
|
-
} catch
|
|
59
|
-
if (request === epoch) error =
|
|
62
|
+
} catch {
|
|
63
|
+
if (request === epoch) error = failureMessage();
|
|
60
64
|
} finally {
|
|
61
65
|
if (request === epoch) pending = false;
|
|
62
66
|
}
|
|
@@ -87,8 +91,8 @@
|
|
|
87
91
|
<p>{isZh ? '将此登记密钥添加到身份验证器,然后输入验证码。' : 'Add this enrollment secret to your authenticator, then enter its verification code.'}</p>
|
|
88
92
|
<code>{secret}</code>
|
|
89
93
|
<form onsubmit={(event) => { event.preventDefault(); void enroll(true); }}>
|
|
90
|
-
<label for=
|
|
91
|
-
<Input id=
|
|
94
|
+
<label for={`${controlId}-mfa-code`}>{i18n.t('auth.twoFactorEnterCode')}</label>
|
|
95
|
+
<Input id={`${controlId}-mfa-code`} bind:value={code} inputmode="numeric" autocomplete="one-time-code" maxlength={6} disabled={pending} />
|
|
92
96
|
<Button type="submit" disabled={pending}>{pending ? i18n.t('common.loading') : i18n.t('common.confirm')}</Button>
|
|
93
97
|
</form>
|
|
94
98
|
{:else}
|
|
@@ -82,31 +82,33 @@
|
|
|
82
82
|
|
|
83
83
|
<div class={cn('svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-cef5b893cf23 svadmin-u-2cd02d11d1af', className)}>
|
|
84
84
|
<!-- Header & Toolbar -->
|
|
85
|
-
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-56796a90bec5 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-967d113a1451">
|
|
86
|
-
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
|
|
85
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-56796a90bec5 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-967d113a1451 svadmin-u-1eb5c6df38c1">
|
|
86
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-7e0b7cdf1a94">
|
|
87
87
|
<h4 class="svadmin-u-359090c2d529 svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">Record Comparison</h4>
|
|
88
88
|
<Badge variant="outline" class="svadmin-u-d058ca6de60f">
|
|
89
89
|
{modifiedCount} change{modifiedCount === 1 ? '' : 's'}
|
|
90
90
|
</Badge>
|
|
91
91
|
</div>
|
|
92
92
|
|
|
93
|
-
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421">
|
|
93
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-1eb5c6df38c1">
|
|
94
94
|
<Button
|
|
95
95
|
variant={mode === 'split' ? 'secondary' : 'ghost'}
|
|
96
96
|
size="sm"
|
|
97
|
-
class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34"
|
|
97
|
+
class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34 svadmin-u-012fbd121f37"
|
|
98
|
+
aria-pressed={mode === 'split'}
|
|
98
99
|
onclick={() => { mode = 'split'; }}
|
|
99
100
|
>
|
|
100
|
-
<Columns2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
101
|
+
<Columns2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" aria-hidden="true" />
|
|
101
102
|
Side by Side
|
|
102
103
|
</Button>
|
|
103
104
|
<Button
|
|
104
105
|
variant={mode === 'unified' ? 'secondary' : 'ghost'}
|
|
105
106
|
size="sm"
|
|
106
|
-
class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34"
|
|
107
|
+
class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34 svadmin-u-012fbd121f37"
|
|
108
|
+
aria-pressed={mode === 'unified'}
|
|
107
109
|
onclick={() => { mode = 'unified'; }}
|
|
108
110
|
>
|
|
109
|
-
<AlignJustify class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
111
|
+
<AlignJustify class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" aria-hidden="true" />
|
|
110
112
|
Unified
|
|
111
113
|
</Button>
|
|
112
114
|
</div>
|
|
@@ -10,28 +10,29 @@
|
|
|
10
10
|
|
|
11
11
|
const context = captureAdminContext();
|
|
12
12
|
const i18n = useTranslation();
|
|
13
|
+
const controlId = $props.id();
|
|
13
14
|
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
14
15
|
const provider = $derived(context.organizationProvider);
|
|
15
16
|
let organization = $state<Organization | null>(null);
|
|
16
17
|
let name = $state('');
|
|
17
18
|
let loading = $state(false);
|
|
18
19
|
let saving = $state(false);
|
|
19
|
-
let error = $state(
|
|
20
|
-
let loadError = $state(
|
|
20
|
+
let error = $state(false);
|
|
21
|
+
let loadError = $state(false);
|
|
21
22
|
let status = $state('');
|
|
22
23
|
let epoch = 0;
|
|
23
24
|
async function load() {
|
|
24
25
|
const current = provider;
|
|
25
26
|
const request = ++epoch;
|
|
26
|
-
organization = null; name = ''; error =
|
|
27
|
+
organization = null; name = ''; error = false; loadError = false; status = ''; saving = false;
|
|
27
28
|
loading = Boolean(current);
|
|
28
29
|
if (!current) return;
|
|
29
30
|
try {
|
|
30
31
|
const result = await current.getCurrentOrganization(context.enterpriseRequestContext);
|
|
31
32
|
if (request !== epoch) return;
|
|
32
33
|
organization = result; name = result?.name ?? '';
|
|
33
|
-
} catch
|
|
34
|
-
if (request === epoch) loadError =
|
|
34
|
+
} catch {
|
|
35
|
+
if (request === epoch) loadError = true;
|
|
35
36
|
} finally {
|
|
36
37
|
if (request === epoch) loading = false;
|
|
37
38
|
}
|
|
@@ -46,14 +47,14 @@
|
|
|
46
47
|
const current = provider;
|
|
47
48
|
if (!current?.updateCurrentOrganization || !organization || !name.trim() || saving) return;
|
|
48
49
|
const request = epoch;
|
|
49
|
-
saving = true; error =
|
|
50
|
+
saving = true; error = false; status = '';
|
|
50
51
|
try {
|
|
51
52
|
const result = await current.updateCurrentOrganization({ name: name.trim() }, context.enterpriseRequestContext);
|
|
52
53
|
if (request !== epoch) return;
|
|
53
54
|
organization = result; name = result.name;
|
|
54
55
|
status = isZh ? '组织信息已保存' : 'Organization saved';
|
|
55
|
-
} catch
|
|
56
|
-
if (request === epoch) error =
|
|
56
|
+
} catch {
|
|
57
|
+
if (request === epoch) error = true;
|
|
57
58
|
} finally {
|
|
58
59
|
if (request === epoch) saving = false;
|
|
59
60
|
}
|
|
@@ -67,16 +68,16 @@
|
|
|
67
68
|
{:else if loading}
|
|
68
69
|
<DataState state="loading" />
|
|
69
70
|
{:else if loadError}
|
|
70
|
-
<DataState state="error" description={
|
|
71
|
+
<DataState state="error" description={i18n.t('common.operationFailed')} retry={load} />
|
|
71
72
|
{:else if !organization}
|
|
72
73
|
<DataState state="empty" title={isZh ? '暂无组织资料' : 'No organization details'} />
|
|
73
74
|
{:else}
|
|
74
75
|
<form onsubmit={save}>
|
|
75
|
-
<label for=
|
|
76
|
-
<Input id=
|
|
76
|
+
<label for={`${controlId}-company-name`}>{isZh ? '组织名称' : 'Organization name'}</label>
|
|
77
|
+
<Input id={`${controlId}-company-name`} bind:value={name} required disabled={saving || !provider.updateCurrentOrganization} />
|
|
77
78
|
{#if !provider.updateCurrentOrganization}<p>{isZh ? '当前组织资料为只读。' : 'Organization details are read-only.'}</p>{/if}
|
|
78
79
|
<Button type="submit" disabled={saving || !provider.updateCurrentOrganization || !name.trim() || name.trim() === organization.name}>{saving ? i18n.t('common.loading') : i18n.t('common.save')}</Button>
|
|
79
|
-
{#if error}<p role="alert">{
|
|
80
|
+
{#if error}<p role="alert">{i18n.t('common.operationFailed')}</p>{/if}
|
|
80
81
|
{#if status}<p role="status">{status}</p>{/if}
|
|
81
82
|
</form>
|
|
82
83
|
{/if}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
</script>
|
|
16
16
|
<script lang="ts">
|
|
17
|
+
import { untrack } from 'svelte';
|
|
17
18
|
import { captureAdminContext } from '@svadmin/core';
|
|
18
19
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
19
20
|
import { Button } from '../ui/button/index.js';
|
|
@@ -34,7 +35,8 @@
|
|
|
34
35
|
let loading = $state(false);
|
|
35
36
|
let sending = $state(false);
|
|
36
37
|
let error = $state('');
|
|
37
|
-
let
|
|
38
|
+
let requestError = $state(false);
|
|
39
|
+
let loadError = $state(false);
|
|
38
40
|
let status = $state('');
|
|
39
41
|
let epoch = 0;
|
|
40
42
|
const filtered = $derived(rows.filter(row => `${row.name} ${row.email}`.toLowerCase().includes(query.toLowerCase())));
|
|
@@ -43,22 +45,23 @@
|
|
|
43
45
|
searchInput?.focus();
|
|
44
46
|
}
|
|
45
47
|
async function load() {
|
|
48
|
+
if (untrack(() => sending)) return;
|
|
46
49
|
const current = provider;
|
|
47
50
|
const request = ++epoch;
|
|
48
|
-
rows = []; loadError =
|
|
51
|
+
rows = []; loadError = false; requestError = false; loading = Boolean(current);
|
|
49
52
|
if (!current) return;
|
|
50
53
|
try {
|
|
51
54
|
const result = await current.list();
|
|
52
55
|
if (request === epoch) rows = result;
|
|
53
|
-
} catch
|
|
54
|
-
if (request === epoch) loadError =
|
|
56
|
+
} catch {
|
|
57
|
+
if (request === epoch) loadError = true;
|
|
55
58
|
} finally {
|
|
56
59
|
if (request === epoch) loading = false;
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
$effect(() => {
|
|
60
63
|
void provider; void context.authProvider; void context.tenantCacheKey?.__svadminTenant;
|
|
61
|
-
email = ''; query = ''; error = ''; status = ''; sending = false;
|
|
64
|
+
email = ''; query = ''; error = ''; requestError = false; status = ''; sending = false;
|
|
62
65
|
void load();
|
|
63
66
|
return () => { epoch += 1; };
|
|
64
67
|
});
|
|
@@ -66,6 +69,7 @@
|
|
|
66
69
|
event.preventDefault();
|
|
67
70
|
const current = provider;
|
|
68
71
|
if (!current?.invite || sending || loading) return;
|
|
72
|
+
requestError = false;
|
|
69
73
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
|
|
70
74
|
error = isZh ? '请输入有效邮箱' : 'Enter a valid email address';
|
|
71
75
|
return;
|
|
@@ -77,8 +81,8 @@
|
|
|
77
81
|
if (request !== epoch) return;
|
|
78
82
|
email = '';
|
|
79
83
|
status = isZh ? '邀请已提交' : 'Invitation submitted';
|
|
80
|
-
} catch
|
|
81
|
-
if (request === epoch)
|
|
84
|
+
} catch {
|
|
85
|
+
if (request === epoch) requestError = true;
|
|
82
86
|
} finally {
|
|
83
87
|
if (request === epoch) sending = false;
|
|
84
88
|
}
|
|
@@ -86,7 +90,7 @@
|
|
|
86
90
|
async function copyLink() {
|
|
87
91
|
if (!provider?.invitationUrl) return;
|
|
88
92
|
const request = epoch;
|
|
89
|
-
status = ''; error = '';
|
|
93
|
+
status = ''; error = ''; requestError = false;
|
|
90
94
|
try {
|
|
91
95
|
await navigator.clipboard.writeText(provider.invitationUrl);
|
|
92
96
|
if (request === epoch) status = isZh ? '链接已复制' : 'Link copied';
|
|
@@ -113,12 +117,12 @@
|
|
|
113
117
|
<p class="text-sm text-muted-foreground">{isZh ? '当前目录为只读,未配置邀请发送能力。' : 'This directory is read-only. Invitation delivery is not configured.'}</p>
|
|
114
118
|
{/if}
|
|
115
119
|
{#if provider.invitationUrl}<div><Button variant="outline" onclick={copyLink}><Copy size={16} />{i18n.t('account.copyLink')}</Button></div>{/if}
|
|
116
|
-
{#if error}<p role="alert" class="text-sm text-destructive">{error}</p>{/if}
|
|
120
|
+
{#if requestError || error}<p role="alert" class="text-sm text-destructive">{requestError ? i18n.t('common.operationFailed') : error}</p>{/if}
|
|
117
121
|
{#if status}<p role="status" class="text-sm text-foreground">{status}</p>{/if}
|
|
118
122
|
{#if loading}
|
|
119
123
|
<DataState state="loading" />
|
|
120
124
|
{:else if loadError}
|
|
121
|
-
<DataState state="error" description={
|
|
125
|
+
<DataState state="error" description={i18n.t('common.operationFailed')} {...(sending ? {} : { retry: load })} />
|
|
122
126
|
{:else}
|
|
123
127
|
<div class="w-full max-w-sm space-y-2">
|
|
124
128
|
<label for={`${controlId}-search`} class="block text-sm font-medium text-foreground">{isZh ? '搜索成员' : 'Search members'}</label>
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
let saving = $state(false);
|
|
44
44
|
let savingOrganization = $state(false);
|
|
45
45
|
let testing = $state(false);
|
|
46
|
-
let error = $state
|
|
46
|
+
let error = $state(false);
|
|
47
47
|
let requestId = 0;
|
|
48
48
|
const organizationChanged = $derived(Boolean(
|
|
49
49
|
organizationProvider?.updateCurrentOrganization
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
const auditRetentionValid = $derived(isPositiveInteger(auditRetentionValue));
|
|
59
59
|
const policyNumbersValid = $derived(sessionTimeoutValid && auditRetentionValid);
|
|
60
60
|
|
|
61
|
-
function
|
|
62
|
-
return
|
|
61
|
+
function failureMessage(): string {
|
|
62
|
+
return i18n.t('common.operationFailed');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
async function loadSettings(): Promise<void> {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
auditRetentionValue = '';
|
|
75
75
|
identityProviders = [];
|
|
76
76
|
events = [];
|
|
77
|
-
error =
|
|
77
|
+
error = false;
|
|
78
78
|
loading = false;
|
|
79
79
|
saving = false;
|
|
80
80
|
savingOrganization = false;
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
organization = nextOrganization;
|
|
98
98
|
organizationName = nextOrganization?.name ?? '';
|
|
99
99
|
metadataUrl = nextIdentityProviders[0]?.metadataUrl ?? '';
|
|
100
|
-
} catch
|
|
101
|
-
if (currentRequest === requestId) error =
|
|
100
|
+
} catch {
|
|
101
|
+
if (currentRequest === requestId) error = true;
|
|
102
102
|
} finally {
|
|
103
103
|
if (currentRequest === requestId) loading = false;
|
|
104
104
|
}
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
const provider = identityProvider;
|
|
117
117
|
const currentRequest = requestId;
|
|
118
118
|
const context = adminContext.enterpriseRequestContext;
|
|
119
|
-
if (!provider || !policy || !policyNumbersValid) return;
|
|
119
|
+
if (!provider || !policy || !policyNumbersValid || saving) return;
|
|
120
120
|
saving = true;
|
|
121
121
|
try {
|
|
122
122
|
const updatedPolicy = await provider.updateSecurityPolicy({
|
|
@@ -129,9 +129,9 @@
|
|
|
129
129
|
sessionTimeoutValue = String(updatedPolicy.sessionTimeoutMinutes);
|
|
130
130
|
auditRetentionValue = String(updatedPolicy.auditRetentionDays);
|
|
131
131
|
notifyWithProvider({ type: 'success', message: isZh ? '企业策略已保存' : 'Enterprise policy saved' }, adminContext.notificationProvider);
|
|
132
|
-
} catch
|
|
132
|
+
} catch {
|
|
133
133
|
if (currentRequest !== requestId) return;
|
|
134
|
-
notifyWithProvider({ type: 'error', message:
|
|
134
|
+
notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
|
|
135
135
|
} finally {
|
|
136
136
|
if (currentRequest === requestId) saving = false;
|
|
137
137
|
}
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
const currentRequest = requestId;
|
|
143
143
|
const context = adminContext.enterpriseRequestContext;
|
|
144
144
|
const name = organizationName.trim();
|
|
145
|
-
if (!provider?.updateCurrentOrganization || !organization || !name || name === organization.name) return;
|
|
145
|
+
if (!provider?.updateCurrentOrganization || !organization || !name || name === organization.name || savingOrganization) return;
|
|
146
146
|
savingOrganization = true;
|
|
147
147
|
try {
|
|
148
148
|
const updatedOrganization = await provider.updateCurrentOrganization({ name }, context);
|
|
@@ -150,9 +150,9 @@
|
|
|
150
150
|
organization = updatedOrganization;
|
|
151
151
|
organizationName = updatedOrganization.name;
|
|
152
152
|
notifyWithProvider({ type: 'success', message: isZh ? '组织信息已保存' : 'Organization saved' }, adminContext.notificationProvider);
|
|
153
|
-
} catch
|
|
153
|
+
} catch {
|
|
154
154
|
if (currentRequest !== requestId) return;
|
|
155
|
-
notifyWithProvider({ type: 'error', message:
|
|
155
|
+
notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
|
|
156
156
|
} finally {
|
|
157
157
|
if (currentRequest === requestId) savingOrganization = false;
|
|
158
158
|
}
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
const provider = identityProvider;
|
|
163
163
|
const currentRequest = requestId;
|
|
164
164
|
const context = adminContext.enterpriseRequestContext;
|
|
165
|
-
if (!provider) return;
|
|
165
|
+
if (!provider || testing || loading || error) return;
|
|
166
166
|
testing = true;
|
|
167
167
|
try {
|
|
168
168
|
const selectedProvider = target ?? identityProviders[0];
|
|
@@ -178,11 +178,11 @@
|
|
|
178
178
|
type: result.success ? 'success' : 'error',
|
|
179
179
|
message: result.success
|
|
180
180
|
? (isZh ? '身份源连接测试通过' : 'Identity provider connection succeeded')
|
|
181
|
-
: (
|
|
181
|
+
: failureMessage(),
|
|
182
182
|
}, adminContext.notificationProvider);
|
|
183
|
-
} catch
|
|
183
|
+
} catch {
|
|
184
184
|
if (currentRequest !== requestId) return;
|
|
185
|
-
notifyWithProvider({ type: 'error', message:
|
|
185
|
+
notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
|
|
186
186
|
} finally {
|
|
187
187
|
if (currentRequest === requestId) testing = false;
|
|
188
188
|
}
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
{#if loading}
|
|
215
215
|
<DataState state="loading" />
|
|
216
216
|
{:else if error}
|
|
217
|
-
<DataState state="error" title={isZh ? '企业设置加载失败' : 'Enterprise settings failed to load'} description={
|
|
217
|
+
<DataState state="error" title={isZh ? '企业设置加载失败' : 'Enterprise settings failed to load'} description={failureMessage()} retry={loadSettings} />
|
|
218
218
|
{:else}
|
|
219
219
|
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-60541e1e26f8 svadmin-u-0c3bc98565dd svadmin-u-2f27a80ed92b">
|
|
220
220
|
<SettingsGroup title={i18n.t('account.securityPolicy')} description={isZh ? '配置组织级身份、会话与审计策略。' : 'Configure organization-wide identity, session, and audit controls.'} bodyClass="space-y-4">
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
<DataState state="empty" title={isZh ? '尚未配置身份源' : 'No identity provider configured'} description={isZh ? '请在后端 IdentityGovernanceProvider 中创建并验证身份源。' : 'Create and verify an identity provider through the backend IdentityGovernanceProvider.'} />
|
|
245
245
|
{:else}
|
|
246
246
|
{#each identityProviders as connection (connection.id)}
|
|
247
|
-
<IntegrationCard integration={definedOptions({ id: connection.id, name: connection.name, account: connection.protocol.toUpperCase(), connected: connection.status === 'connected', description: connection.domain ?? connection.metadataUrl })} onconnect={() => testConnection(connection)} />
|
|
247
|
+
<IntegrationCard integration={definedOptions({ id: connection.id, name: connection.name, account: connection.protocol.toUpperCase(), connected: connection.status === 'connected', description: connection.domain ?? connection.metadataUrl })} actionLabel={i18n.t('common.test')} disabled={testing} onconnect={() => testConnection(connection)} />
|
|
248
248
|
{/each}
|
|
249
249
|
{/if}
|
|
250
250
|
</section>
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<script lang="ts">
|
|
17
17
|
import type { Snippet } from 'svelte';
|
|
18
18
|
import { definedOptions } from '@svadmin/core/options';
|
|
19
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
19
20
|
import { cn } from '../../utils.js';
|
|
20
21
|
import StatusBadge from './StatusBadge.svelte';
|
|
21
22
|
import DataState from './DataState.svelte';
|
|
@@ -37,8 +38,8 @@
|
|
|
37
38
|
|
|
38
39
|
let {
|
|
39
40
|
items = [],
|
|
40
|
-
emptyTitle
|
|
41
|
-
emptyDescription
|
|
41
|
+
emptyTitle,
|
|
42
|
+
emptyDescription,
|
|
42
43
|
state,
|
|
43
44
|
stateTitle,
|
|
44
45
|
stateDescription,
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
class: className = '',
|
|
50
51
|
}: Props = $props();
|
|
51
52
|
|
|
53
|
+
const i18n = useTranslation();
|
|
52
54
|
const dotClass: Record<Status, string> = {
|
|
53
55
|
success: 'svadmin-u-3355648fe22b svadmin-u-18a6e7a36f29 svadmin-u-76747e5e02ff',
|
|
54
56
|
warning: 'svadmin-u-a486cacb3a15 svadmin-u-2f960aa0c478 svadmin-u-3a4ff758c2ab',
|
|
@@ -64,8 +66,8 @@
|
|
|
64
66
|
<DataState
|
|
65
67
|
state={resolvedState}
|
|
66
68
|
{...definedOptions({
|
|
67
|
-
title: stateTitle ?? (resolvedState === 'empty' ? emptyTitle : undefined),
|
|
68
|
-
description: stateDescription ?? (resolvedState === 'empty' ? emptyDescription : undefined),
|
|
69
|
+
title: stateTitle ?? (resolvedState === 'empty' ? emptyTitle ?? i18n.t('audit.emptyTitle') : undefined),
|
|
70
|
+
description: stateDescription ?? (resolvedState === 'empty' ? emptyDescription ?? i18n.t('audit.emptyDescription') : undefined),
|
|
69
71
|
retry,
|
|
70
72
|
retryLabel,
|
|
71
73
|
loadingLabel,
|
|
@@ -89,10 +91,10 @@
|
|
|
89
91
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
|
|
90
92
|
<span class="svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{item.title}</span>
|
|
91
93
|
{#if item.actor}
|
|
92
|
-
<span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">
|
|
94
|
+
<span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">{i18n.t('audit.actorPrefix')}{item.actor}</span>
|
|
93
95
|
{/if}
|
|
94
96
|
{#if item.status}
|
|
95
|
-
<StatusBadge status={item.status} label={item.tag ?? item.status} class="svadmin-u-d058ca6de60f svadmin-u-68ecb30dbec6 svadmin-u-11e59c6d5f6b" />
|
|
97
|
+
<StatusBadge status={item.status} label={item.tag ?? i18n.t(`audit.status.${item.status}`)} class="svadmin-u-d058ca6de60f svadmin-u-68ecb30dbec6 svadmin-u-11e59c6d5f6b" />
|
|
96
98
|
{:else if item.tag}
|
|
97
99
|
<span class="svadmin-u-52083e7da442 svadmin-u-3960ffc248d9 svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-45d828117213 svadmin-u-465609a240a8 svadmin-u-d058ca6de60f svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-2ef11f1cb219">
|
|
98
100
|
{item.tag}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
loading: [i18n.t('common.loading'), i18n.t('common.loading')],
|
|
33
33
|
empty: [i18n.t('empty.title'), i18n.t('empty.description')],
|
|
34
34
|
'filtered-empty': [isZh ? '没有匹配结果' : 'No matching results', isZh ? '请调整或清除筛选条件。' : 'Adjust or clear your filters.'],
|
|
35
|
-
error: [i18n.t('common.error'), i18n.t('common.
|
|
35
|
+
error: [i18n.t('common.error'), i18n.t('common.operationFailed')],
|
|
36
36
|
forbidden: [isZh ? '无权访问' : 'Access restricted', isZh ? '你没有查看此内容的权限。' : 'You do not have permission to view this content.'],
|
|
37
37
|
} as const);
|
|
38
38
|
const resolvedTitle = $derived(title ?? defaults[state][0]);
|
|
@@ -1,23 +1,54 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { definedOptions } from '@svadmin/core/options';
|
|
3
|
-
|
|
4
|
-
import { Download, File, Trash2 } from '@lucide/svelte';
|
|
5
|
-
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
|
-
import { Button } from '../ui/button/index.js';
|
|
7
3
|
import DataState from './DataState.svelte';
|
|
8
4
|
import type { DataStateKind } from './DataState.svelte';
|
|
5
|
+
import FileListRow from './FileListRow.svelte';
|
|
6
|
+
|
|
9
7
|
export interface FileItem { id?: string; name: string; size?: string; type?: string; updatedAt?: string; }
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
type FileActionHandler = (file: FileItem) => void;
|
|
9
|
+
interface Props {
|
|
10
|
+
files?: FileItem[];
|
|
11
|
+
state?: DataStateKind;
|
|
12
|
+
stateTitle?: string;
|
|
13
|
+
stateDescription?: string;
|
|
14
|
+
emptyTitle?: string;
|
|
15
|
+
emptyDescription?: string;
|
|
16
|
+
retry?: () => void;
|
|
17
|
+
retryLabel?: string;
|
|
18
|
+
loadingLabel?: string;
|
|
19
|
+
ondownload?: FileActionHandler;
|
|
20
|
+
onpreview?: FileActionHandler;
|
|
21
|
+
onremove?: FileActionHandler;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
let {
|
|
25
|
+
files = [], state, stateTitle, stateDescription, emptyTitle, emptyDescription,
|
|
26
|
+
retry, retryLabel, loadingLabel, ondownload, onpreview, onremove, class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
13
28
|
const resolvedState = $derived(state ?? (files.length === 0 ? 'empty' : undefined));
|
|
29
|
+
const isEmptyState = $derived(resolvedState === 'empty' || resolvedState === 'filtered-empty');
|
|
30
|
+
|
|
31
|
+
const rows = $derived.by(() => {
|
|
32
|
+
const occurrences = new Map<string, number>();
|
|
33
|
+
return files.map((file, index) => {
|
|
34
|
+
if (file.id === undefined) return { file, key: JSON.stringify(['index', index, file.name]) };
|
|
35
|
+
const occurrence = occurrences.get(file.id) ?? 0;
|
|
36
|
+
occurrences.set(file.id, occurrence + 1);
|
|
37
|
+
return { file, key: JSON.stringify(['id', file.id, occurrence]) };
|
|
38
|
+
});
|
|
39
|
+
});
|
|
14
40
|
</script>
|
|
41
|
+
|
|
15
42
|
{#if resolvedState}
|
|
16
|
-
<DataState state={resolvedState} {...definedOptions({
|
|
43
|
+
<DataState state={resolvedState} {...definedOptions({
|
|
44
|
+
title: stateTitle ?? (isEmptyState ? emptyTitle : undefined),
|
|
45
|
+
description: stateDescription ?? (isEmptyState ? emptyDescription : undefined),
|
|
46
|
+
retry, retryLabel, loadingLabel,
|
|
47
|
+
})} class={className} />
|
|
17
48
|
{:else}
|
|
18
|
-
<div class={'svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-2cd02d11d1af svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 ' + className}>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</div>
|
|
49
|
+
<div class={'svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-2cd02d11d1af svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 ' + className} role="list" data-svadmin-file-list>
|
|
50
|
+
{#each rows as { file, key } (key)}
|
|
51
|
+
<FileListRow {file} {...definedOptions({ ondownload, onpreview, onremove })} />
|
|
52
|
+
{/each}
|
|
53
|
+
</div>
|
|
23
54
|
{/if}
|
|
@@ -6,6 +6,7 @@ export interface FileItem {
|
|
|
6
6
|
type?: string;
|
|
7
7
|
updatedAt?: string;
|
|
8
8
|
}
|
|
9
|
+
type FileActionHandler = (file: FileItem) => void;
|
|
9
10
|
interface Props {
|
|
10
11
|
files?: FileItem[];
|
|
11
12
|
state?: DataStateKind;
|
|
@@ -16,8 +17,9 @@ interface Props {
|
|
|
16
17
|
retry?: () => void;
|
|
17
18
|
retryLabel?: string;
|
|
18
19
|
loadingLabel?: string;
|
|
19
|
-
ondownload?:
|
|
20
|
-
|
|
20
|
+
ondownload?: FileActionHandler;
|
|
21
|
+
onpreview?: FileActionHandler;
|
|
22
|
+
onremove?: FileActionHandler;
|
|
21
23
|
class?: string;
|
|
22
24
|
}
|
|
23
25
|
declare const FileList: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, untrack } from 'svelte';
|
|
3
|
+
import { Download, Eye, File, Trash2 } from '@lucide/svelte';
|
|
4
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
5
|
+
import { Button } from '../ui/button/index.js';
|
|
6
|
+
import type { FileItem } from './FileList.svelte';
|
|
7
|
+
|
|
8
|
+
type Action = 'download' | 'preview' | 'remove';
|
|
9
|
+
type Handler = (file: FileItem) => void;
|
|
10
|
+
interface Props { file: FileItem; ondownload?: Handler; onpreview?: Handler; onremove?: Handler; }
|
|
11
|
+
let { file, ondownload, onpreview, onremove }: Props = $props();
|
|
12
|
+
const i18n = useTranslation();
|
|
13
|
+
const instanceId = $props.id();
|
|
14
|
+
const errorId = `${instanceId}-error`;
|
|
15
|
+
let pending = $state<Action | undefined>();
|
|
16
|
+
let failed = $state<Action | undefined>();
|
|
17
|
+
let generation = 0;
|
|
18
|
+
let destroyed = false;
|
|
19
|
+
let observed = untrack(() => ({ file, ondownload, onpreview, onremove }));
|
|
20
|
+
|
|
21
|
+
$effect.pre(() => {
|
|
22
|
+
if (file === observed.file && ondownload === observed.ondownload
|
|
23
|
+
&& onpreview === observed.onpreview && onremove === observed.onremove) return;
|
|
24
|
+
observed = { file, ondownload, onpreview, onremove };
|
|
25
|
+
generation += 1;
|
|
26
|
+
pending = undefined;
|
|
27
|
+
failed = undefined;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
onDestroy(() => { destroyed = true; generation += 1; });
|
|
31
|
+
|
|
32
|
+
function label(action: Action): string {
|
|
33
|
+
return i18n.t(action === 'download' ? 'common.download'
|
|
34
|
+
: action === 'preview' ? 'common.preview' : 'common.remove');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function run(action: Action): Promise<void> {
|
|
38
|
+
if (destroyed || pending) return;
|
|
39
|
+
const handler = action === 'download' ? ondownload : action === 'preview' ? onpreview : onremove;
|
|
40
|
+
if (!handler) return;
|
|
41
|
+
const owner = { file, ondownload, onpreview, onremove };
|
|
42
|
+
const current = ++generation;
|
|
43
|
+
const isCurrent = () => !destroyed && generation === current && file === owner.file
|
|
44
|
+
&& ondownload === owner.ondownload && onpreview === owner.onpreview && onremove === owner.onremove;
|
|
45
|
+
pending = action;
|
|
46
|
+
failed = undefined;
|
|
47
|
+
try {
|
|
48
|
+
await handler(file);
|
|
49
|
+
} catch {
|
|
50
|
+
if (isCurrent()) failed = action;
|
|
51
|
+
} finally {
|
|
52
|
+
if (isCurrent()) pending = undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div class="file-list-row svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-1004c0c3954c svadmin-u-f0faeb26d656 svadmin-u-1b2d54a3fd12"
|
|
58
|
+
role="listitem" aria-label={file.name} aria-busy={Boolean(pending)}>
|
|
59
|
+
<span class="svadmin-u-60fbb7713999 svadmin-u-665f07fe73cc svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-2ef11f1cb219 svadmin-u-bfa603190748" aria-hidden="true"><File class="svadmin-u-f7b5fa971871" /></span>
|
|
60
|
+
<div class="svadmin-u-7e0b7cdf1a94 svadmin-u-36e579c0b41c">
|
|
61
|
+
<p class="svadmin-u-f283ea9bea0e svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359" title={file.name}>{file.name}</p>
|
|
62
|
+
{#if pending}
|
|
63
|
+
<p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748" role="status">{i18n.t('common.loading')}</p>
|
|
64
|
+
{:else}
|
|
65
|
+
<p class="file-list-row-meta svadmin-u-359090c2d529 svadmin-u-bfa603190748">{file.size ?? file.type ?? i18n.t('common.file')}{file.updatedAt ? ' / ' + file.updatedAt : ''}</p>
|
|
66
|
+
{/if}
|
|
67
|
+
{#if failed}<p class="file-list-row-error" role="alert" id={errorId}>{label(failed)}: {i18n.t('common.error')}</p>{/if}
|
|
68
|
+
</div>
|
|
69
|
+
{#if onpreview}<Button type="button" variant="ghost" size="icon-sm"
|
|
70
|
+
aria-label={`${label('preview')} ${file.name}`} title={`${label('preview')} ${file.name}`}
|
|
71
|
+
aria-describedby={failed ? errorId : undefined} disabled={Boolean(pending)}
|
|
72
|
+
onclick={() => { void run('preview'); }}><Eye aria-hidden="true" class="svadmin-u-f7b5fa971871" /></Button>{/if}
|
|
73
|
+
{#if ondownload}<Button type="button" variant="ghost" size="icon-sm"
|
|
74
|
+
aria-label={`${label('download')} ${file.name}`} title={`${label('download')} ${file.name}`}
|
|
75
|
+
aria-describedby={failed ? errorId : undefined} disabled={Boolean(pending)}
|
|
76
|
+
onclick={() => { void run('download'); }}><Download aria-hidden="true" class="svadmin-u-f7b5fa971871" /></Button>{/if}
|
|
77
|
+
{#if onremove}<Button type="button" variant="ghost" size="icon-sm"
|
|
78
|
+
aria-label={`${label('remove')} ${file.name}`} title={`${label('remove')} ${file.name}`}
|
|
79
|
+
aria-describedby={failed ? errorId : undefined} disabled={Boolean(pending)}
|
|
80
|
+
onclick={() => { void run('remove'); }}><Trash2 aria-hidden="true" class="svadmin-u-f7b5fa971871" /></Button>{/if}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<style>
|
|
84
|
+
.file-list-row-error { margin: 4px 0 0; font-size: 14px; line-height: 20px; color: var(--destructive); overflow-wrap: anywhere; }
|
|
85
|
+
.file-list-row-meta { overflow-wrap: anywhere; }
|
|
86
|
+
.file-list-row :global(button) { flex-shrink: 0; }
|
|
87
|
+
</style>
|