@svadmin/ui 0.88.1 → 0.92.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.
Files changed (172) hide show
  1. package/dist/app.css +4 -0
  2. package/dist/app.theme.css +4 -0
  3. package/dist/component-registry.svelte.d.ts +2 -2
  4. package/dist/components/AboutSettings.svelte +23 -4
  5. package/dist/components/AdminApp.svelte +6 -4
  6. package/dist/components/AdminApp.svelte.d.ts +3 -3
  7. package/dist/components/ApiSettings.svelte +43 -27
  8. package/dist/components/ApprovalActionCard.svelte +8 -9
  9. package/dist/components/ApprovalCenter.svelte +24 -5
  10. package/dist/components/AuditLogViewer.svelte +2 -2
  11. package/dist/components/AutoComplete.svelte +610 -0
  12. package/dist/components/AutoComplete.svelte.d.ts +25 -0
  13. package/dist/components/AutoForm.svelte +265 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +141 -36
  16. package/dist/components/AutoTable.svelte.d.ts +8 -0
  17. package/dist/components/BatchActionBar.svelte +42 -14
  18. package/dist/components/BatchActionBar.svelte.d.ts +5 -0
  19. package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
  20. package/dist/components/CancelTaskButton.svelte +14 -1
  21. package/dist/components/Cascader.svelte +412 -53
  22. package/dist/components/Cascader.svelte.d.ts +7 -1
  23. package/dist/components/ColorPicker.svelte +1 -1
  24. package/dist/components/ColorPicker.svelte.d.ts +1 -1
  25. package/dist/components/ComboboxField.svelte +270 -34
  26. package/dist/components/ComboboxField.svelte.d.ts +4 -0
  27. package/dist/components/CommandPalette.svelte +4 -4
  28. package/dist/components/DashboardView.svelte +73 -7
  29. package/dist/components/DashboardView.svelte.d.ts +3 -1
  30. package/dist/components/DateRangeInput.svelte +8 -1
  31. package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
  32. package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
  33. package/dist/components/DetailWorkspaceSections.svelte +233 -0
  34. package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
  35. package/dist/components/DrawerForm.svelte +78 -9
  36. package/dist/components/DrawerForm.svelte.d.ts +3 -3
  37. package/dist/components/ErrorBoundary.svelte +1 -1
  38. package/dist/components/ErrorComponent.svelte +19 -6
  39. package/dist/components/ErrorComponent.svelte.d.ts +1 -0
  40. package/dist/components/ErrorSummary.svelte +9 -4
  41. package/dist/components/FieldRenderer.svelte +203 -90
  42. package/dist/components/FieldRenderer.svelte.d.ts +16 -1
  43. package/dist/components/FileUpload.svelte +116 -51
  44. package/dist/components/FileUpload.svelte.d.ts +7 -27
  45. package/dist/components/FileUploadField.svelte +170 -0
  46. package/dist/components/FileUploadField.svelte.d.ts +21 -0
  47. package/dist/components/FilterBuilder.svelte +13 -7
  48. package/dist/components/FilterCollectionInput.svelte +9 -7
  49. package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
  50. package/dist/components/ForgotPasswordPage.svelte +1 -1
  51. package/dist/components/Header.svelte +1 -1
  52. package/dist/components/ImportWizard.svelte +197 -28
  53. package/dist/components/ImportWizard.svelte.d.ts +5 -1
  54. package/dist/components/InferencerPanel.svelte +4 -2
  55. package/dist/components/InfiniteList.svelte +52 -9
  56. package/dist/components/JsonSchemaForm.svelte +237 -42
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
  58. package/dist/components/KanbanBoard.svelte +43 -7
  59. package/dist/components/KanbanBoard.svelte.d.ts +2 -2
  60. package/dist/components/Layout.svelte +1 -1
  61. package/dist/components/LoginPage.svelte +1 -1
  62. package/dist/components/MetricStrip.svelte +8 -3
  63. package/dist/components/MetricStrip.svelte.d.ts +1 -0
  64. package/dist/components/ModalForm.svelte +85 -9
  65. package/dist/components/ModalForm.svelte.d.ts +3 -3
  66. package/dist/components/MoneyInput.svelte +5 -3
  67. package/dist/components/MoneyInput.svelte.d.ts +4 -2
  68. package/dist/components/MultiTabKeepAlive.svelte +502 -43
  69. package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
  70. package/dist/components/NotificationsSettings.svelte +10 -10
  71. package/dist/components/NumberInput.svelte +5 -3
  72. package/dist/components/NumberInput.svelte.d.ts +4 -2
  73. package/dist/components/NumericInput.svelte +277 -25
  74. package/dist/components/NumericInput.svelte.d.ts +3 -1
  75. package/dist/components/PercentInput.svelte +5 -3
  76. package/dist/components/PercentInput.svelte.d.ts +4 -2
  77. package/dist/components/PermissionMatrix.svelte +44 -8
  78. package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
  79. package/dist/components/ProfilePage.svelte +38 -13
  80. package/dist/components/RadioGroup.svelte +203 -4
  81. package/dist/components/RadioGroup.svelte.d.ts +1 -1
  82. package/dist/components/Rate.svelte +3 -3
  83. package/dist/components/Rate.svelte.d.ts +2 -2
  84. package/dist/components/RecordDetailDrawer.svelte +1 -1
  85. package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
  86. package/dist/components/RegisterPage.svelte +1 -1
  87. package/dist/components/RelationPicker.svelte +39 -11
  88. package/dist/components/ResourceAccessGuard.svelte +7 -2
  89. package/dist/components/RetryTaskButton.svelte +14 -1
  90. package/dist/components/RolesSettings.svelte +74 -28
  91. package/dist/components/SecuritySettings.svelte +66 -51
  92. package/dist/components/SegmentedControl.svelte +4 -2
  93. package/dist/components/SegmentedControl.svelte.d.ts +1 -1
  94. package/dist/components/Sidebar.svelte +1 -1
  95. package/dist/components/StepsForm.svelte +62 -10
  96. package/dist/components/StepsForm.svelte.d.ts +4 -2
  97. package/dist/components/SvadminLogo.svelte +1 -0
  98. package/dist/components/SvarDataGrid.svelte +42 -24
  99. package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
  100. package/dist/components/SvarResourceRead.svelte +2 -0
  101. package/dist/components/SvarResourceTableView.svelte +4 -2
  102. package/dist/components/TagsInput.svelte +3 -3
  103. package/dist/components/TagsInput.svelte.d.ts +2 -2
  104. package/dist/components/TaskDetails.svelte +22 -16
  105. package/dist/components/TaskDetails.svelte.d.ts +1 -1
  106. package/dist/components/TaskList.svelte +57 -15
  107. package/dist/components/TaskQueueDrawer.svelte +54 -34
  108. package/dist/components/Toast.svelte +13 -2
  109. package/dist/components/Transfer.svelte +67 -5
  110. package/dist/components/Transfer.svelte.d.ts +4 -0
  111. package/dist/components/TreeSelect.svelte +337 -77
  112. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  113. package/dist/components/TreeTable.svelte +6 -1
  114. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  115. package/dist/components/UpdatePasswordPage.svelte +1 -1
  116. package/dist/components/VersionDiffViewer.svelte +9 -7
  117. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  118. package/dist/components/account/MemberDirectory.svelte +14 -10
  119. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  120. package/dist/components/content/AuditTimeline.svelte +8 -6
  121. package/dist/components/content/DataState.svelte +1 -1
  122. package/dist/components/content/FileList.svelte +44 -13
  123. package/dist/components/content/FileList.svelte.d.ts +4 -2
  124. package/dist/components/content/FileListRow.svelte +87 -0
  125. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  126. package/dist/components/content/ImportDropzone.svelte +109 -12
  127. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  128. package/dist/components/content/IntegrationCard.svelte +3 -3
  129. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  130. package/dist/components/content/MediaThumbnail.svelte +6 -2
  131. package/dist/components/detail-workspace.d.ts +60 -0
  132. package/dist/components/detail-workspace.js +3 -0
  133. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  134. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  135. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  136. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  137. package/dist/components/enterprise/schema-form-references.js +316 -0
  138. package/dist/components/fields/ArrayField.svelte +178 -14
  139. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  140. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  141. package/dist/components/fields/array-field-metadata.js +36 -0
  142. package/dist/components/file-upload-reference.d.ts +11 -0
  143. package/dist/components/file-upload-reference.js +50 -0
  144. package/dist/components/file-upload-types.d.ts +32 -0
  145. package/dist/components/file-upload-types.js +1 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  147. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  148. package/dist/components/json-schema-form-state.d.ts +19 -0
  149. package/dist/components/list-deletion.svelte.js +6 -1
  150. package/dist/components/list-state.svelte.d.ts +12 -1
  151. package/dist/components/list-state.svelte.js +56 -16
  152. package/dist/components/numeric-format.d.ts +44 -0
  153. package/dist/components/numeric-format.js +198 -0
  154. package/dist/components/record-detail.svelte.js +2 -0
  155. package/dist/components/saved-list-views.d.ts +9 -2
  156. package/dist/components/saved-list-views.js +42 -11
  157. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  158. package/dist/components/saved-list-views.svelte.js +56 -7
  159. package/dist/components/task-action.svelte.d.ts +1 -0
  160. package/dist/components/task-action.svelte.js +55 -3
  161. package/dist/components/task-utils.d.ts +1 -0
  162. package/dist/components/task-utils.js +18 -0
  163. package/dist/components/ui/input/input.svelte +1 -1
  164. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  165. package/dist/components/ui/sheet/sheet.svelte +4 -1
  166. package/dist/components/version-history.d.ts +14 -0
  167. package/dist/components/version-history.js +32 -0
  168. package/dist/components/workspace-tab-state.d.ts +78 -0
  169. package/dist/components/workspace-tab-state.js +587 -0
  170. package/dist/index.d.ts +13 -1
  171. package/dist/index.js +4 -0
  172. package/package.json +5 -5
package/dist/app.css CHANGED
@@ -5830,6 +5830,9 @@
5830
5830
  .overflow-x-auto {
5831
5831
  overflow-x: auto;
5832
5832
  }
5833
+ .rounded {
5834
+ border-radius: 0.25rem;
5835
+ }
5833
5836
  .rounded-lg {
5834
5837
  border-radius: var(--radius);
5835
5838
  }
@@ -8180,6 +8183,7 @@ main {
8180
8183
  position: fixed;
8181
8184
  z-index: 50;
8182
8185
  display: flex;
8186
+ box-sizing: border-box;
8183
8187
  height: 100%;
8184
8188
  width: 75%;
8185
8189
  max-width: 24rem;
@@ -5830,6 +5830,9 @@
5830
5830
  .overflow-x-auto {
5831
5831
  overflow-x: auto;
5832
5832
  }
5833
+ .rounded {
5834
+ border-radius: 0.25rem;
5835
+ }
5833
5836
  .rounded-lg {
5834
5837
  border-radius: var(--radius);
5835
5838
  }
@@ -8180,6 +8183,7 @@ main {
8180
8183
  position: fixed;
8181
8184
  z-index: 50;
8182
8185
  display: flex;
8186
+ box-sizing: border-box;
8183
8187
  height: 100%;
8184
8188
  width: 75%;
8185
8189
  max-width: 24rem;
@@ -4,7 +4,7 @@
4
4
  * Allows users to override any registered component via the `components`
5
5
  * prop on `AdminApp`, while providing sensible defaults.
6
6
  */
7
- import { type Component } from 'svelte';
7
+ import { type Component, type ComponentProps } from 'svelte';
8
8
  import type Layout from './components/Layout.svelte';
9
9
  import type Sidebar from './components/Sidebar.svelte';
10
10
  import type Header from './components/Header.svelte';
@@ -25,7 +25,7 @@ export interface ComponentRegistry {
25
25
  Header: typeof Header;
26
26
  LoginPage: typeof LoginPage;
27
27
  AutoTable: typeof AutoTable;
28
- AutoForm: typeof AutoForm;
28
+ AutoForm: Component<ComponentProps<typeof AutoForm>>;
29
29
  ShowPage: typeof ShowPage;
30
30
  ErrorPage?: typeof ErrorPage;
31
31
  Button: typeof Button;
@@ -8,7 +8,7 @@
8
8
 
9
9
  const i18n = useTranslation();
10
10
  const adminContext = captureAdminContext();
11
- const resources = $derived(adminContext.resources);
11
+ const resources = $derived(adminContext.resources.filter((resource) => resource.meta?.['aboutVisible'] !== false));
12
12
  const providerNames = $derived(adminContext.getDataProviderNames());
13
13
  let { version: suppliedVersion, headingLevel = 'h1' }: { version?: string; headingLevel?: 'h1' | 'h2' } = $props();
14
14
  const buildVersion = '__SVADMIN_VERSION__';
@@ -17,6 +17,25 @@
17
17
  ? (i18n.locale === 'zh-CN' ? '开发构建(未提供版本)' : 'Development build (version unavailable)')
18
18
  : buildVersion
19
19
  ));
20
+ const themeLabelKeys: Record<string, string> = {
21
+ light: 'settings.light',
22
+ dark: 'settings.dark',
23
+ system: 'settings.systemTheme',
24
+ };
25
+ const accentLabelKeys: Record<string, string> = {
26
+ neutral: 'settings.accent.neutral',
27
+ indigo: 'settings.accent.indigo',
28
+ blue: 'settings.accent.blue',
29
+ green: 'settings.accent.green',
30
+ rose: 'settings.accent.rose',
31
+ orange: 'settings.accent.orange',
32
+ violet: 'settings.accent.violet',
33
+ stripe: 'settings.accent.stripe',
34
+ };
35
+ function environmentLabel(value: string, labelKeys: Record<string, string>): string {
36
+ const key = labelKeys[value];
37
+ return key ? i18n.t(key) : value;
38
+ }
20
39
  </script>
21
40
 
22
41
  <div class="svadmin-u-b3542e058833">
@@ -25,12 +44,12 @@
25
44
  <DescriptionList columns={2} items={[
26
45
  { label: i18n.t('settings.version'), value: version },
27
46
  { label: i18n.t('settings.language'), value: i18n.locale },
28
- { label: i18n.t('settings.themeMode'), value: getResolvedTheme() },
29
- { label: i18n.t('settings.colorAccent'), value: getColorTheme() },
47
+ { label: i18n.t('settings.themeMode'), value: environmentLabel(getResolvedTheme(), themeLabelKeys) },
48
+ { label: i18n.t('settings.colorAccent'), value: environmentLabel(getColorTheme(), accentLabelKeys) },
30
49
  ]} />
31
50
  <div class="svadmin-u-b950dda299d3 svadmin-u-18049387f0af svadmin-u-52be28846b5f">
32
51
  <h3 class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('settings.registeredResources')}</h3>
33
- <div class="svadmin-u-eccd13ef4f2f svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-77a2a20e90d4">{#each resources as resource (resource.name)}<Badge variant="outline" class="svadmin-u-0e65706bcccd svadmin-u-359090c2d529">{resource.name}</Badge>{:else}<span class="svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">—</span>{/each}</div>
52
+ <div class="svadmin-u-eccd13ef4f2f svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-77a2a20e90d4">{#each resources as resource (resource.name)}<Badge variant="outline" class="svadmin-u-359090c2d529">{resource.label || resource.name}</Badge>{:else}<span class="svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">—</span>{/each}</div>
34
53
  </div>
35
54
  <div class="svadmin-u-b950dda299d3 svadmin-u-18049387f0af svadmin-u-52be28846b5f">
36
55
  <h3 class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('settings.dataProvider')}</h3>
@@ -1,9 +1,9 @@
1
1
  <script module lang="ts">
2
- import type { Component as SvelteComponent } from 'svelte';
2
+ import type { Component as PageComponent } from 'svelte';
3
3
 
4
4
  export interface AdminPageRoute {
5
5
  readonly path: string;
6
- readonly component: SvelteComponent<{ route: string; params: Record<string, string | undefined> }>;
6
+ readonly component: PageComponent<{ route: string; params: Record<string, string | undefined> }>;
7
7
  readonly title?: string;
8
8
  }
9
9
 
@@ -215,7 +215,7 @@
215
215
  // Default component registry
216
216
  const defaultComponents: ComponentRegistry = {
217
217
  Layout, Sidebar, Header,
218
- LoginPage, AutoTable, AutoForm, ShowPage,
218
+ LoginPage, AutoTable, AutoForm: AutoForm as unknown as ComponentRegistry['AutoForm'], ShowPage,
219
219
  TaskQueueDrawer, ErrorPage,
220
220
  Button, Input, Badge, Skeleton,
221
221
  };
@@ -406,7 +406,7 @@
406
406
  const resource = renderedParams['resource']
407
407
  ? resources.find((candidate) => candidate.name === renderedParams['resource'])
408
408
  : undefined;
409
- let pageLabel = '';
409
+ let pageLabel: string;
410
410
 
411
411
  if (resource) {
412
412
  pageLabel = resource.label || resource.name;
@@ -432,6 +432,8 @@
432
432
  pageLabel = translation.t('auth.login');
433
433
  } else if (renderedRoute === '/' || renderedRoute === '/inventory-dashboard') {
434
434
  pageLabel = translation.t('common.dashboard');
435
+ } else {
436
+ pageLabel = translation.t('common.pageNotFound');
435
437
  }
436
438
 
437
439
  return pageLabel && pageLabel !== title ? `${pageLabel} - ${title}` : title;
@@ -1,7 +1,7 @@
1
- import { SvelteComponent } from "svelte";
1
+ import type { Component as PageComponent } from 'svelte';
2
2
  export interface AdminPageRoute {
3
3
  readonly path: string;
4
- readonly component: SvelteComponent<{
4
+ readonly component: PageComponent<{
5
5
  route: string;
6
6
  params: Record<string, string | undefined>;
7
7
  }>;
@@ -101,6 +101,6 @@ interface Props {
101
101
  /** Optional TanStack Query default options override when QueryClient is not supplied. */
102
102
  queryClientDefaultOptions?: DefaultOptions;
103
103
  }
104
- declare const AdminApp: Component<Props, {}, "locale">;
104
+ declare const AdminApp: PageComponent<Props, {}, "locale">;
105
105
  type AdminApp = ReturnType<typeof AdminApp>;
106
106
  export default AdminApp;
@@ -20,6 +20,11 @@
20
20
  const adminContext = captureAdminContext();
21
21
  const credentialProvider = $derived(adminContext.credentialProvider);
22
22
  const isZh = $derived(i18n.locale === 'zh-CN');
23
+ function permissionLabel(permission: string): string {
24
+ const key = `api.permission.${permission.toLowerCase()}`;
25
+ const translated = i18n.t(key);
26
+ return translated === key ? permission : translated;
27
+ }
23
28
 
24
29
  let keys = $state.raw<ApiCredentialSummary[]>([]);
25
30
  let webhooks = $state.raw<WebhookSummary[]>([]);
@@ -32,7 +37,7 @@
32
37
  let newWebhookUrl = $state('');
33
38
  let newWebhookEvent = $state('resource.created');
34
39
  let loading = $state(false);
35
- let error = $state<string | null>(null);
40
+ let error = $state(false);
36
41
  let submitting = $state(false);
37
42
  let requestId = 0;
38
43
  let pendingDanger = $state<{ label: string; run: () => Promise<void> } | null>(null);
@@ -46,8 +51,8 @@
46
51
  } };
47
52
  }
48
53
 
49
- function message(error: unknown): string {
50
- return error instanceof Error ? error.message : String(error);
54
+ function failureMessage(): string {
55
+ return i18n.t('common.operationFailed');
51
56
  }
52
57
 
53
58
  async function loadCredentials(): Promise<void> {
@@ -59,7 +64,7 @@
59
64
  generatedSecret = '';
60
65
  copied = false;
61
66
  copyError = '';
62
- error = null;
67
+ error = false;
63
68
  pendingDanger = null;
64
69
  deleting = false;
65
70
  loading = false;
@@ -71,8 +76,8 @@
71
76
  if (currentRequest !== requestId) return;
72
77
  keys = nextKeys;
73
78
  webhooks = nextWebhooks;
74
- } catch (caught) {
75
- if (currentRequest === requestId) error = message(caught);
79
+ } catch {
80
+ if (currentRequest === requestId) error = true;
76
81
  } finally {
77
82
  if (currentRequest === requestId) loading = false;
78
83
  }
@@ -90,7 +95,7 @@
90
95
  const provider = credentialProvider;
91
96
  const currentRequest = requestId;
92
97
  const context = adminContext.enterpriseRequestContext;
93
- if (!provider || !newWebhookName.trim() || !newWebhookUrl.trim()) return;
98
+ if (!provider || submitting || !newWebhookName.trim() || !newWebhookUrl.trim()) return;
94
99
  submitting = true;
95
100
  try {
96
101
  const webhook = await provider.createWebhook({ name: newWebhookName.trim(), url: newWebhookUrl.trim(), eventType: newWebhookEvent }, context);
@@ -98,9 +103,9 @@
98
103
  webhooks = [...webhooks, webhook];
99
104
  newWebhookName = '';
100
105
  newWebhookUrl = '';
101
- } catch (caught) {
106
+ } catch {
102
107
  if (currentRequest !== requestId) return;
103
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
108
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
104
109
  } finally {
105
110
  if (currentRequest === requestId) submitting = false;
106
111
  }
@@ -111,7 +116,7 @@
111
116
  const provider = credentialProvider;
112
117
  const currentRequest = requestId;
113
118
  const context = adminContext.enterpriseRequestContext;
114
- if (!provider || !newKeyName.trim()) return;
119
+ if (!provider || submitting || !newKeyName.trim()) return;
115
120
  submitting = true;
116
121
  try {
117
122
  const permissions = Object.entries(newKeyPermissions).filter(([, enabled]) => enabled).map(([name]) => name);
@@ -123,9 +128,9 @@
123
128
  copyError = '';
124
129
  newKeyName = '';
125
130
  newKeyPermissions = { Read: true, Write: false, Delete: false, Admin: false };
126
- } catch (caught) {
131
+ } catch {
127
132
  if (currentRequest !== requestId) return;
128
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
133
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
129
134
  } finally {
130
135
  if (currentRequest === requestId) submitting = false;
131
136
  }
@@ -142,12 +147,12 @@
142
147
  if (!provider) return;
143
148
  try {
144
149
  const result = await provider.revokeApiCredential(id, context);
145
- if (!result.success) throw new Error(result.error?.message ?? (isZh ? '密钥撤销失败' : 'Credential revocation failed'));
150
+ if (!result.success) throw new Error('Credential revocation failed');
146
151
  if (currentRequest !== requestId) return;
147
152
  keys = keys.filter((key) => key.id !== id);
148
- } catch (caught) {
153
+ } catch {
149
154
  if (currentRequest !== requestId) return;
150
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
155
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
151
156
  }
152
157
  }
153
158
 
@@ -162,12 +167,12 @@
162
167
  if (!provider) return;
163
168
  try {
164
169
  const result = await provider.deleteWebhook(id, context);
165
- if (!result.success) throw new Error(result.error?.message ?? (isZh ? 'Webhook 删除失败' : 'Webhook deletion failed'));
170
+ if (!result.success) throw new Error('Webhook deletion failed');
166
171
  if (currentRequest !== requestId) return;
167
172
  webhooks = webhooks.filter((webhook) => webhook.id !== id);
168
- } catch (caught) {
173
+ } catch {
169
174
  if (currentRequest !== requestId) return;
170
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
175
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
171
176
  }
172
177
  }
173
178
 
@@ -200,13 +205,13 @@
200
205
  {/if}
201
206
  {/snippet}
202
207
  {#snippet primary()}
208
+ {#if loading}
209
+ <DataState state="loading" />
210
+ {:else if error}
211
+ <DataState state="error" description={failureMessage()} retry={loadCredentials} />
212
+ {:else}
203
213
  <div class="svadmin-u-b3542e058833">
204
214
  <SettingsGroup title={i18n.t('api.activeKeysTitle')} description={i18n.t('api.activeKeysDesc')} bodyClass="p-0">
205
- {#if loading}
206
- <DataState state="loading" />
207
- {:else if error}
208
- <DataState state="error" description={error} retry={loadCredentials} />
209
- {:else}
210
215
  <ApiKeyList
211
216
  class="svadmin-u-0c5e9137c7de svadmin-u-119b2aa0b8f6"
212
217
  keys={keys.map((key) => definedOptions({
@@ -219,7 +224,6 @@
219
224
  stateDescription={credentialProvider ? i18n.t('api.generateDesc') : (isZh ? '配置 CredentialProvider 后可管理真实凭据。' : 'Configure CredentialProvider to manage real credentials.')}
220
225
  {...definedOptions({ onrevoke: credentialProvider ? (key: { id: string }) => revokeKey(key.id) : undefined })}
221
226
  />
222
- {/if}
223
227
  </SettingsGroup>
224
228
  <SettingsGroup title={i18n.t('api.webhooks')} description={i18n.t('api.webhooksDesc')} bodyClass="space-y-5">
225
229
  <form class="svadmin-u-f3c543ad5fe9 svadmin-u-7e0b7cdf1a94 svadmin-u-0c3bc98565dd svadmin-u-e4d6f343b9ff" onsubmit={handleAddWebhook}>
@@ -244,23 +248,35 @@
244
248
  <Button type="submit" class="svadmin-u-6da6a3c3f741" disabled={!credentialProvider || submitting || !newWebhookName.trim() || !newWebhookUrl.trim()}><Plus class="svadmin-u-f7b5fa971871" />{i18n.t('api.addWebhook')}</Button>
245
249
  </div>
246
250
  </form>
247
- {#if webhooks.length === 0}<DataState state="empty" title={i18n.t('api.noWebhooks')} description={credentialProvider ? i18n.t('api.webhooksDesc') : (isZh ? '配置 CredentialProvider 后可管理真实 Webhook。' : 'Configure CredentialProvider to manage real webhooks.')} />{:else}<div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-b950dda299d3 svadmin-u-18049387f0af">{#each webhooks as hook (hook.id)}<div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-1004c0c3954c svadmin-u-cb11fec3bb46 svadmin-u-020ba687fa12 svadmin-u-9f76a62f4f44 svadmin-u-3b9871a0bf93"><div class="svadmin-u-7e0b7cdf1a94"><p class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{hook.name}</p><p class="svadmin-u-f283ea9bea0e svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-bfa603190748">{hook.url}</p></div><div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-1004c0c3954c svadmin-u-1f51d781e606"><span class="svadmin-u-07389a777c1f svadmin-u-2ef11f1cb219 svadmin-u-d5eab218aa34 svadmin-u-660d2effb880 svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-bfa603190748">{hook.eventType}</span><Button variant="ghost" size="icon-sm" onclick={() => deleteWebhook(hook.id)} aria-label={i18n.t('common.delete') + ' ' + hook.name}><Trash2 class="svadmin-u-f7b5fa971871" /></Button></div></div>{/each}</div>{/if}
251
+ {#if webhooks.length === 0}
252
+ <DataState state="empty" title={i18n.t('api.noWebhooks')} description={credentialProvider ? i18n.t('api.webhooksDesc') : (isZh ? '配置 CredentialProvider 后可管理真实 Webhook。' : 'Configure CredentialProvider to manage real webhooks.')} />
253
+ {:else}
254
+ <div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-b950dda299d3 svadmin-u-18049387f0af">
255
+ {#each webhooks as hook (hook.id)}
256
+ <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-1004c0c3954c svadmin-u-cb11fec3bb46 svadmin-u-020ba687fa12 svadmin-u-9f76a62f4f44 svadmin-u-3b9871a0bf93">
257
+ <div class="svadmin-u-7e0b7cdf1a94"><p class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{hook.name}</p><p class="svadmin-u-f283ea9bea0e svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-bfa603190748">{hook.url}</p></div>
258
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-1004c0c3954c svadmin-u-1f51d781e606"><span class="svadmin-u-07389a777c1f svadmin-u-2ef11f1cb219 svadmin-u-d5eab218aa34 svadmin-u-660d2effb880 svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-bfa603190748">{hook.eventType}</span><Button variant="ghost" size="icon-sm" onclick={() => deleteWebhook(hook.id)} aria-label={i18n.t('common.delete') + ' ' + hook.name}><Trash2 class="svadmin-u-f7b5fa971871" /></Button></div>
259
+ </div>
260
+ {/each}
261
+ </div>
262
+ {/if}
248
263
  </SettingsGroup>
249
264
  </div>
265
+ {/if}
250
266
  {/snippet}
251
267
  {#snippet secondary()}
252
268
  <SettingsGroup title={i18n.t('api.generateTitle')} description={i18n.t('api.generateDesc')}>
253
269
  <form onsubmit={handleGenerateKey} class="svadmin-u-b43b4c086d9a">
254
270
  <div class="svadmin-u-6f7e013d6499">
255
271
  <Label for={`${controlId}-api-key-name`}>{i18n.t('api.keyName')}</Label>
256
- <Input id={`${controlId}-api-key-name`} placeholder="CI deployment token" bind:value={newKeyName} required disabled={!credentialProvider || submitting} />
272
+ <Input id={`${controlId}-api-key-name`} placeholder={i18n.t('api.keyNamePlaceholder')} bind:value={newKeyName} required disabled={!credentialProvider || submitting} />
257
273
  </div>
258
274
  <fieldset class="svadmin-u-6ed543e2fbbb" disabled={!credentialProvider || submitting}>
259
275
  <legend class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('api.permissions')}</legend>
260
276
  {#each Object.keys(newKeyPermissions) as permission (permission)}
261
277
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
262
278
  <Checkbox id={`${controlId}-perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} />
263
- <Label for={`${controlId}-perm-${permission}`} class="svadmin-u-34516836730d svadmin-u-fc7473ca09eb svadmin-u-8ecebc9f80e6">{permission}</Label>
279
+ <Label for={`${controlId}-perm-${permission}`} class="svadmin-u-34516836730d svadmin-u-fc7473ca09eb svadmin-u-8ecebc9f80e6">{permissionLabel(permission)}</Label>
264
280
  </div>
265
281
  {/each}
266
282
  </fieldset>
@@ -67,9 +67,8 @@
67
67
  }
68
68
  });
69
69
 
70
- function failureMessage(caught: unknown): string {
71
- if (caught instanceof Error && caught.message.trim()) return caught.message;
72
- return isZh ? '操作失败,请检查输入后重试。' : 'Action failed. Check the input and retry.';
70
+ function failureMessage(): string {
71
+ return i18n.t('common.operationFailed');
73
72
  }
74
73
 
75
74
  async function handleApprove() {
@@ -79,8 +78,8 @@
79
78
  await onapprove?.(approveComment || undefined);
80
79
  approveDialogOpen = false;
81
80
  approveComment = '';
82
- } catch (caught) {
83
- errorMessage = failureMessage(caught);
81
+ } catch {
82
+ errorMessage = failureMessage();
84
83
  } finally {
85
84
  isSubmitting = false;
86
85
  }
@@ -94,8 +93,8 @@
94
93
  await onreject?.(rejectReason);
95
94
  rejectDialogOpen = false;
96
95
  rejectReason = '';
97
- } catch (caught) {
98
- errorMessage = failureMessage(caught);
96
+ } catch {
97
+ errorMessage = failureMessage();
99
98
  } finally {
100
99
  isSubmitting = false;
101
100
  }
@@ -109,8 +108,8 @@
109
108
  await ontransfer?.(transferTarget);
110
109
  transferDialogOpen = false;
111
110
  transferTarget = '';
112
- } catch (caught) {
113
- errorMessage = failureMessage(caught);
111
+ } catch {
112
+ errorMessage = failureMessage();
114
113
  } finally {
115
114
  isSubmitting = false;
116
115
  }
@@ -45,6 +45,7 @@
45
45
  let epoch = 0;
46
46
  let requestScope: EnterpriseProviderRequestContext | undefined;
47
47
  let generatedRequestContext: EnterpriseProviderRequestContext | undefined;
48
+ let lastTransition: ApprovalTransition | undefined;
48
49
  onDestroy(() => { mounted = false; clearTimeout(timer); });
49
50
 
50
51
  const activeRecord = $derived(records.find(record => record.id === selectedId));
@@ -73,9 +74,14 @@
73
74
  }
74
75
 
75
76
  async function load(): Promise<void> {
77
+ if (actionPending) return;
76
78
  const currentEpoch = ++epoch;
79
+ lastTransition = undefined;
77
80
  loading = true;
78
81
  error = undefined;
82
+ records = [];
83
+ actionDialog = undefined;
84
+ actionOpen = false;
79
85
  try {
80
86
  const scope = resolveRequestContext();
81
87
  requestScope = scope;
@@ -124,9 +130,11 @@
124
130
  }
125
131
 
126
132
  function openAction(action: string): void {
133
+ if (loading || actionPending) return;
127
134
  if (!activeRecord?.allowedActions.some(candidate => candidate.id === action)) return;
128
135
  actionDialog = action;
129
136
  actionOpen = true;
137
+ error = undefined;
130
138
  comment = '';
131
139
  targetId = '';
132
140
  }
@@ -135,21 +143,30 @@
135
143
  const record = activeRecord;
136
144
  const action = activeAction;
137
145
  const scope = requestScope;
138
- if (!record || !action || !scope || !provider.transition || actionPending) return;
146
+ if (!record || !action || !scope || !provider.transition || actionPending || loading) return;
139
147
  let input: ApprovalTransition;
140
148
  try {
149
+ const target = targetId.trim() || undefined;
150
+ // 原样重试复用标识;不同载荷不能共用幂等键。
151
+ const previous = lastTransition;
152
+ const retry = previous?.id === record.id
153
+ && previous.expectedVersion === record.version
154
+ && previous.action === action.id
155
+ && previous.comment === comment
156
+ && previous.targetId === target;
141
157
  input = decodeApprovalTransition({
142
158
  id: record.id,
143
159
  expectedVersion: record.version,
144
160
  action: action.id,
145
161
  comment,
146
- ...(targetId.trim() ? { targetId: targetId.trim() } : {}),
147
- idempotencyKey: `${record.id}:${record.version}:${action.id}:${crypto.randomUUID()}`,
162
+ ...(target ? { targetId: target } : {}),
163
+ idempotencyKey: retry ? previous.idempotencyKey : `${record.id}:${record.version}:${action.id}:${crypto.randomUUID()}`,
148
164
  }, record);
149
165
  } catch {
150
166
  error = 'action';
151
167
  return;
152
168
  }
169
+ lastTransition = { ...input };
153
170
  actionPending = true;
154
171
  const actionEpoch = epoch;
155
172
  error = undefined;
@@ -160,6 +177,7 @@
160
177
  error = 'conflict';
161
178
  return;
162
179
  }
180
+ lastTransition = undefined;
163
181
  actionDialog = undefined;
164
182
  actionOpen = false;
165
183
  records = records.map(candidate => candidate.id === receipt.record.id ? receipt.record : candidate);
@@ -201,7 +219,7 @@
201
219
  <div>
202
220
  <h2>{title ?? i18n.t('approval.title')}</h2>
203
221
  </div>
204
- <Button type="button" variant="outline" size="sm" aria-label={i18n.t('common.refresh')} onclick={() => void load()}>
222
+ <Button type="button" variant="outline" size="sm" aria-label={i18n.t('common.refresh')} disabled={loading || actionPending} onclick={() => void load()}>
205
223
  <RefreshCw class={loading ? 'svadmin-approval-center__spin' : ''} />
206
224
  </Button>
207
225
  </header>
@@ -213,7 +231,7 @@
213
231
  <Input value={search} oninput={scheduleSearch} placeholder={i18n.t('approval.search')} aria-label={i18n.t('approval.search')} />
214
232
  </label>
215
233
  </div>
216
- {#if error && records.length > 0}
234
+ {#if error && records.length > 0 && !actionOpen}
217
235
  <div role="alert">{i18n.t(`approval.${error}`)}</div>
218
236
  {/if}
219
237
  <div class="svadmin-approval-center__body" data-svadmin-approval-body>
@@ -293,6 +311,7 @@
293
311
  <Input bind:value={targetId} required maxlength={200} />
294
312
  </label>
295
313
  {/if}
314
+ {#if error}<div role="alert">{i18n.t(`approval.${error}`)}</div>{/if}
296
315
  <Dialog.Footer>
297
316
  <Button type="button" variant="outline" onclick={() => { actionOpen = false; actionDialog = undefined; }}>{i18n.t('common.cancel')}</Button>
298
317
  <Button type="button" disabled={actionPending || !activeAction || !canSubmit(activeAction)} onclick={() => void submitAction()}>
@@ -56,9 +56,9 @@
56
56
  if (currentId !== requestId) return;
57
57
  logs = result.data;
58
58
  total = result.total;
59
- } catch (e) {
59
+ } catch {
60
60
  if (currentId !== requestId) return;
61
- error = e instanceof Error ? e.message : String(e);
61
+ error = i18n.t('common.error') ?? 'Something went wrong';
62
62
  toast.error(error);
63
63
  } finally {
64
64
  if (currentId === requestId) loading = false;