@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
|
@@ -206,6 +206,7 @@
|
|
|
206
206
|
{@const allowed = filterOperators(editorFields.find(item => item.key === node.field))}
|
|
207
207
|
{@const collection = isCollectionOperator(node.operator)}
|
|
208
208
|
{@const invalid = issues.some((issue) => issue.path === node.id)}
|
|
209
|
+
{@const describedby = issues.flatMap((issue, index) => issue.path === node.id ? [`${uid}-issue-${index}`] : []).join(' ') || undefined}
|
|
209
210
|
<div class="filter-rule" data-testid="filter-builder-rule" data-filter-rule={node.id}>
|
|
210
211
|
{#if node.readonly}
|
|
211
212
|
<output aria-label={i18n.t('filter.readonly')}>{node.field} {node.operator} {JSON.stringify(node.value)}</output>
|
|
@@ -225,15 +226,20 @@
|
|
|
225
226
|
</Select>
|
|
226
227
|
</div>
|
|
227
228
|
<div>
|
|
228
|
-
|
|
229
|
+
{#if collection && field}
|
|
230
|
+
<span id={`${uid}-${node.id}-value-label`}>{chinese ? '值' : 'Value'}</span>
|
|
231
|
+
{:else}
|
|
232
|
+
<label for={`${uid}-${node.id}-value`}>{collection ? (chinese ? '值(JSON 数组)' : 'Value (JSON array)') : (chinese ? '值' : 'Value')}</label>
|
|
233
|
+
{/if}
|
|
229
234
|
{#if isNullOperator(node.operator)}
|
|
230
235
|
<Input id={`${uid}-${node.id}-value`} value={chinese ? '无需填值' : 'No value required'} disabled />
|
|
231
236
|
{:else if collection && field}
|
|
232
237
|
<FilterCollectionInput {field} value={node.value}
|
|
238
|
+
{invalid} {describedby} labelledby={`${uid}-${node.id}-value-label`}
|
|
233
239
|
range={node.operator === 'between' || node.operator === 'nbetween'}
|
|
234
240
|
onchange={(value) => { if (!disabled && !node.readonly) node.value = value; }} />
|
|
235
241
|
{:else if !collection && field?.options?.length}
|
|
236
|
-
<Select id={`${uid}-${node.id}-value`} aria-invalid={invalid}
|
|
242
|
+
<Select id={`${uid}-${node.id}-value`} aria-invalid={invalid} aria-describedby={describedby}
|
|
237
243
|
value={field.options.findIndex((option) => option.value === node.value) < 0 ? '' : String(field.options.findIndex((option) => option.value === node.value))}
|
|
238
244
|
onchange={(event: Event) => {
|
|
239
245
|
if (disabled || node.readonly || !(event.currentTarget instanceof HTMLSelectElement)) return;
|
|
@@ -247,15 +253,15 @@
|
|
|
247
253
|
{#each field.options as option, index (index)}<option value={String(index)} disabled={option.disabled}>{option.label}</option>{/each}
|
|
248
254
|
</Select>
|
|
249
255
|
{:else if !collection && field?.type === 'boolean'}
|
|
250
|
-
<Select id={`${uid}-${node.id}-value`} aria-invalid={invalid} value={valueText(node.value)}
|
|
256
|
+
<Select id={`${uid}-${node.id}-value`} aria-invalid={invalid} aria-describedby={describedby} value={valueText(node.value)}
|
|
251
257
|
onchange={(event: Event) => { if (event.currentTarget instanceof HTMLSelectElement) changeValue(node, event.currentTarget.value); }}>
|
|
252
258
|
<option value="">{chinese ? '请选择' : 'Choose'}</option><option value="true">true</option><option value="false">false</option>
|
|
253
259
|
</Select>
|
|
254
260
|
{:else if !collection && isNumericField(field)}
|
|
255
|
-
<Input id={`${uid}-${node.id}-value`} type="number" step="any" aria-invalid={invalid} value={typeof node.value === 'number' ? node.value : undefined}
|
|
261
|
+
<Input id={`${uid}-${node.id}-value`} type="number" step="any" aria-invalid={invalid} aria-describedby={describedby} value={typeof node.value === 'number' ? node.value : undefined}
|
|
256
262
|
oninput={(event: Event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
|
|
257
263
|
{:else}
|
|
258
|
-
<Input id={`${uid}-${node.id}-value`} type="text" aria-invalid={invalid} value={valueText(node.value)} placeholder={collection ? '[1, 2]' : ''}
|
|
264
|
+
<Input id={`${uid}-${node.id}-value`} type="text" aria-invalid={invalid} aria-describedby={describedby} value={valueText(node.value)} placeholder={collection ? '[1, 2]' : ''}
|
|
259
265
|
oninput={(event: Event) => { if (event.currentTarget instanceof HTMLInputElement) changeValue(node, event.currentTarget.value); }} />
|
|
260
266
|
{/if}
|
|
261
267
|
</div>
|
|
@@ -277,7 +283,7 @@
|
|
|
277
283
|
<Button type="button" size="sm" disabled={disabled} data-testid="filter-builder-apply" onclick={apply}><Check size={14} aria-hidden="true" />{i18n.t('common.confirm')}</Button>
|
|
278
284
|
</div>
|
|
279
285
|
</div>
|
|
280
|
-
{#if issues.length}<div role="alert" class="errors" data-testid="filter-builder-errors">{#each issues as issue, index (`${issue.path}-${issue.code}-${index}`)}<p>{issue.path}: {message(issue.code)}</p>{/each}</div>{/if}
|
|
286
|
+
{#if issues.length}<div role="alert" class="errors" data-testid="filter-builder-errors">{#each issues as issue, index (`${issue.path}-${issue.code}-${index}`)}<p id={`${uid}-issue-${index}`}>{issue.path}: {message(issue.code)}</p>{/each}</div>{/if}
|
|
281
287
|
{#if !loadIssues.length}{@render groupEditor(root, root.wrapped || logicalOperator === 'or' ? 1 : 0, true)}{/if}
|
|
282
288
|
</div>
|
|
283
289
|
|
|
@@ -292,7 +298,7 @@
|
|
|
292
298
|
.filter-rule { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 2fr) auto; align-items: end; gap: .5rem; margin-block-start: .75rem; }
|
|
293
299
|
.filter-rule > div { min-width: 0; }
|
|
294
300
|
.filter-rule > output { grid-column: 1 / -2; overflow-wrap: anywhere; }
|
|
295
|
-
.filter-rule label { display: block; margin-block-end: .25rem; color: var(--muted-foreground); }
|
|
301
|
+
.filter-rule label, .filter-rule > div > span { display: block; margin-block-end: .25rem; color: var(--muted-foreground); }
|
|
296
302
|
.empty { padding-block: .75rem; color: var(--muted-foreground); }
|
|
297
303
|
.errors { color: var(--destructive); overflow-wrap: anywhere; }
|
|
298
304
|
.nested-group { align-items: flex-start; }
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
import { Input } from './ui/input/index.js';
|
|
7
7
|
import { Select } from './ui/select/index.js';
|
|
8
8
|
|
|
9
|
-
let { field, value, range = false, onchange }: {
|
|
10
|
-
field: FieldDefinition; value: unknown; range?: boolean;
|
|
9
|
+
let { field, value, range = false, labelledby, describedby, invalid = false, onchange }: {
|
|
10
|
+
field: FieldDefinition; value: unknown; range?: boolean;
|
|
11
|
+
labelledby?: string | undefined; describedby?: string | undefined; invalid?: boolean;
|
|
12
|
+
onchange: (value: unknown[]) => void;
|
|
11
13
|
} = $props();
|
|
12
14
|
const i18n = useTranslation();
|
|
13
15
|
const values = $derived<unknown[]>(Array.isArray(value) ? value : range ? [null, null] : [null]);
|
|
@@ -17,14 +19,14 @@
|
|
|
17
19
|
}
|
|
18
20
|
</script>
|
|
19
21
|
|
|
20
|
-
<div class="filter-collection">
|
|
22
|
+
<div class="filter-collection" role="group" aria-labelledby={labelledby} aria-describedby={describedby}>
|
|
21
23
|
{#each values as item, index (index)}
|
|
22
24
|
{@const label = range
|
|
23
25
|
? i18n.t(index === 0 ? 'filter.rangeStart' : 'filter.rangeEnd')
|
|
24
26
|
: i18n.t('filter.collectionItem', { number: String(index + 1) })}
|
|
25
27
|
<div class="filter-collection-item">
|
|
26
28
|
{#if field.type === 'select'}
|
|
27
|
-
<Select aria-label={label} value={String(field.options?.findIndex(option => option.value === item) ?? -1)}
|
|
29
|
+
<Select aria-label={label} aria-invalid={invalid} aria-describedby={describedby} value={String(field.options?.findIndex(option => option.value === item) ?? -1)}
|
|
28
30
|
onchange={(event: Event) => {
|
|
29
31
|
if (!(event.currentTarget instanceof HTMLSelectElement)) return;
|
|
30
32
|
const option = field.options?.[Number(event.currentTarget.value)];
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
{/each}
|
|
37
39
|
</Select>
|
|
38
40
|
{:else if field.type === 'boolean'}
|
|
39
|
-
<Select aria-label={label} value={typeof item === 'boolean' ? String(item) : ''}
|
|
41
|
+
<Select aria-label={label} aria-invalid={invalid} aria-describedby={describedby} value={typeof item === 'boolean' ? String(item) : ''}
|
|
40
42
|
onchange={(event: Event) => {
|
|
41
43
|
if (!(event.currentTarget instanceof HTMLSelectElement)) return;
|
|
42
44
|
update(index, event.currentTarget.value === 'true' ? true : event.currentTarget.value === 'false' ? false : null);
|
|
@@ -46,12 +48,12 @@
|
|
|
46
48
|
<option value="false">{i18n.t('common.no')}</option>
|
|
47
49
|
</Select>
|
|
48
50
|
{:else if isNumericFilterField(field)}
|
|
49
|
-
<Input type="number" step="any" aria-label={label} value={typeof item === 'number' ? item : undefined}
|
|
51
|
+
<Input type="number" step="any" aria-label={label} aria-invalid={invalid} aria-describedby={describedby} value={typeof item === 'number' ? item : undefined}
|
|
50
52
|
oninput={(event: Event) => {
|
|
51
53
|
if (event.currentTarget instanceof HTMLInputElement) update(index, event.currentTarget.value === '' ? null : event.currentTarget.valueAsNumber);
|
|
52
54
|
}} />
|
|
53
55
|
{:else}
|
|
54
|
-
<Input type="text" aria-label={label} value={typeof item === 'string' ? item : ''}
|
|
56
|
+
<Input type="text" aria-label={label} aria-invalid={invalid} aria-describedby={describedby} value={typeof item === 'string' ? item : ''}
|
|
55
57
|
oninput={(event: Event) => {
|
|
56
58
|
if (event.currentTarget instanceof HTMLInputElement) update(index, event.currentTarget.value);
|
|
57
59
|
}} />
|
|
@@ -3,6 +3,9 @@ type $$ComponentProps = {
|
|
|
3
3
|
field: FieldDefinition;
|
|
4
4
|
value: unknown;
|
|
5
5
|
range?: boolean;
|
|
6
|
+
labelledby?: string | undefined;
|
|
7
|
+
describedby?: string | undefined;
|
|
8
|
+
invalid?: boolean;
|
|
6
9
|
onchange: (value: unknown[]) => void;
|
|
7
10
|
};
|
|
8
11
|
declare const FilterCollectionInput: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<Button variant="outline" size="sm" onclick={onSearchClick} aria-label={i18n.t('common.search')} class="svadmin-header-search svadmin-u-77a2a20e90d4 svadmin-u-bfa603190748 svadmin-u-e7a768f922d2 svadmin-u-0e17f2bd9074 svadmin-u-5f22e64f2282 svadmin-u-05faf5c801ff">
|
|
72
72
|
<Search class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
|
|
73
73
|
<span class="svadmin-u-99d72c7fc3e2 svadmin-u-ee3c1259a368 svadmin-u-359090c2d529">{i18n.t('common.search')}</span>
|
|
74
|
-
<kbd class="svadmin-u-99d72c7fc3e2 svadmin-u-e3fd07f96126 svadmin-u-3960ffc248d9 svadmin-u-a3899220f90e svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-706701550477 svadmin-u-45d828117213 svadmin-u-465609a240a8 svadmin-u-1dc571a3609f svadmin-u-2689f3958069 svadmin-u-0e65706bcccd svadmin-u-bfa603190748">
|
|
74
|
+
<kbd style="flex-shrink: 0; white-space: nowrap" class="whitespace-nowrap svadmin-u-99d72c7fc3e2 svadmin-u-e3fd07f96126 svadmin-u-3960ffc248d9 svadmin-u-a3899220f90e svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-706701550477 svadmin-u-45d828117213 svadmin-u-465609a240a8 svadmin-u-1dc571a3609f svadmin-u-2689f3958069 svadmin-u-0e65706bcccd svadmin-u-bfa603190748">
|
|
75
75
|
<span class="svadmin-u-359090c2d529">⌘</span>K
|
|
76
76
|
</kbd>
|
|
77
77
|
</Button>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { captureAdminContext, captureAuthSession, IMPORT_LIMITS, snapshotImportArtifact, snapshotImportTaskResult, useCan, useImport, useResourceContract, useSubmitTask, useTask } from '@svadmin/core';
|
|
2
|
+
import { captureAdminContext, captureAuthSession, IMPORT_LIMITS, invalidImportPreviewFailure, snapshotImportArtifact, snapshotImportTaskResult, useCan, useImport, useResourceContract, useSubmitTask, useTask } from '@svadmin/core';
|
|
3
3
|
import { untrack } from 'svelte';
|
|
4
4
|
import { definedReactiveOptions } from '@svadmin/core/options';
|
|
5
5
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
6
|
import { parseContractCreateInput } from '@svadmin/core/resource-contract';
|
|
7
|
-
import type { ImportArtifactProvider, TaskProvider } from '@svadmin/core';
|
|
7
|
+
import type { ImportArtifactProvider, ImportPreviewResult, TaskProvider } from '@svadmin/core';
|
|
8
8
|
import { normalizeTaskStatus, resolveTaskProgress, canCancelTask } from './task-utils.js';
|
|
9
9
|
import TaskStatusBadge from './TaskStatusBadge.svelte';
|
|
10
10
|
import CancelTaskButton from './CancelTaskButton.svelte';
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
import { Badge } from './ui/badge/index.js';
|
|
16
16
|
import { Upload, ArrowRight, ArrowLeft, AlertCircle, Download, FileSpreadsheet, Loader2, RotateCw } from '@lucide/svelte';
|
|
17
17
|
|
|
18
|
+
/** 仅用于标记文件读取或 JSON 语法异常,避免把浏览器原始诊断展示给用户。 */
|
|
19
|
+
class ImportFileReadError extends Error {}
|
|
20
|
+
|
|
18
21
|
interface Props {
|
|
19
22
|
resourceName: string;
|
|
20
23
|
open?: boolean;
|
|
@@ -30,6 +33,10 @@
|
|
|
30
33
|
initialTaskId?: string;
|
|
31
34
|
onTaskSubmitted?: (taskId: string) => void;
|
|
32
35
|
onSuccess?: (result: { succeeded: number; failed: number }) => void;
|
|
36
|
+
/** 只做本地解析、映射和逐行校验,不调用 Provider 写入,也不提交、轮询或取消任务。 */
|
|
37
|
+
dryRun?: boolean;
|
|
38
|
+
/** 预览结果回调;接收隔离的只读快照,失败原因与失败报告一样只含安全通用文本。 */
|
|
39
|
+
onPreview?: (result: ImportPreviewResult) => void;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
let {
|
|
@@ -46,6 +53,8 @@
|
|
|
46
53
|
initialTaskId,
|
|
47
54
|
onTaskSubmitted,
|
|
48
55
|
onSuccess,
|
|
56
|
+
dryRun = false,
|
|
57
|
+
onPreview,
|
|
49
58
|
}: Props = $props();
|
|
50
59
|
|
|
51
60
|
const i18n = useTranslation();
|
|
@@ -78,14 +87,14 @@
|
|
|
78
87
|
let parseToken = 0;
|
|
79
88
|
let mounted = true;
|
|
80
89
|
let cancelled = $state(false);
|
|
81
|
-
let durableTaskId = $state<string | undefined>(untrack(() => initialTaskId));
|
|
82
|
-
let durableTaskScope = $state.raw<typeof scope | undefined>(untrack(() => scope));
|
|
83
|
-
const ownedTaskId = $derived(durableTaskScope && sameScope(durableTaskScope, scope) ? durableTaskId : undefined);
|
|
90
|
+
let durableTaskId = $state<string | undefined>(untrack(() => dryRun ? undefined : initialTaskId));
|
|
91
|
+
let durableTaskScope = $state.raw<typeof scope | undefined>(untrack(() => dryRun ? undefined : scope));
|
|
92
|
+
const ownedTaskId = $derived(!dryRun && durableTaskScope && sameScope(durableTaskScope, scope) ? durableTaskId : undefined);
|
|
84
93
|
let durableHandledTaskId = $state<string | undefined>();
|
|
85
94
|
let durableSubmitAttempt = $state.raw<object | undefined>();
|
|
86
95
|
let artifactUploadController: AbortController | undefined;
|
|
87
96
|
let durablePolling = $state(true);
|
|
88
|
-
const durableMode = $derived(taskName !== undefined);
|
|
97
|
+
const durableMode = $derived(!dryRun && taskName !== undefined);
|
|
89
98
|
const submitTask = useSubmitTask();
|
|
90
99
|
const taskQuery = useTask(definedReactiveOptions({
|
|
91
100
|
get taskId() { return ownedTaskId; },
|
|
@@ -101,12 +110,13 @@
|
|
|
101
110
|
const durableStatus = $derived(normalizeTaskStatus(taskQuery.data?.status));
|
|
102
111
|
const retryReceipt = $derived(durableStatus === 'completed'
|
|
103
112
|
? snapshotImportTaskResult(taskQuery.data?.result ?? taskQuery.data?.result_data)?.retry : undefined);
|
|
104
|
-
const durableActive = $derived(durableStatus === 'queued' || durableStatus === 'processing'
|
|
105
|
-
|| !!durableSubmitAttempt);
|
|
113
|
+
const durableActive = $derived(!dryRun && (durableStatus === 'queued' || durableStatus === 'processing'
|
|
114
|
+
|| !!durableSubmitAttempt));
|
|
106
115
|
|
|
107
116
|
let importResult = $state<{ succeeded: number; failed: number } | null>(null);
|
|
108
117
|
let succeededCount = $state(0);
|
|
109
118
|
let failedRecords = $state<Array<{ row: Record<string, unknown>; error: string }>>([]);
|
|
119
|
+
let previewResult = $state.raw<ImportPreviewResult | null>(null);
|
|
110
120
|
|
|
111
121
|
const importer = useImport(definedReactiveOptions({
|
|
112
122
|
get resource() { return binding.resource; },
|
|
@@ -133,6 +143,7 @@
|
|
|
133
143
|
durableTaskId = undefined;
|
|
134
144
|
durableHandledTaskId = undefined;
|
|
135
145
|
durableSubmitAttempt = undefined;
|
|
146
|
+
previewResult = null;
|
|
136
147
|
selectedFile = null;
|
|
137
148
|
fileName = file.name;
|
|
138
149
|
rawHeaders = [];
|
|
@@ -144,13 +155,13 @@
|
|
|
144
155
|
throw new Error(i18n.t('import.invalidLimits'));
|
|
145
156
|
}
|
|
146
157
|
if (file.size > maxBytes) throw new Error(i18n.t('import.limit'));
|
|
147
|
-
const text = await file
|
|
158
|
+
const text = await readFileText(file);
|
|
148
159
|
if (!current(origin, token)) return;
|
|
149
160
|
if (text.length > maxBytes) throw new Error(i18n.t('import.limit'));
|
|
150
161
|
const cleanText = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
151
162
|
|
|
152
163
|
if (file.name.toLowerCase().endsWith('.json')) {
|
|
153
|
-
const json: unknown =
|
|
164
|
+
const json: unknown = parseImportJson(cleanText);
|
|
154
165
|
if (Array.isArray(json) && json.length > maxRows) throw new Error(i18n.t('import.limit'));
|
|
155
166
|
if (!Array.isArray(json) || json.length === 0 || json.some((item) => (
|
|
156
167
|
typeof item !== 'object' || item === null || Array.isArray(item)
|
|
@@ -201,11 +212,29 @@
|
|
|
201
212
|
currentStep = 2;
|
|
202
213
|
} catch (error) {
|
|
203
214
|
if (!current(origin, token)) return;
|
|
204
|
-
fileError = error instanceof
|
|
215
|
+
fileError = error instanceof ImportFileReadError
|
|
216
|
+
? i18n.t('import.fileReadFailed')
|
|
217
|
+
: error instanceof Error ? error.message : i18n.t('import.fileReadFailed');
|
|
205
218
|
currentStep = 1;
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
|
|
222
|
+
async function readFileText(file: File): Promise<string> {
|
|
223
|
+
try {
|
|
224
|
+
return await file.text();
|
|
225
|
+
} catch {
|
|
226
|
+
throw new ImportFileReadError();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseImportJson(text: string): unknown {
|
|
231
|
+
try {
|
|
232
|
+
return JSON.parse(text);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new ImportFileReadError();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
209
238
|
function handleDrop(e: DragEvent) {
|
|
210
239
|
e.preventDefault();
|
|
211
240
|
if (e.dataTransfer?.files?.[0]) {
|
|
@@ -244,20 +273,78 @@
|
|
|
244
273
|
return record;
|
|
245
274
|
}
|
|
246
275
|
|
|
247
|
-
function
|
|
276
|
+
function mapSourceRow(source: unknown[]): Record<string, unknown> {
|
|
277
|
+
return mapRow(Object.fromEntries(rawHeaders.map((header, column) => [header, source[column]])));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function snapshotPreviewResult(result: ImportPreviewResult): ImportPreviewResult {
|
|
281
|
+
const failedRows = result.failedRows.map(failure => Object.freeze({
|
|
282
|
+
row: failure.row,
|
|
283
|
+
error: failure.error,
|
|
284
|
+
}));
|
|
285
|
+
Object.freeze(failedRows);
|
|
286
|
+
return Object.freeze({
|
|
287
|
+
totalRows: result.totalRows,
|
|
288
|
+
importableRows: result.importableRows,
|
|
289
|
+
failedRows,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function evaluateMappedRows(): ImportPreviewResult {
|
|
294
|
+
const failedRows: ImportPreviewResult['failedRows'] = [];
|
|
295
|
+
for (const [index, source] of rawRows.entries()) {
|
|
296
|
+
try {
|
|
297
|
+
parseContractCreateInput(binding.resource, mapSourceRow(source));
|
|
298
|
+
} catch {
|
|
299
|
+
failedRows.push(invalidImportPreviewFailure(index + 1));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return snapshotPreviewResult({
|
|
303
|
+
totalRows: rawRows.length,
|
|
304
|
+
importableRows: rawRows.length - failedRows.length,
|
|
305
|
+
failedRows,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function firstInvalidMappedRow(): number | undefined {
|
|
248
310
|
for (const [index, source] of rawRows.entries()) {
|
|
249
311
|
try {
|
|
250
|
-
|
|
251
|
-
parseContractCreateInput(binding.resource, mapped);
|
|
312
|
+
parseContractCreateInput(binding.resource, mapSourceRow(source));
|
|
252
313
|
} catch {
|
|
253
|
-
return
|
|
314
|
+
return index + 1;
|
|
254
315
|
}
|
|
255
316
|
}
|
|
256
|
-
return
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function validateMappedRows(): string | null {
|
|
321
|
+
const row = firstInvalidMappedRow();
|
|
322
|
+
return row === undefined ? null : i18n.t('import.invalidRow', { row });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function previewImport() {
|
|
326
|
+
if (!selectedFile || rawRows.length === 0 || isImporting || !open || !allowed) return;
|
|
327
|
+
if (!dryRun && !onPreview) return;
|
|
328
|
+
const targets = Object.values(columnMapping).filter(Boolean);
|
|
329
|
+
if (targets.length === 0 || new Set(targets).size !== targets.length) {
|
|
330
|
+
fileError = 'Map at least one column, and use each target field only once.';
|
|
331
|
+
previewResult = null;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
fileError = null;
|
|
335
|
+
importError = null;
|
|
336
|
+
previewResult = evaluateMappedRows();
|
|
337
|
+
currentStep = 3;
|
|
338
|
+
try {
|
|
339
|
+
// 观察者只拿到隔离的只读快照;同步抛错或异步拒绝都不能改变已完成的本地预览。
|
|
340
|
+
const observerSnapshot = snapshotPreviewResult(previewResult);
|
|
341
|
+
void Promise.resolve(onPreview?.(observerSnapshot)).catch(() => {});
|
|
342
|
+
} catch { /* 观察者失败不能改变已完成的本地预览结果。 */ }
|
|
257
343
|
}
|
|
258
344
|
|
|
259
345
|
async function startImport() {
|
|
260
346
|
if (!selectedFile || rawRows.length === 0 || isImporting || !open || !allowed) return;
|
|
347
|
+
if (dryRun) { previewImport(); return; }
|
|
261
348
|
if (durableMode && (!activeTaskProvider || !taskName?.trim() || !taskIdempotencyKey?.trim())) {
|
|
262
349
|
fileError = i18n.t('import.taskConfiguration');
|
|
263
350
|
return;
|
|
@@ -275,6 +362,7 @@
|
|
|
275
362
|
const origin = scope;
|
|
276
363
|
const token = parseToken;
|
|
277
364
|
const notifySuccess = onSuccess;
|
|
365
|
+
previewResult = null;
|
|
278
366
|
currentStep = 3;
|
|
279
367
|
succeededCount = 0;
|
|
280
368
|
failedRecords = [];
|
|
@@ -292,8 +380,7 @@
|
|
|
292
380
|
throw new Error(i18n.t('import.taskConfiguration'));
|
|
293
381
|
}
|
|
294
382
|
const records = taskArtifactProvider ? undefined : rawRows.map((source, index) => {
|
|
295
|
-
|
|
296
|
-
try { return parseContractCreateInput(binding.resource, mapped); }
|
|
383
|
+
try { return parseContractCreateInput(binding.resource, mapSourceRow(source)); }
|
|
297
384
|
catch { throw new Error(i18n.t('import.invalidRow', { row: index + 1 })); }
|
|
298
385
|
});
|
|
299
386
|
const uploaded = taskArtifactProvider
|
|
@@ -363,12 +450,12 @@
|
|
|
363
450
|
succeededCount = result.succeeded.length;
|
|
364
451
|
failedRecords = result.errored.map(item => ({
|
|
365
452
|
row: item.request as Record<string, unknown>,
|
|
366
|
-
error: item.error.
|
|
453
|
+
error: importFailureMessage(item.error, item.row),
|
|
367
454
|
}));
|
|
368
455
|
importResult = { succeeded: succeededCount, failed: failedRecords.length };
|
|
369
456
|
} catch (error) {
|
|
370
457
|
if (!current(origin, token)) return;
|
|
371
|
-
importError = error
|
|
458
|
+
importError = importFailureMessage(error);
|
|
372
459
|
importResult = null;
|
|
373
460
|
}
|
|
374
461
|
if (importResult) {
|
|
@@ -391,6 +478,33 @@
|
|
|
391
478
|
left.taskIdempotencyKey === right.taskIdempotencyKey;
|
|
392
479
|
}
|
|
393
480
|
|
|
481
|
+
/** Core 的 HttpError.message 只用于诊断;界面仅按安全 code 选择本地化文案。 */
|
|
482
|
+
function importFailureMessage(error: unknown, row?: number): string {
|
|
483
|
+
let code: unknown;
|
|
484
|
+
try {
|
|
485
|
+
if (typeof error === 'object' && error !== null) {
|
|
486
|
+
code = Object.getOwnPropertyDescriptor(error, 'code')?.value;
|
|
487
|
+
}
|
|
488
|
+
} catch { /* 错误对象属于不可信诊断来源。 */ }
|
|
489
|
+
if (code === 'IMPORT_LIMIT_EXCEEDED') return i18n.t('import.limit');
|
|
490
|
+
if (code === 'INVALID_RESOURCE_INPUT') {
|
|
491
|
+
const failedRow = row ?? failureRow(error);
|
|
492
|
+
if (failedRow !== undefined) return i18n.t('import.invalidRow', { row: failedRow });
|
|
493
|
+
}
|
|
494
|
+
return i18n.t('common.operationFailed', { defaultValue: 'Import failed' });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function failureRow(error: unknown): number | undefined {
|
|
498
|
+
try {
|
|
499
|
+
if (typeof error !== 'object' || error === null) return undefined;
|
|
500
|
+
const details: unknown = Object.getOwnPropertyDescriptor(error, 'details')?.value;
|
|
501
|
+
if (typeof details !== 'object' || details === null) return undefined;
|
|
502
|
+
const row: unknown = Object.getOwnPropertyDescriptor(details, 'row')?.value;
|
|
503
|
+
return typeof row === 'number' && Number.isSafeInteger(row) && row >= 1 && row <= maxRows
|
|
504
|
+
? row : undefined;
|
|
505
|
+
} catch { return undefined; }
|
|
506
|
+
}
|
|
507
|
+
|
|
394
508
|
async function retryFailedRows(): Promise<void> {
|
|
395
509
|
const receipt = retryReceipt;
|
|
396
510
|
const parentTaskId = ownedTaskId;
|
|
@@ -472,6 +586,7 @@
|
|
|
472
586
|
succeededCount = 0;
|
|
473
587
|
failedRecords = [];
|
|
474
588
|
importResult = null;
|
|
589
|
+
previewResult = null;
|
|
475
590
|
fileError = null;
|
|
476
591
|
importError = null;
|
|
477
592
|
cancelled = false;
|
|
@@ -497,20 +612,27 @@
|
|
|
497
612
|
|
|
498
613
|
let observedScope = untrack(() => scope);
|
|
499
614
|
let observedOpen = untrack(() => open);
|
|
615
|
+
let observedDryRun = untrack(() => dryRun);
|
|
500
616
|
$effect.pre(() => {
|
|
501
|
-
const changed = !sameScope(observedScope, scope) || (observedScope.allowed && !allowed)
|
|
617
|
+
const changed = !sameScope(observedScope, scope) || (observedScope.allowed && !allowed)
|
|
618
|
+
|| observedDryRun !== dryRun;
|
|
502
619
|
if (changed) {
|
|
503
620
|
durableTaskId = undefined;
|
|
504
621
|
durableTaskScope = undefined;
|
|
505
622
|
durableHandledTaskId = undefined;
|
|
623
|
+
durableSubmitAttempt = undefined;
|
|
624
|
+
artifactUploadController?.abort();
|
|
625
|
+
artifactUploadController = undefined;
|
|
626
|
+
durablePolling = true;
|
|
506
627
|
}
|
|
507
628
|
if (changed || observedOpen !== open) untrack(reset);
|
|
508
629
|
observedScope = scope;
|
|
509
630
|
observedOpen = open;
|
|
631
|
+
observedDryRun = dryRun;
|
|
510
632
|
});
|
|
511
633
|
$effect(() => {
|
|
512
634
|
const task = taskQuery.data;
|
|
513
|
-
if (!open || !allowed || !durableTaskId || !task || durableHandledTaskId === durableTaskId) return;
|
|
635
|
+
if (dryRun || !open || !allowed || !durableTaskId || !task || durableHandledTaskId === durableTaskId) return;
|
|
514
636
|
if (!['completed', 'failed', 'cancelled'].includes(durableStatus)) return;
|
|
515
637
|
durableHandledTaskId = durableTaskId;
|
|
516
638
|
durablePolling = false;
|
|
@@ -555,7 +677,41 @@
|
|
|
555
677
|
{#if !allowed}
|
|
556
678
|
<p role="status">{i18n.t('common.operationFailed', { defaultValue: 'Import is unavailable.' })}</p>
|
|
557
679
|
{/if}
|
|
558
|
-
{#if
|
|
680
|
+
{#if currentStep === 3 && previewResult}
|
|
681
|
+
<div class="svadmin-u-b43b4c086d9a svadmin-u-03b4dd7f172b">
|
|
682
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
|
|
683
|
+
<FileSpreadsheet class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e" />
|
|
684
|
+
<span>{i18n.t('import.preview')}</span>
|
|
685
|
+
</div>
|
|
686
|
+
<p role="status">
|
|
687
|
+
{i18n.t('import.previewSummary', {
|
|
688
|
+
total: previewResult.totalRows,
|
|
689
|
+
importable: previewResult.importableRows,
|
|
690
|
+
failed: previewResult.failedRows.length,
|
|
691
|
+
})}
|
|
692
|
+
</p>
|
|
693
|
+
<p>{i18n.t('import.previewHint')}</p>
|
|
694
|
+
{#if previewResult.failedRows.length > 0}
|
|
695
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-811148b13d1e">
|
|
696
|
+
<AlertCircle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
|
|
697
|
+
<span>{i18n.t('import.previewFailed')}</span>
|
|
698
|
+
</div>
|
|
699
|
+
<ul>
|
|
700
|
+
{#each previewResult.failedRows.slice(0, 20) as failure (failure.row)}
|
|
701
|
+
<li>{i18n.t('import.previewFailedRow', { row: failure.row, error: failure.error })}</li>
|
|
702
|
+
{/each}
|
|
703
|
+
</ul>
|
|
704
|
+
{/if}
|
|
705
|
+
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77c08e015d14 svadmin-u-77a2a20e90d4">
|
|
706
|
+
<Button variant="outline" size="sm" onclick={() => { previewResult = null; currentStep = 2; }}>
|
|
707
|
+
{i18n.t('import.backToImport')}
|
|
708
|
+
</Button>
|
|
709
|
+
<Button variant="outline" size="sm" onclick={reset}>
|
|
710
|
+
{i18n.t('common.importAnother', { defaultValue: 'Import Another' })}
|
|
711
|
+
</Button>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
{:else if durableMode && (durableTaskId || durableSubmitAttempt || currentStep === 3)}
|
|
559
715
|
{#if durableTaskId}
|
|
560
716
|
<p>{durableTaskId}</p>
|
|
561
717
|
{#if taskQuery.isError}
|
|
@@ -672,16 +828,29 @@
|
|
|
672
828
|
<ArrowLeft class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-618162408e7a" />
|
|
673
829
|
{i18n.t('common.back', { defaultValue: 'Back' })}
|
|
674
830
|
</Button>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
831
|
+
{#if dryRun}
|
|
832
|
+
<Button size="sm" disabled={!allowed || isImporting} onclick={previewImport}>
|
|
833
|
+
{i18n.t('import.preview')}
|
|
834
|
+
<ArrowRight class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-f58b02572ab2" />
|
|
835
|
+
</Button>
|
|
836
|
+
{:else if onPreview}
|
|
837
|
+
<Button variant="outline" size="sm" disabled={!allowed || isImporting} onclick={previewImport}>
|
|
838
|
+
{i18n.t('common.preview')}
|
|
839
|
+
<ArrowRight class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-f58b02572ab2" />
|
|
840
|
+
</Button>
|
|
841
|
+
{/if}
|
|
842
|
+
{#if !dryRun}
|
|
843
|
+
<Button size="sm" disabled={!allowed || isImporting} onclick={startImport}>
|
|
844
|
+
{i18n.t('common.startImport', { defaultValue: 'Start Import' })}
|
|
845
|
+
<ArrowRight class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-f58b02572ab2" />
|
|
846
|
+
</Button>
|
|
847
|
+
{/if}
|
|
679
848
|
</div>
|
|
680
849
|
</div>
|
|
681
850
|
{/if}
|
|
682
851
|
|
|
683
852
|
<!-- Step 3: Progress & Summary -->
|
|
684
|
-
{#if currentStep === 3 && !durableMode}
|
|
853
|
+
{#if currentStep === 3 && !durableMode && !previewResult}
|
|
685
854
|
<div class="svadmin-u-b43b4c086d9a svadmin-u-03b4dd7f172b">
|
|
686
855
|
<div class="svadmin-u-6f7e013d6499">
|
|
687
856
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-359090c2d529">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImportArtifactProvider, TaskProvider } from '@svadmin/core';
|
|
1
|
+
import type { ImportArtifactProvider, ImportPreviewResult, TaskProvider } from '@svadmin/core';
|
|
2
2
|
interface Props {
|
|
3
3
|
resourceName: string;
|
|
4
4
|
open?: boolean;
|
|
@@ -17,6 +17,10 @@ interface Props {
|
|
|
17
17
|
succeeded: number;
|
|
18
18
|
failed: number;
|
|
19
19
|
}) => void;
|
|
20
|
+
/** 只做本地解析、映射和逐行校验,不调用 Provider 写入,也不提交、轮询或取消任务。 */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** 预览结果回调;接收隔离的只读快照,失败原因与失败报告一样只含安全通用文本。 */
|
|
23
|
+
onPreview?: (result: ImportPreviewResult) => void;
|
|
20
24
|
}
|
|
21
25
|
declare const ImportWizard: import("svelte").Component<Props, {}, "open">;
|
|
22
26
|
type ImportWizard = ReturnType<typeof ImportWizard>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
|
|
3
3
|
import { captureAdminContext, inferResource } from '@svadmin/core';
|
|
4
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
5
|
import type { InferResult } from '@svadmin/core';
|
|
5
6
|
import { Button } from './ui/button/index.js';
|
|
6
7
|
import { Input } from './ui/input/index.js';
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
import { Wand2, Copy, Check, RefreshCw, Loader2, AlertCircle } from '@lucide/svelte';
|
|
15
16
|
|
|
16
17
|
const adminContext = captureAdminContext();
|
|
18
|
+
const i18n = useTranslation();
|
|
17
19
|
const resources = $derived(adminContext.resources);
|
|
18
20
|
|
|
19
21
|
let selectedResource = $state('');
|
|
@@ -70,9 +72,9 @@
|
|
|
70
72
|
resourceName,
|
|
71
73
|
response.data as Record<string, unknown>[],
|
|
72
74
|
);
|
|
73
|
-
} catch
|
|
75
|
+
} catch {
|
|
74
76
|
if (epoch === inferenceEpoch) {
|
|
75
|
-
error =
|
|
77
|
+
error = i18n.t('common.error') ?? 'Something went wrong';
|
|
76
78
|
}
|
|
77
79
|
} finally {
|
|
78
80
|
if (epoch === inferenceEpoch) loading = false;
|