@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.
Files changed (171) 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 +263 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +135 -35
  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 +60 -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/Transfer.svelte +67 -5
  109. package/dist/components/Transfer.svelte.d.ts +4 -0
  110. package/dist/components/TreeSelect.svelte +337 -77
  111. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  112. package/dist/components/TreeTable.svelte +6 -1
  113. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  114. package/dist/components/UpdatePasswordPage.svelte +1 -1
  115. package/dist/components/VersionDiffViewer.svelte +9 -7
  116. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  117. package/dist/components/account/MemberDirectory.svelte +14 -10
  118. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  119. package/dist/components/content/AuditTimeline.svelte +8 -6
  120. package/dist/components/content/DataState.svelte +1 -1
  121. package/dist/components/content/FileList.svelte +44 -13
  122. package/dist/components/content/FileList.svelte.d.ts +4 -2
  123. package/dist/components/content/FileListRow.svelte +87 -0
  124. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  125. package/dist/components/content/ImportDropzone.svelte +109 -12
  126. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  127. package/dist/components/content/IntegrationCard.svelte +3 -3
  128. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  129. package/dist/components/content/MediaThumbnail.svelte +6 -2
  130. package/dist/components/detail-workspace.d.ts +60 -0
  131. package/dist/components/detail-workspace.js +3 -0
  132. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  133. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  134. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  135. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  136. package/dist/components/enterprise/schema-form-references.js +316 -0
  137. package/dist/components/fields/ArrayField.svelte +178 -14
  138. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  139. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  140. package/dist/components/fields/array-field-metadata.js +36 -0
  141. package/dist/components/file-upload-reference.d.ts +11 -0
  142. package/dist/components/file-upload-reference.js +50 -0
  143. package/dist/components/file-upload-types.d.ts +32 -0
  144. package/dist/components/file-upload-types.js +1 -0
  145. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  147. package/dist/components/json-schema-form-state.d.ts +19 -0
  148. package/dist/components/list-deletion.svelte.js +6 -1
  149. package/dist/components/list-state.svelte.d.ts +12 -1
  150. package/dist/components/list-state.svelte.js +56 -16
  151. package/dist/components/numeric-format.d.ts +44 -0
  152. package/dist/components/numeric-format.js +198 -0
  153. package/dist/components/record-detail.svelte.js +2 -0
  154. package/dist/components/saved-list-views.d.ts +9 -2
  155. package/dist/components/saved-list-views.js +42 -11
  156. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  157. package/dist/components/saved-list-views.svelte.js +56 -7
  158. package/dist/components/task-action.svelte.d.ts +1 -0
  159. package/dist/components/task-action.svelte.js +55 -3
  160. package/dist/components/task-utils.d.ts +1 -0
  161. package/dist/components/task-utils.js +18 -0
  162. package/dist/components/ui/input/input.svelte +1 -1
  163. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  164. package/dist/components/ui/sheet/sheet.svelte +4 -1
  165. package/dist/components/version-history.d.ts +14 -0
  166. package/dist/components/version-history.js +32 -0
  167. package/dist/components/workspace-tab-state.d.ts +78 -0
  168. package/dist/components/workspace-tab-state.js +587 -0
  169. package/dist/index.d.ts +13 -1
  170. package/dist/index.js +4 -0
  171. package/package.json +5 -5
@@ -4,6 +4,7 @@
4
4
  import { Plus, GripVertical, RotateCw, ArrowUp, ArrowDown } from '@lucide/svelte';
5
5
  import { cn } from '../utils.js';
6
6
  import { useTranslation } from '@svadmin/core/i18n';
7
+ import { snapshotPlainData } from '@svadmin/core/schema';
7
8
  import { onDestroy } from 'svelte';
8
9
 
9
10
  export interface KanbanCard {
@@ -27,9 +28,9 @@
27
28
  columns: KanbanColumn[];
28
29
  cards?: KanbanCard[];
29
30
  allowAddCard?: boolean;
30
- oncardmove?: (cardId: string, targetColumnId: string, targetIndex: number) => void | Promise<void>;
31
+ oncardmove?: ((cardId: string, targetColumnId: string, targetIndex: number) => KanbanCard | undefined | Promise<KanbanCard | undefined>) | undefined;
31
32
  canMoveCard?: (card: KanbanCard, targetColumnId: string) => boolean;
32
- oncardmoveerror?: (error: unknown) => void;
33
+ oncardmoveerror?: ((error: unknown) => void) | undefined;
33
34
  oncardclick?: (card: KanbanCard) => void;
34
35
  oncardadd?: (columnId: string, title: string) => KanbanCard | Promise<KanbanCard>;
35
36
  scopeKey?: string | number;
@@ -62,6 +63,7 @@
62
63
  let newCardTitle = $state('');
63
64
  let movingCardId = $state<string | null>(null);
64
65
  let moveError = $state<string | null>(null);
66
+ let failedMove = $state<{ cardId: string; targetColumnId: string; targetIndex: number } | null>(null);
65
67
  let adding = $state(false);
66
68
  let mounted = true;
67
69
  const i18n = useTranslation();
@@ -100,11 +102,26 @@
100
102
  && (card.dueDate === undefined || typeof card.dueDate === 'string')
101
103
  && (card.priority === undefined || ['low', 'medium', 'high', 'urgent'].includes(card.priority));
102
104
  }
105
+ function decodeMoveReceipt(receipt: unknown): KanbanCard | undefined {
106
+ if (receipt === undefined) return undefined;
107
+ if (receipt === null || typeof receipt !== 'object' || Array.isArray(receipt)) {
108
+ throw new Error('Invalid move receipt');
109
+ }
110
+ // 将已提交卡片与回调持有方的可变对象图分离,避免别名修改已提交状态。
111
+ const snapshot = snapshotPlainData(receipt);
112
+ if (snapshot === null || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
113
+ throw new Error('Invalid move receipt');
114
+ }
115
+ return snapshot as unknown as KanbanCard;
116
+ }
103
117
  function moveAllowed(card: KanbanCard, targetColumn: string): boolean {
104
118
  if (!oncardmove || loading || displayError || busy || !columns.some(column => column.id === targetColumn)) return false;
105
119
  try { return canMoveCard === undefined || canMoveCard(card, targetColumn) === true; }
106
120
  catch { return false; }
107
121
  }
122
+ function canDragCard(card: KanbanCard): boolean {
123
+ return !!oncardmove && !loading && !displayError && !busy && columns.some(column => moveAllowed(card, column.id));
124
+ }
108
125
  function active(origin: typeof scope): boolean {
109
126
  return mounted && origin === scope;
110
127
  }
@@ -114,11 +131,13 @@
114
131
  addingColumnId = null;
115
132
  newCardTitle = '';
116
133
  moveError = null;
134
+ failedMove = null;
117
135
  });
118
136
  onDestroy(() => { mounted = false; });
119
137
 
120
138
  function handleDragStart(e: DragEvent, cardId: string) {
121
- if (!oncardmove || busy || loading || displayError) { e.preventDefault(); return; }
139
+ const card = cards.find((c) => c.id === cardId);
140
+ if (!card || !canDragCard(card)) { e.preventDefault(); return; }
122
141
  draggedCardId = cardId;
123
142
  if (e.dataTransfer) {
124
143
  e.dataTransfer.effectAllowed = 'move';
@@ -151,12 +170,23 @@
151
170
  adding = false;
152
171
  movingCardId = card.id;
153
172
  moveError = null;
173
+ failedMove = null;
154
174
  try {
155
- await origin.oncardmove?.(card.id, targetColId, index);
156
- if (active(origin) && operation === current) cards = optimistic;
175
+ const receipt = await origin.oncardmove?.(card.id, targetColId, index);
176
+ if (!active(origin) || operation !== current) return;
177
+ let confirmed = optimistic;
178
+ if (receipt !== undefined) {
179
+ const authoritative = decodeMoveReceipt(receipt);
180
+ if (!authoritative || !validCard(authoritative) || authoritative.id !== card.id || authoritative.columnId !== targetColId) {
181
+ throw new Error('Invalid move receipt');
182
+ }
183
+ confirmed = optimistic.map(item => item.id === card.id ? authoritative : item);
184
+ }
185
+ cards = confirmed;
157
186
  } catch (cause) {
158
187
  if (active(origin) && operation === current) {
159
188
  moveError = i18n.t('kanban.moveFailed');
189
+ failedMove = { cardId: card.id, targetColumnId: targetColId, targetIndex: index };
160
190
  try { oncardmoveerror?.(cause); } catch { /* 错误观察者不能再次触发写入。 */ }
161
191
  }
162
192
  } finally {
@@ -164,6 +194,12 @@
164
194
  }
165
195
  }
166
196
 
197
+ function retryFailedMove() {
198
+ const failed = failedMove;
199
+ if (!failed || busy) return;
200
+ void moveCard(failed.cardId, failed.targetColumnId, failed.targetIndex);
201
+ }
202
+
167
203
  function handleDrop(targetColId: string, index?: number) {
168
204
  const id = draggedCardId;
169
205
  draggedCardId = null;
@@ -208,7 +244,7 @@
208
244
  }
209
245
  </script>
210
246
 
211
- {#if moveError}<div role="alert">{moveError}</div>{/if}
247
+ {#if moveError}<div role="alert"><span>{moveError}</span>{#if failedMove}<Button type="button" variant="ghost" size="sm" disabled={busy} onclick={retryFailedMove}>{i18n.t('common.retry')}</Button>{/if}</div>{/if}
212
248
  <div role="region" aria-label={ariaLabel ?? i18n.t('kanban.label')} aria-busy={loading || busy}
213
249
  class={cn('svadmin-u-60fbb7713999 svadmin-u-0c3bc98565dd svadmin-u-1384f66f41d0 svadmin-u-9fcd8a13827e svadmin-u-359090c2d529 svadmin-u-7f6912283f11', className)}>
214
250
  {#if loading}
@@ -265,7 +301,7 @@
265
301
  role="group"
266
302
  aria-label={card.title}
267
303
  aria-busy={busy && movingCardId === card.id}
268
- draggable={!!oncardmove && !busy}
304
+ draggable={canDragCard(card)}
269
305
  class={cn(
270
306
  'group svadmin-u-d89972fe17d6 svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-77a2a20e90d4 svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-05faf5c801ff svadmin-u-cd0ad9a56558 svadmin-u-eb6e8b881acd svadmin-u-cef5b893cf23 svadmin-u-0fe7d7d814d0 svadmin-u-8d08385288a6 svadmin-u-d9bff91ef7ee svadmin-u-512e82e6a68f svadmin-u-ab1dd417ce7d svadmin-u-2eba0d65d059',
271
307
  draggedCardId === card.id ? 'svadmin-u-2a2db4667b27 svadmin-u-16b1efa5875e svadmin-u-2691847b25ed' : ''
@@ -20,9 +20,9 @@ interface Props {
20
20
  columns: KanbanColumn[];
21
21
  cards?: KanbanCard[];
22
22
  allowAddCard?: boolean;
23
- oncardmove?: (cardId: string, targetColumnId: string, targetIndex: number) => void | Promise<void>;
23
+ oncardmove?: ((cardId: string, targetColumnId: string, targetIndex: number) => KanbanCard | undefined | Promise<KanbanCard | undefined>) | undefined;
24
24
  canMoveCard?: (card: KanbanCard, targetColumnId: string) => boolean;
25
- oncardmoveerror?: (error: unknown) => void;
25
+ oncardmoveerror?: ((error: unknown) => void) | undefined;
26
26
  oncardclick?: (card: KanbanCard) => void;
27
27
  oncardadd?: (columnId: string, title: string) => KanbanCard | Promise<KanbanCard>;
28
28
  scopeKey?: string | number;
@@ -125,7 +125,7 @@
125
125
 
126
126
  function handleGlobalKeydown(e: KeyboardEvent) {
127
127
  if (!ownsLayoutEvent(e)) return;
128
- if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
128
+ if ((e.ctrlKey || e.metaKey) && (e.key.toLowerCase() === 'k' || e.code === 'KeyK')) {
129
129
  e.preventDefault();
130
130
  commandOpen = true;
131
131
  }
@@ -69,7 +69,7 @@
69
69
  if (result.success) {
70
70
  onSuccess?.();
71
71
  } else {
72
- error = result.error?.message ?? i18n.t('common.loginFailed');
72
+ error = i18n.t('common.loginFailed');
73
73
  }
74
74
  }
75
75
  </script>
@@ -16,6 +16,7 @@
16
16
  };
17
17
  icon?: import('svelte').Component<{ class?: string }>;
18
18
  href?: string;
19
+ onActivate?: () => void;
19
20
  trend?: {
20
21
  value: number;
21
22
  label?: string;
@@ -52,10 +53,14 @@
52
53
  {@render children()}
53
54
  {:else}
54
55
  {#each items as item, index (item.id || index)}
55
- {@const isInteractive = Boolean(item.href)}
56
+ {@const hasActivation = Boolean(item.onActivate)}
57
+ {@const isInteractive = Boolean(item.href) || hasActivation}
56
58
  <svelte:element
57
- this={isInteractive ? 'a' : 'div'}
58
- href={item.href}
59
+ this={hasActivation ? 'button' : item.href ? 'a' : 'div'}
60
+ type={hasActivation ? 'button' : undefined}
61
+ href={hasActivation ? undefined : item.href}
62
+ onclick={item.onActivate}
63
+ role={hasActivation ? 'button' : item.href ? 'link' : undefined}
59
64
  data-interactive={isInteractive ? 'true' : undefined}
60
65
  data-tone={item.tone || 'default'}
61
66
  class={cn('svadmin-metric-item', item.class)}
@@ -13,6 +13,7 @@ export interface MetricStripItem {
13
13
  class?: string;
14
14
  }>;
15
15
  href?: string;
16
+ onActivate?: () => void;
16
17
  trend?: {
17
18
  value: number;
18
19
  label?: string;
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
2
+ import { onDestroy, type Snippet } from 'svelte';
3
3
  import * as Dialog from './ui/dialog/index.js';
4
4
  import { Button } from './ui/button/index.js';
5
5
  import { Loader2 } from '@lucide/svelte';
6
6
  import { cn } from '../utils.js';
7
+ import { useTranslation } from '@svadmin/core/i18n';
8
+ import FeedbackNotice from './content/FeedbackNotice.svelte';
7
9
 
8
10
  interface Props {
9
11
  open?: boolean;
@@ -17,8 +19,8 @@
17
19
  widthClass?: string;
18
20
  isSubmitting?: boolean;
19
21
  disabled?: boolean;
20
- onsubmit?: () => void | Promise<void>;
21
- oncancel?: () => void;
22
+ onsubmit?: (() => void | Promise<void>) | undefined;
23
+ oncancel?: (() => void) | undefined;
22
24
  class?: string;
23
25
  triggerSnippet?: Snippet;
24
26
  children?: Snippet;
@@ -43,26 +45,96 @@
43
45
  children,
44
46
  }: Props = $props();
45
47
 
48
+ const i18n = useTranslation();
46
49
  let loading = $state(false);
50
+ let submitError = $state('');
51
+ let round = 0;
52
+ let nextRequestId = 0;
53
+ let activeRequest: { id: number; round: number; onsubmit?: Props['onsubmit'] } | undefined;
54
+ let formElement: HTMLFormElement | undefined;
55
+ let destroyed = false;
56
+ const busy = $derived(loading || isSubmitting);
57
+
58
+ let lastOpen = open;
59
+ let lastOnsubmit: Props['onsubmit'];
60
+ let hasLastOnsubmit = false;
61
+ function resetRequestState() {
62
+ round += 1;
63
+ activeRequest = undefined;
64
+ loading = false;
65
+ submitError = '';
66
+ }
67
+
68
+ $effect(() => {
69
+ if (open === lastOpen) return;
70
+ lastOpen = open;
71
+ resetRequestState();
72
+ });
73
+
74
+ $effect(() => {
75
+ const handler = onsubmit;
76
+ if (!hasLastOnsubmit) {
77
+ hasLastOnsubmit = true;
78
+ lastOnsubmit = handler;
79
+ return;
80
+ }
81
+ if (handler === lastOnsubmit) return;
82
+ lastOnsubmit = handler;
83
+ resetRequestState();
84
+ });
85
+
86
+ onDestroy(() => {
87
+ destroyed = true;
88
+ formElement = undefined;
89
+ resetRequestState();
90
+ });
91
+
92
+ function isCurrentRequest(token: { id: number; round: number; onsubmit?: Props['onsubmit'] }) {
93
+ return !destroyed && activeRequest === token && token.round === round && token.onsubmit === onsubmit;
94
+ }
47
95
 
48
96
  async function handleSubmit(e: SubmitEvent) {
49
97
  e.preventDefault();
98
+ if (!open || destroyed || e.currentTarget !== formElement || disabled || busy) return;
99
+ const token = { id: ++nextRequestId, round, onsubmit };
100
+ activeRequest = token;
50
101
  loading = true;
102
+ submitError = '';
51
103
  try {
52
104
  await onsubmit?.();
105
+ if (!isCurrentRequest(token)) return;
53
106
  open = false;
107
+ } catch {
108
+ if (!isCurrentRequest(token)) return;
109
+ submitError = i18n.t('common.operationFailed');
54
110
  } finally {
55
- loading = false;
111
+ if (activeRequest === token) {
112
+ activeRequest = undefined;
113
+ loading = false;
114
+ }
56
115
  }
57
116
  }
58
117
 
59
118
  function handleCancel() {
119
+ if (busy) return;
60
120
  oncancel?.();
61
121
  open = false;
62
122
  }
123
+
124
+ function handleOpenChange(next: boolean) {
125
+ if (next) {
126
+ open = true;
127
+ return;
128
+ }
129
+ if (busy) {
130
+ open = true;
131
+ return;
132
+ }
133
+ open = false;
134
+ }
63
135
  </script>
64
136
 
65
- <Dialog.Root bind:open>
137
+ <Dialog.Root bind:open onOpenChange={handleOpenChange}>
66
138
  {#if triggerSnippet}
67
139
  <Dialog.Trigger>
68
140
  {#snippet child({ props })}
@@ -82,7 +154,7 @@
82
154
  {/if}
83
155
 
84
156
  <Dialog.Content class={cn(widthClass, 'svadmin-u-b4168890eac6 svadmin-u-92bf82f493b1', className)}>
85
- <form onsubmit={handleSubmit} class="svadmin-u-3e7ce58d64fa">
157
+ <form bind:this={formElement} onsubmit={handleSubmit} aria-busy={busy} class="svadmin-u-3e7ce58d64fa">
86
158
  <Dialog.Header>
87
159
  <Dialog.Title>{title}</Dialog.Title>
88
160
  {#if description}
@@ -90,6 +162,10 @@
90
162
  {/if}
91
163
  </Dialog.Header>
92
164
 
165
+ {#if submitError}
166
+ <FeedbackNotice tone="danger" message={submitError} />
167
+ {/if}
168
+
93
169
  <div class="svadmin-u-03b4dd7f172b svadmin-u-fc7473ca09eb svadmin-u-d4108abe6359">
94
170
  {#if children}
95
171
  {@render children()}
@@ -101,7 +177,7 @@
101
177
  type="button"
102
178
  variant="outline"
103
179
  size="sm"
104
- disabled={loading || isSubmitting}
180
+ disabled={busy}
105
181
  onclick={handleCancel}
106
182
  >
107
183
  {cancelText}
@@ -109,10 +185,10 @@
109
185
  <Button
110
186
  type="submit"
111
187
  size="sm"
112
- disabled={loading || isSubmitting || disabled}
188
+ disabled={busy || disabled}
113
189
  class="svadmin-u-58284b4ea568 svadmin-u-2e7a6d18a20d"
114
190
  >
115
- {#if loading || isSubmitting}
191
+ {#if busy}
116
192
  <Loader2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-afbdd13a380e" />
117
193
  {/if}
118
194
  {submitText}
@@ -1,4 +1,4 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
2
  interface Props {
3
3
  open?: boolean;
4
4
  title: string;
@@ -11,8 +11,8 @@ interface Props {
11
11
  widthClass?: string;
12
12
  isSubmitting?: boolean;
13
13
  disabled?: boolean;
14
- onsubmit?: () => void | Promise<void>;
15
- oncancel?: () => void;
14
+ onsubmit?: (() => void | Promise<void>) | undefined;
15
+ oncancel?: (() => void) | undefined;
16
16
  class?: string;
17
17
  triggerSnippet?: Snippet;
18
18
  children?: Snippet;
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
2
  import NumericInput from './NumericInput.svelte';
3
+ import type { NumericInputLocale } from './numeric-format.js';
3
4
 
4
5
  interface Props {
5
6
  value?: number | null;
7
+ locale?: NumericInputLocale | undefined;
6
8
  currency?: string;
7
9
  min?: number;
8
10
  max?: number;
@@ -14,12 +16,12 @@
14
16
  disabled?: boolean;
15
17
  required?: boolean;
16
18
  invalid?: boolean;
17
- describedby?: string;
19
+ describedby?: string | undefined;
18
20
  class?: string;
19
21
  onchange?: (value: number | null) => void;
20
22
  }
21
23
 
22
- let props: Props = $props();
24
+ let { value = $bindable(null), ...props }: Props = $props();
23
25
  </script>
24
26
 
25
- <NumericInput {...props} mode="currency" />
27
+ <NumericInput {...props} bind:value mode="currency" />
@@ -1,5 +1,7 @@
1
+ import type { NumericInputLocale } from './numeric-format.js';
1
2
  interface Props {
2
3
  value?: number | null;
4
+ locale?: NumericInputLocale | undefined;
3
5
  currency?: string;
4
6
  min?: number;
5
7
  max?: number;
@@ -11,10 +13,10 @@ interface Props {
11
13
  disabled?: boolean;
12
14
  required?: boolean;
13
15
  invalid?: boolean;
14
- describedby?: string;
16
+ describedby?: string | undefined;
15
17
  class?: string;
16
18
  onchange?: (value: number | null) => void;
17
19
  }
18
- declare const MoneyInput: import("svelte").Component<Props, {}, "">;
20
+ declare const MoneyInput: import("svelte").Component<Props, {}, "value">;
19
21
  type MoneyInput = ReturnType<typeof MoneyInput>;
20
22
  export default MoneyInput;