@svadmin/ui 0.72.0 → 0.74.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/README.md +62 -22
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/app.css +8488 -6297
- package/dist/app.theme.css +8514 -6374
- package/dist/class-properties.generated.d.ts +1 -0
- package/dist/class-properties.generated.js +2 -0
- package/dist/classnames.d.ts +8 -0
- package/dist/classnames.js +26 -0
- package/dist/component-registry.d.ts +14977 -0
- package/dist/component-registry.js +216 -0
- package/dist/components/AboutSettings.svelte +7 -1
- package/dist/components/AboutSettings.svelte.d.ts +4 -1
- package/dist/components/ActivityFeed.svelte +9 -3
- package/dist/components/ActivityFeed.svelte.d.ts +3 -0
- package/dist/components/AdminApp.svelte +19 -10
- package/dist/components/AdminApp.svelte.d.ts +6 -0
- package/dist/components/ApiSettings.svelte +41 -5
- package/dist/components/ApprovalCenter.svelte +283 -0
- package/dist/components/ApprovalCenter.svelte.d.ts +10 -0
- package/dist/components/AuditLogViewer.svelte +3 -1
- package/dist/components/AutoForm.svelte +53 -9
- package/dist/components/AutoForm.svelte.d.ts +3 -0
- package/dist/components/AutoTable.svelte +846 -43
- package/dist/components/AutoTable.svelte.d.ts +22 -1
- package/dist/components/BoundRecordDetailDrawer.svelte +17 -2
- package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +4 -0
- package/dist/components/Breadcrumbs.svelte +11 -1
- package/dist/components/CancelTaskButton.svelte +12 -42
- package/dist/components/ColorPicker.svelte +73 -0
- package/dist/components/ColorPicker.svelte.d.ts +15 -0
- package/dist/components/ComboboxField.svelte +143 -34
- package/dist/components/ComboboxField.svelte.d.ts +4 -2
- package/dist/components/CreatePage.svelte +4 -0
- package/dist/components/CreatePage.svelte.d.ts +2 -0
- package/dist/components/DashboardView.svelte +99 -0
- package/dist/components/DashboardView.svelte.d.ts +14 -0
- package/dist/components/DatePicker.svelte +10 -0
- package/dist/components/DatePicker.svelte.d.ts +6 -0
- package/dist/components/DateRangeInput.svelte +155 -0
- package/dist/components/DateRangeInput.svelte.d.ts +35 -0
- package/dist/components/DateRangePicker.svelte +10 -0
- package/dist/components/DateRangePicker.svelte.d.ts +6 -0
- package/dist/components/DateTimeInput.svelte +134 -0
- package/dist/components/DateTimeInput.svelte.d.ts +27 -0
- package/dist/components/DateTimePicker.svelte +10 -0
- package/dist/components/DateTimePicker.svelte.d.ts +6 -0
- package/dist/components/DevTools.svelte +36 -28
- package/dist/components/DevframePanel.svelte +123 -0
- package/dist/components/DevframePanel.svelte.d.ts +3 -0
- package/dist/components/EditPage.svelte +4 -0
- package/dist/components/EditPage.svelte.d.ts +2 -0
- package/dist/components/ErrorSummary.svelte +52 -0
- package/dist/components/ErrorSummary.svelte.d.ts +14 -0
- package/dist/components/FieldRenderer.svelte +172 -99
- package/dist/components/FileUpload.svelte +412 -0
- package/dist/components/FileUpload.svelte.d.ts +43 -0
- package/dist/components/FilterBuilder.svelte +244 -265
- package/dist/components/FilterBuilder.svelte.d.ts +3 -0
- package/dist/components/FilterCollectionInput.svelte +80 -0
- package/dist/components/FilterCollectionInput.svelte.d.ts +10 -0
- package/dist/components/GanttChart.svelte +66 -14
- package/dist/components/GanttChart.svelte.d.ts +6 -0
- package/dist/components/Header.svelte +18 -2
- package/dist/components/ImportWizard.svelte +509 -100
- package/dist/components/ImportWizard.svelte.d.ts +11 -0
- package/dist/components/JsonSchemaForm.svelte +322 -98
- package/dist/components/JsonSchemaForm.svelte.d.ts +18 -1
- package/dist/components/KanbanBoard.svelte +195 -40
- package/dist/components/KanbanBoard.svelte.d.ts +9 -2
- package/dist/components/Layout.svelte +4 -3
- package/dist/components/LazyAutoForm.svelte +28 -0
- package/dist/components/LazyAutoForm.svelte.d.ts +8 -0
- package/dist/components/LazyAutoTable.svelte +28 -0
- package/dist/components/LazyAutoTable.svelte.d.ts +8 -0
- package/dist/components/LazySettingsPage.svelte +28 -0
- package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
- package/dist/components/LazyShowPage.svelte +28 -0
- package/dist/components/LazyShowPage.svelte.d.ts +8 -0
- package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
- package/dist/components/ListPage.svelte +9 -1
- package/dist/components/ListPage.svelte.d.ts +5 -0
- package/dist/components/LoginPage.svelte +2 -2
- package/dist/components/MoneyInput.svelte +25 -0
- package/dist/components/MoneyInput.svelte.d.ts +20 -0
- package/dist/components/MultiTabKeepAlive.svelte +11 -6
- package/dist/components/NotificationsSettings.svelte +69 -2
- package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
- package/dist/components/NumberInput.svelte +25 -0
- package/dist/components/NumberInput.svelte.d.ts +19 -0
- package/dist/components/NumericInput.svelte +110 -0
- package/dist/components/NumericInput.svelte.d.ts +23 -0
- package/dist/components/PercentInput.svelte +25 -0
- package/dist/components/PercentInput.svelte.d.ts +20 -0
- package/dist/components/PermissionMatrix.svelte +20 -1
- package/dist/components/PivotExportButton.svelte +157 -0
- package/dist/components/PivotExportButton.svelte.d.ts +16 -0
- package/dist/components/PivotTable.svelte +229 -74
- package/dist/components/PivotTable.svelte.d.ts +19 -2
- package/dist/components/QuickEditDrawer.svelte +4 -1
- package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
- package/dist/components/RadioGroup.svelte +121 -0
- package/dist/components/RadioGroup.svelte.d.ts +43 -0
- package/dist/components/RangeSlider.svelte +127 -0
- package/dist/components/RangeSlider.svelte.d.ts +22 -0
- package/dist/components/Rate.svelte +121 -0
- package/dist/components/Rate.svelte.d.ts +16 -0
- package/dist/components/RecordDetailDrawer.svelte +6 -2
- package/dist/components/RecordDetailDrawer.svelte.d.ts +4 -0
- package/dist/components/RelationPicker.svelte +341 -0
- package/dist/components/RelationPicker.svelte.d.ts +26 -0
- package/dist/components/ResourceOperationsPage.svelte +25 -19
- package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
- package/dist/components/RetryTaskButton.svelte +12 -42
- package/dist/components/SecuritySettings.svelte +29 -5
- package/dist/components/SegmentedControl.svelte +145 -0
- package/dist/components/SegmentedControl.svelte.d.ts +38 -0
- package/dist/components/SettingsPage.svelte +3 -1
- package/dist/components/SettingsPage.svelte.d.ts +1 -0
- package/dist/components/ShowInferencer.svelte +4 -2
- package/dist/components/ShowInferencer.svelte.d.ts +2 -0
- package/dist/components/ShowPage.svelte +25 -6
- package/dist/components/ShowPage.svelte.d.ts +5 -0
- package/dist/components/Sidebar.svelte +5 -5
- package/dist/components/Slider.svelte +100 -0
- package/dist/components/Slider.svelte.d.ts +19 -0
- package/dist/components/SpreadsheetView.svelte +1037 -124
- package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
- package/dist/components/StepsForm.svelte +5 -5
- package/dist/components/SvarAutoTable.svelte +39 -0
- package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
- package/dist/components/SvarDataGrid.svelte +362 -0
- package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
- package/dist/components/SvarGridCell.svelte +10 -0
- package/dist/components/SvarGridCell.svelte.d.ts +7 -0
- package/dist/components/SvarResourceRead.svelte +50 -0
- package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
- package/dist/components/SvarResourceTable.svelte +63 -0
- package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
- package/dist/components/SvarResourceTableView.svelte +364 -0
- package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
- package/dist/components/TagsInput.svelte +178 -0
- package/dist/components/TagsInput.svelte.d.ts +17 -0
- package/dist/components/TaskQueueDrawer.svelte +72 -16
- package/dist/components/TimePicker.svelte +10 -0
- package/dist/components/TimePicker.svelte.d.ts +6 -0
- package/dist/components/TreeSelect.svelte +440 -46
- package/dist/components/TreeSelect.svelte.d.ts +6 -1
- package/dist/components/TreeTable.svelte +484 -82
- package/dist/components/TreeTable.svelte.d.ts +24 -1
- package/dist/components/TwoFactorAuthPage.svelte +85 -48
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
- package/dist/components/VirtualTable.svelte +116 -20
- package/dist/components/VirtualTable.svelte.d.ts +5 -0
- package/dist/components/account/CompanyProfilePage.svelte +74 -38
- package/dist/components/account/GetStartedPage.svelte +2 -1
- package/dist/components/account/ImportMembersPage.svelte +22 -167
- package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
- package/dist/components/account/MemberDirectory.svelte +156 -0
- package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
- package/dist/components/account/MembersStarterPage.svelte +9 -55
- package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
- package/dist/components/account/SecurityLogPage.svelte +4 -1
- package/dist/components/account/SettingsPlainPage.svelte +7 -38
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
- package/dist/components/account/SettingsSidebarPage.svelte +7 -20
- package/dist/components/account/TeamMembersPage.svelte +4 -20
- package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
- package/dist/components/account/UserProfilePage.svelte +3 -25
- package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
- package/dist/components/auto-table-grid.d.ts +26 -0
- package/dist/components/auto-table-grid.js +1 -0
- package/dist/components/buttons/ExportButton.svelte +56 -12
- package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
- package/dist/components/content/ContentPageHeader.svelte +12 -9
- package/dist/components/content/ContentPageShell.svelte +6 -9
- package/dist/components/content/FileBrowser.svelte +35 -0
- package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
- package/dist/components/content/MetricBlock.svelte +10 -13
- package/dist/components/content/NetworkTable.svelte +3 -3
- package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
- package/dist/components/content/PageToolbar.svelte +6 -10
- package/dist/components/content/ReferenceNotice.svelte +5 -0
- package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
- package/dist/components/content/SectionHeader.svelte +8 -13
- package/dist/components/content/SettingsFieldRow.svelte +9 -23
- package/dist/components/content/SettingsGroup.svelte +11 -27
- package/dist/components/content/StatusBadge.svelte +3 -8
- package/dist/components/content/WorkspaceLayout.svelte +8 -12
- package/dist/components/enterprise/filter-tree.d.ts +48 -0
- package/dist/components/enterprise/filter-tree.js +224 -0
- package/dist/components/enterprise/json-schema-form.d.ts +39 -0
- package/dist/components/enterprise/json-schema-form.js +195 -0
- package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
- package/dist/components/enterprise/recursive-schema-form.js +196 -0
- package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
- package/dist/components/enterprise/spreadsheet-formula.js +245 -0
- package/dist/components/export-reference.d.ts +2 -0
- package/dist/components/export-reference.js +9 -0
- package/dist/components/fieldComponentMap.js +2 -0
- package/dist/components/fields/DateField.svelte +12 -12
- package/dist/components/fields/date-display.d.ts +6 -0
- package/dist/components/fields/date-display.js +24 -0
- package/dist/components/fields/date-field-value.d.ts +2 -0
- package/dist/components/fields/date-field-value.js +13 -0
- package/dist/components/json-schema-form-state.d.ts +37 -0
- package/dist/components/json-schema-form-state.js +168 -0
- package/dist/components/network/TeamCrewTablePage.svelte +6 -1
- package/dist/components/network/UserCardsNFTPage.svelte +2 -0
- package/dist/components/profile/PublicProfilePage.svelte +2 -0
- package/dist/components/record-detail.svelte.d.ts +3 -0
- package/dist/components/record-detail.svelte.js +10 -4
- package/dist/components/saved-list-views.d.ts +74 -1
- package/dist/components/saved-list-views.js +191 -3
- package/dist/components/svar-grid-cells.d.ts +14 -0
- package/dist/components/svar-grid-cells.js +2 -0
- package/dist/components/svar-grid-contract.d.ts +80 -0
- package/dist/components/svar-grid-contract.js +159 -0
- package/dist/components/svar-grid-interactions.d.ts +54 -0
- package/dist/components/svar-grid-interactions.js +174 -0
- package/dist/components/svar-grid-loading.d.ts +40 -0
- package/dist/components/svar-grid-loading.js +180 -0
- package/dist/components/svar-grid-operations.d.ts +37 -0
- package/dist/components/svar-grid-operations.js +186 -0
- package/dist/components/svar-grid-panes.d.ts +34 -0
- package/dist/components/svar-grid-panes.js +66 -0
- package/dist/components/svar-grid-resource-loading.d.ts +18 -0
- package/dist/components/svar-grid-resource-loading.js +68 -0
- package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
- package/dist/components/svar-grid-resource-session.svelte.js +61 -0
- package/dist/components/table-contract.d.ts +12 -1
- package/dist/components/task-action.svelte.d.ts +13 -0
- package/dist/components/task-action.svelte.js +73 -0
- package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
- package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
- package/dist/components/ui/badge/badge-variants.d.ts +3 -2
- package/dist/components/ui/badge/badge-variants.js +2 -1
- package/dist/components/ui/badge/badge.svelte +3 -0
- package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
- package/dist/components/ui/button/button-variants.d.ts +5 -4
- package/dist/components/ui/button/button-variants.js +7 -2
- package/dist/components/ui/checkbox/checkbox.svelte +5 -1
- package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
- package/dist/components/ui/input/input.svelte +8 -6
- package/dist/components/ui/input/input.svelte.d.ts +4 -1
- package/dist/components/ui/select/select.svelte +4 -1
- package/dist/components/ui/select/select.svelte.d.ts +2 -1
- package/dist/components/ui/textarea/textarea.svelte +2 -1
- package/dist/components.css +3110 -0
- package/dist/date-input-policy.d.ts +4 -0
- package/dist/date-input-policy.js +35 -0
- package/dist/design-contract.d.ts +13 -0
- package/dist/design-contract.js +11 -0
- package/dist/design-system-components.d.ts +100 -0
- package/dist/design-system-components.js +101 -0
- package/dist/design-system.d.ts +15 -0
- package/dist/design-system.js +7 -0
- package/dist/devtools-bridge.d.ts +80 -0
- package/dist/devtools-bridge.js +92 -0
- package/dist/index.d.ts +50 -1
- package/dist/index.js +32 -2
- package/dist/page-chrome.css +12 -49
- package/dist/recipes.d.ts +960 -0
- package/dist/recipes.js +264 -0
- package/dist/rendering/index.d.ts +87 -0
- package/dist/rendering/index.js +117 -0
- package/dist/styles/aliases.css +49 -0
- package/dist/styles/compatibility.epilogue.css +384 -0
- package/dist/styles/compatibility.prelude.css +5513 -0
- package/dist/styles/conditional-states.css +28 -0
- package/dist/styles/conditional-utilities.css +22 -0
- package/dist/tree-table-model.d.ts +24 -0
- package/dist/tree-table-model.js +111 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -1
- package/package.json +40 -10
- package/styles-compatibility.json +12 -0
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { captureAdminContext, captureAuthSession, IMPORT_LIMITS, snapshotImportArtifact, snapshotImportTaskResult, useCan, useImport, useResourceContract, useSubmitTask, useTask } from '@svadmin/core';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import { definedReactiveOptions } from '@svadmin/core/options';
|
|
3
5
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
6
|
+
import { parseContractCreateInput } from '@svadmin/core/resource-contract';
|
|
7
|
+
import type { ImportArtifactProvider, TaskProvider } from '@svadmin/core';
|
|
8
|
+
import { normalizeTaskStatus, resolveTaskProgress, canCancelTask } from './task-utils.js';
|
|
9
|
+
import TaskStatusBadge from './TaskStatusBadge.svelte';
|
|
10
|
+
import CancelTaskButton from './CancelTaskButton.svelte';
|
|
4
11
|
import { parseCSV, toCsv } from '@svadmin/core';
|
|
5
12
|
import * as Dialog from './ui/dialog/index.js';
|
|
6
13
|
import { Button } from './ui/button/index.js';
|
|
7
14
|
import { Progress } from './ui/progress/index.js';
|
|
8
15
|
import { Badge } from './ui/badge/index.js';
|
|
9
|
-
import { Upload, ArrowRight, ArrowLeft, AlertCircle, Download, FileSpreadsheet, Loader2 } from '@lucide/svelte';
|
|
16
|
+
import { Upload, ArrowRight, ArrowLeft, AlertCircle, Download, FileSpreadsheet, Loader2, RotateCw } from '@lucide/svelte';
|
|
10
17
|
|
|
11
18
|
interface Props {
|
|
12
19
|
resourceName: string;
|
|
13
20
|
open?: boolean;
|
|
14
21
|
batchSize?: number;
|
|
22
|
+
maxRows?: number;
|
|
23
|
+
maxBytes?: number;
|
|
24
|
+
taskName?: string;
|
|
25
|
+
retryTaskName?: string;
|
|
26
|
+
taskProvider?: TaskProvider;
|
|
27
|
+
/** 上传原文件并以 artifact 引用提交任务,避免任务请求重复携带 records。 */
|
|
28
|
+
taskArtifactProvider?: ImportArtifactProvider;
|
|
29
|
+
taskIdempotencyKey?: string;
|
|
30
|
+
initialTaskId?: string;
|
|
31
|
+
onTaskSubmitted?: (taskId: string) => void;
|
|
15
32
|
onSuccess?: (result: { succeeded: number; failed: number }) => void;
|
|
16
33
|
}
|
|
17
34
|
|
|
@@ -19,69 +36,174 @@
|
|
|
19
36
|
resourceName,
|
|
20
37
|
open = $bindable(false),
|
|
21
38
|
batchSize = 20,
|
|
39
|
+
maxRows = IMPORT_LIMITS.maxRows,
|
|
40
|
+
maxBytes = IMPORT_LIMITS.maxBytes,
|
|
41
|
+
taskName,
|
|
42
|
+
retryTaskName,
|
|
43
|
+
taskProvider,
|
|
44
|
+
taskArtifactProvider,
|
|
45
|
+
taskIdempotencyKey,
|
|
46
|
+
initialTaskId,
|
|
47
|
+
onTaskSubmitted,
|
|
22
48
|
onSuccess,
|
|
23
49
|
}: Props = $props();
|
|
24
50
|
|
|
25
51
|
const i18n = useTranslation();
|
|
26
|
-
const
|
|
27
|
-
const
|
|
52
|
+
const context = captureAdminContext();
|
|
53
|
+
const binding = useResourceContract(() => resourceName);
|
|
54
|
+
const resource = $derived(context.getResource(resourceName));
|
|
55
|
+
const can = useCan(() => ({ resource: resourceName, action: 'import' }));
|
|
56
|
+
const session = $derived(captureAuthSession(context.authProvider));
|
|
57
|
+
const allowed = $derived(can.allowed === true && resource.canCreate !== false && session.available);
|
|
58
|
+
const activeTaskProvider = $derived(taskProvider ?? context.taskProvider);
|
|
59
|
+
const scope = $derived({
|
|
60
|
+
contract: binding.resource, provider: context.providers?.[binding.dataProviderName],
|
|
61
|
+
tenant: context.tenantCacheKey?.__svadminTenant, meta: binding.meta,
|
|
62
|
+
auth: context.authProvider, router: context.routerProvider, session, allowed,
|
|
63
|
+
taskProvider: activeTaskProvider, taskName, retryTaskName, taskIdempotencyKey, taskArtifactProvider,
|
|
64
|
+
access: context.accessControlProvider,
|
|
65
|
+
});
|
|
28
66
|
const availableFields = $derived(
|
|
29
67
|
resource.fields.filter((f) => f.key !== (resource.primaryKey ?? 'id') && f.showInForm !== false)
|
|
30
68
|
);
|
|
31
69
|
|
|
32
70
|
let currentStep = $state<1 | 2 | 3>(1);
|
|
33
71
|
let fileName = $state('');
|
|
72
|
+
let selectedFile = $state<File | null>(null);
|
|
34
73
|
let rawHeaders = $state<string[]>([]);
|
|
35
|
-
let rawRows = $state<
|
|
74
|
+
let rawRows = $state<unknown[][]>([]);
|
|
36
75
|
let columnMapping = $state<Record<string, string>>({}); // header -> fieldKey or ''
|
|
76
|
+
let fileError = $state<string | null>(null);
|
|
77
|
+
let importError = $state<string | null>(null);
|
|
78
|
+
let parseToken = 0;
|
|
79
|
+
let mounted = true;
|
|
80
|
+
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);
|
|
84
|
+
let durableHandledTaskId = $state<string | undefined>();
|
|
85
|
+
let durableSubmitAttempt = $state.raw<object | undefined>();
|
|
86
|
+
let artifactUploadController: AbortController | undefined;
|
|
87
|
+
let durablePolling = $state(true);
|
|
88
|
+
const durableMode = $derived(taskName !== undefined);
|
|
89
|
+
const submitTask = useSubmitTask();
|
|
90
|
+
const taskQuery = useTask(definedReactiveOptions({
|
|
91
|
+
get taskId() { return ownedTaskId; },
|
|
92
|
+
get taskProvider() { return activeTaskProvider; },
|
|
93
|
+
get queryOptions() {
|
|
94
|
+
return {
|
|
95
|
+
enabled: open && allowed && durableMode && !!activeTaskProvider && !!ownedTaskId,
|
|
96
|
+
refetchInterval: open && allowed && durableMode && ownedTaskId && durablePolling ? 2000 : false as const,
|
|
97
|
+
refetchOnWindowFocus: true,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
const durableStatus = $derived(normalizeTaskStatus(taskQuery.data?.status));
|
|
102
|
+
const retryReceipt = $derived(durableStatus === 'completed'
|
|
103
|
+
? snapshotImportTaskResult(taskQuery.data?.result ?? taskQuery.data?.result_data)?.retry : undefined);
|
|
104
|
+
const durableActive = $derived(durableStatus === 'queued' || durableStatus === 'processing'
|
|
105
|
+
|| !!durableSubmitAttempt);
|
|
37
106
|
|
|
38
|
-
let
|
|
39
|
-
let processedCount = $state(0);
|
|
107
|
+
let importResult = $state<{ succeeded: number; failed: number } | null>(null);
|
|
40
108
|
let succeededCount = $state(0);
|
|
41
109
|
let failedRecords = $state<Array<{ row: Record<string, unknown>; error: string }>>([]);
|
|
42
110
|
|
|
111
|
+
const importer = useImport(definedReactiveOptions({
|
|
112
|
+
get resource() { return binding.resource; },
|
|
113
|
+
get dataProviderName() { return binding.dataProviderName; },
|
|
114
|
+
get batchSize() { return batchSize; },
|
|
115
|
+
get maxRows() { return maxRows; },
|
|
116
|
+
get maxBytes() { return maxBytes; },
|
|
117
|
+
get enabled() { return open && allowed; },
|
|
118
|
+
mapData: mapRow,
|
|
119
|
+
}));
|
|
120
|
+
const isImporting = $derived(importer.isLoading || durableActive);
|
|
121
|
+
const processedCount = $derived(importer.progress.processedAmount);
|
|
122
|
+
const totalCount = $derived(importer.progress.totalAmount || rawRows.length);
|
|
43
123
|
const progressPercent = $derived(
|
|
44
|
-
|
|
124
|
+
totalCount > 0 ? Math.min(100, Math.round((processedCount / totalCount) * 100)) : 0
|
|
45
125
|
);
|
|
46
126
|
|
|
47
|
-
async function handleFileSelect(
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
127
|
+
async function handleFileSelect(file: File) {
|
|
128
|
+
if (!open || !allowed || isImporting) return;
|
|
129
|
+
const origin = scope;
|
|
130
|
+
const token = ++parseToken;
|
|
131
|
+
fileError = null;
|
|
132
|
+
importError = null;
|
|
133
|
+
durableTaskId = undefined;
|
|
134
|
+
durableHandledTaskId = undefined;
|
|
135
|
+
durableSubmitAttempt = undefined;
|
|
136
|
+
selectedFile = null;
|
|
137
|
+
fileName = file.name;
|
|
138
|
+
rawHeaders = [];
|
|
139
|
+
rawRows = [];
|
|
140
|
+
columnMapping = {};
|
|
141
|
+
try {
|
|
142
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > IMPORT_LIMITS.maxRows
|
|
143
|
+
|| !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > IMPORT_LIMITS.maxBytes) {
|
|
144
|
+
throw new Error(i18n.t('import.invalidLimits'));
|
|
145
|
+
}
|
|
146
|
+
if (file.size > maxBytes) throw new Error(i18n.t('import.limit'));
|
|
147
|
+
const text = await file.text();
|
|
148
|
+
if (!current(origin, token)) return;
|
|
149
|
+
if (text.length > maxBytes) throw new Error(i18n.t('import.limit'));
|
|
150
|
+
const cleanText = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
51
151
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (Array.isArray(json)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
152
|
+
if (file.name.toLowerCase().endsWith('.json')) {
|
|
153
|
+
const json: unknown = JSON.parse(cleanText);
|
|
154
|
+
if (Array.isArray(json) && json.length > maxRows) throw new Error(i18n.t('import.limit'));
|
|
155
|
+
if (!Array.isArray(json) || json.length === 0 || json.some((item) => (
|
|
156
|
+
typeof item !== 'object' || item === null || Array.isArray(item)
|
|
157
|
+
))) {
|
|
158
|
+
throw new Error('The JSON file must contain a non-empty array of records.');
|
|
159
|
+
}
|
|
160
|
+
const records = json as Record<string, unknown>[];
|
|
161
|
+
if (records.length > maxRows) throw new Error(i18n.t('import.limit'));
|
|
162
|
+
const keys = [...new Set(records.flatMap(item => Object.keys(item)))];
|
|
163
|
+
if (keys.length === 0) throw new Error('The import file has no columns.');
|
|
164
|
+
rawHeaders = keys;
|
|
165
|
+
rawRows = records.map((item) => keys.map((key) => item[key]));
|
|
166
|
+
} else if (file.name.toLowerCase().endsWith('.csv')) {
|
|
167
|
+
const rows = parseCSV(cleanText);
|
|
168
|
+
const firstRow = rows[0];
|
|
169
|
+
if (!firstRow || firstRow.length === 0 || firstRow.every((cell) => !cell.trim())) {
|
|
170
|
+
throw new Error('The CSV file has no header row.');
|
|
171
|
+
}
|
|
172
|
+
if (firstRow.some(header => !header.trim()) || new Set(firstRow).size !== firstRow.length) {
|
|
173
|
+
throw new Error('CSV column names must be non-empty and unique.');
|
|
174
|
+
}
|
|
175
|
+
const records = rows.slice(1).filter((r: string[]) =>
|
|
176
|
+
r.some((cell: string) => cell.trim().length > 0));
|
|
177
|
+
if (records.length === 0) throw new Error('The import file has no records.');
|
|
178
|
+
if (records.length > maxRows) throw new Error(i18n.t('import.limit'));
|
|
179
|
+
if (records.some((row) => row.length !== firstRow.length)) {
|
|
180
|
+
throw new Error('The CSV file contains rows with inconsistent column counts.');
|
|
59
181
|
}
|
|
60
|
-
} catch {
|
|
61
|
-
/* invalid json */
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
const rows = parseCSV(cleanText);
|
|
65
|
-
const firstRow = rows[0];
|
|
66
|
-
if (firstRow) {
|
|
67
182
|
rawHeaders = firstRow;
|
|
68
|
-
rawRows =
|
|
183
|
+
rawRows = records;
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error('Only CSV and JSON files are supported.');
|
|
69
186
|
}
|
|
70
|
-
}
|
|
71
187
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
(
|
|
78
|
-
f
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
188
|
+
if (!current(origin, token)) return;
|
|
189
|
+
selectedFile = file;
|
|
190
|
+
const initialMapping: Record<string, string> = {};
|
|
191
|
+
for (const h of rawHeaders) {
|
|
192
|
+
const normalized = h.toLowerCase().trim().replace(/[-_]/g, '');
|
|
193
|
+
const match = availableFields.find(
|
|
194
|
+
(f) =>
|
|
195
|
+
f.key.toLowerCase().replace(/[-_]/g, '') === normalized ||
|
|
196
|
+
f.label.toLowerCase().replace(/[-_]/g, '') === normalized
|
|
197
|
+
);
|
|
198
|
+
initialMapping[h] = match ? match.key : '';
|
|
199
|
+
}
|
|
200
|
+
columnMapping = initialMapping;
|
|
201
|
+
currentStep = 2;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (!current(origin, token)) return;
|
|
204
|
+
fileError = error instanceof Error ? error.message : 'Unable to read the import file.';
|
|
205
|
+
currentStep = 1;
|
|
82
206
|
}
|
|
83
|
-
columnMapping = initialMapping;
|
|
84
|
-
currentStep = 2;
|
|
85
207
|
}
|
|
86
208
|
|
|
87
209
|
function handleDrop(e: DragEvent) {
|
|
@@ -91,63 +213,228 @@
|
|
|
91
213
|
}
|
|
92
214
|
}
|
|
93
215
|
|
|
216
|
+
function mapRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
217
|
+
const record: Record<string, unknown> = {};
|
|
218
|
+
rawHeaders.forEach((header) => {
|
|
219
|
+
const targetFieldKey = columnMapping[header];
|
|
220
|
+
if (!targetFieldKey) return;
|
|
221
|
+
const fieldDef = availableFields.find((f) => f.key === targetFieldKey);
|
|
222
|
+
let val: unknown = row[header];
|
|
223
|
+
if (typeof val === 'string') {
|
|
224
|
+
if (fieldDef?.type === 'number' || fieldDef?.type === 'currency' || fieldDef?.type === 'percent') {
|
|
225
|
+
const num = val.trim() === '' ? NaN : Number(val);
|
|
226
|
+
val = val.trim() === '' ? undefined : Number.isFinite(num) ? num : val;
|
|
227
|
+
} else if (fieldDef?.type === 'boolean') {
|
|
228
|
+
const normalized = val.trim().toLowerCase();
|
|
229
|
+
if (['true', '1', 'yes', '是'].includes(normalized)) val = true;
|
|
230
|
+
else if (['false', '0', 'no', '否'].includes(normalized)) val = false;
|
|
231
|
+
} else if (fieldDef?.type === 'select' && fieldDef.options && selectedFile?.name.toLowerCase().endsWith('.csv')) {
|
|
232
|
+
const matches = fieldDef.options.filter(option => String(option.value) === val);
|
|
233
|
+
const match = matches[0];
|
|
234
|
+
if (matches.length !== 1 || !match || match.disabled) throw new Error('Invalid import option.');
|
|
235
|
+
val = match.value;
|
|
236
|
+
} else if (fieldDef?.type === 'json' && selectedFile?.name.toLowerCase().endsWith('.csv')) {
|
|
237
|
+
val = JSON.parse(val);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (val !== undefined) {
|
|
241
|
+
Object.defineProperty(record, targetFieldKey, { value: val, enumerable: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return record;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function validateMappedRows(): string | null {
|
|
248
|
+
for (const [index, source] of rawRows.entries()) {
|
|
249
|
+
try {
|
|
250
|
+
const mapped = mapRow(Object.fromEntries(rawHeaders.map((header, column) => [header, source[column]])));
|
|
251
|
+
parseContractCreateInput(binding.resource, mapped);
|
|
252
|
+
} catch {
|
|
253
|
+
return i18n.t('import.invalidRow', { row: index + 1 });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
94
259
|
async function startImport() {
|
|
260
|
+
if (!selectedFile || rawRows.length === 0 || isImporting || !open || !allowed) return;
|
|
261
|
+
if (durableMode && (!activeTaskProvider || !taskName?.trim() || !taskIdempotencyKey?.trim())) {
|
|
262
|
+
fileError = i18n.t('import.taskConfiguration');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const targets = Object.values(columnMapping).filter(Boolean);
|
|
266
|
+
if (targets.length === 0 || new Set(targets).size !== targets.length) {
|
|
267
|
+
fileError = 'Map at least one column, and use each target field only once.';
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const mappingError = validateMappedRows();
|
|
271
|
+
if (mappingError) {
|
|
272
|
+
fileError = mappingError;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const origin = scope;
|
|
276
|
+
const token = parseToken;
|
|
277
|
+
const notifySuccess = onSuccess;
|
|
95
278
|
currentStep = 3;
|
|
96
|
-
isImporting = true;
|
|
97
|
-
processedCount = 0;
|
|
98
279
|
succeededCount = 0;
|
|
99
280
|
failedRecords = [];
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (fieldDef?.type === 'number') {
|
|
113
|
-
const num = Number(val);
|
|
114
|
-
val = isNaN(num) ? val : num;
|
|
115
|
-
} else if (fieldDef?.type === 'boolean') {
|
|
116
|
-
val = val === 'true' || val === '1' || val === 'yes' || val === '是';
|
|
117
|
-
}
|
|
118
|
-
record[targetFieldKey] = val;
|
|
281
|
+
importResult = null;
|
|
282
|
+
importError = null;
|
|
283
|
+
cancelled = false;
|
|
284
|
+
if (durableMode) {
|
|
285
|
+
if (durableTaskId || durableSubmitAttempt) return;
|
|
286
|
+
const attempt = {};
|
|
287
|
+
durableSubmitAttempt = attempt;
|
|
288
|
+
const uploadController = new AbortController();
|
|
289
|
+
artifactUploadController = uploadController;
|
|
290
|
+
try {
|
|
291
|
+
if (!activeTaskProvider || !taskName?.trim() || !taskIdempotencyKey?.trim()) {
|
|
292
|
+
throw new Error(i18n.t('import.taskConfiguration'));
|
|
119
293
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
294
|
+
const records = taskArtifactProvider ? undefined : rawRows.map((source, index) => {
|
|
295
|
+
const mapped = mapRow(Object.fromEntries(rawHeaders.map((header, column) => [header, source[column]])));
|
|
296
|
+
try { return parseContractCreateInput(binding.resource, mapped); }
|
|
297
|
+
catch { throw new Error(i18n.t('import.invalidRow', { row: index + 1 })); }
|
|
298
|
+
});
|
|
299
|
+
const uploaded = taskArtifactProvider
|
|
300
|
+
? snapshotImportArtifact(await taskArtifactProvider.upload({
|
|
301
|
+
file: selectedFile,
|
|
302
|
+
fileName,
|
|
303
|
+
size: selectedFile.size,
|
|
304
|
+
resource: binding.resource.name,
|
|
305
|
+
idempotencyKey: JSON.stringify(['import-artifact', taskIdempotencyKey]),
|
|
306
|
+
signal: uploadController.signal,
|
|
307
|
+
}))
|
|
308
|
+
: undefined;
|
|
309
|
+
if (durableSubmitAttempt !== attempt || !current(origin, token) || uploadController.signal.aborted) return;
|
|
310
|
+
if (taskArtifactProvider && !uploaded) throw new Error(i18n.t('import.taskPayloadLimit'));
|
|
311
|
+
if (uploaded && (uploaded.size !== selectedFile.size || uploaded.fileName !== fileName)) {
|
|
312
|
+
throw new Error(i18n.t('import.taskPayloadLimit'));
|
|
135
313
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
314
|
+
const body = uploaded ? {
|
|
315
|
+
protocolVersion: 2,
|
|
316
|
+
resource: binding.resource.name,
|
|
317
|
+
fileName,
|
|
318
|
+
mapping: { ...columnMapping },
|
|
319
|
+
artifact: uploaded,
|
|
320
|
+
} : {
|
|
321
|
+
protocolVersion: 1,
|
|
322
|
+
resource: binding.resource.name,
|
|
323
|
+
fileName,
|
|
324
|
+
mapping: { ...columnMapping },
|
|
325
|
+
records,
|
|
326
|
+
};
|
|
327
|
+
const encoded = JSON.stringify(body);
|
|
328
|
+
if (new TextEncoder().encode(encoded).byteLength > maxBytes) {
|
|
329
|
+
fileError = i18n.t('import.taskPayloadLimit');
|
|
330
|
+
currentStep = 2;
|
|
331
|
+
return;
|
|
144
332
|
}
|
|
333
|
+
const handle = await submitTask.mutation.mutateAsync({
|
|
334
|
+
taskName: taskName.trim(),
|
|
335
|
+
taskProvider: activeTaskProvider,
|
|
336
|
+
successNotification: false,
|
|
337
|
+
errorNotification: false,
|
|
338
|
+
options: {
|
|
339
|
+
body,
|
|
340
|
+
idempotencyKey: taskIdempotencyKey,
|
|
341
|
+
...(binding.meta === undefined ? {} : { meta: binding.meta }),
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
if (durableSubmitAttempt !== attempt || !current(origin, token)) return;
|
|
345
|
+
durableTaskId = handle.id;
|
|
346
|
+
durableTaskScope = scope;
|
|
347
|
+
durableHandledTaskId = undefined;
|
|
348
|
+
currentStep = 3;
|
|
349
|
+
try { void Promise.resolve(onTaskSubmitted?.(handle.id)).catch(() => {}); }
|
|
350
|
+
catch { /* 观察者失败不重发任务。 */ }
|
|
351
|
+
} catch {
|
|
352
|
+
if (durableSubmitAttempt !== attempt || !current(origin, token)) return;
|
|
353
|
+
importError = i18n.t('import.taskUnconfirmed');
|
|
354
|
+
} finally {
|
|
355
|
+
if (artifactUploadController === uploadController) artifactUploadController = undefined;
|
|
356
|
+
if (durableSubmitAttempt === attempt) durableSubmitAttempt = undefined;
|
|
145
357
|
}
|
|
146
|
-
|
|
358
|
+
return;
|
|
147
359
|
}
|
|
360
|
+
try {
|
|
361
|
+
const result = await importer.handleChange({ file: selectedFile });
|
|
362
|
+
if (!current(origin, token)) return;
|
|
363
|
+
succeededCount = result.succeeded.length;
|
|
364
|
+
failedRecords = result.errored.map(item => ({
|
|
365
|
+
row: item.request as Record<string, unknown>,
|
|
366
|
+
error: item.error.message,
|
|
367
|
+
}));
|
|
368
|
+
importResult = { succeeded: succeededCount, failed: failedRecords.length };
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (!current(origin, token)) return;
|
|
371
|
+
importError = error instanceof Error ? error.message : 'Import failed.';
|
|
372
|
+
importResult = null;
|
|
373
|
+
}
|
|
374
|
+
if (importResult) {
|
|
375
|
+
try { notifySuccess?.(importResult); }
|
|
376
|
+
catch { /* 消费方回调失败不能把已完成的写入改判为导入失败。 */ }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function current(origin: typeof scope, token: number): boolean {
|
|
381
|
+
return mounted && open && allowed && sameScope(origin, scope) && origin.session.isCurrent() && token === parseToken;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function sameScope(left: typeof scope, right: typeof scope): boolean {
|
|
385
|
+
return left.contract === right.contract && left.provider === right.provider &&
|
|
386
|
+
left.tenant === right.tenant && JSON.stringify(left.meta) === JSON.stringify(right.meta) &&
|
|
387
|
+
left.auth === right.auth && left.session.cacheKey === right.session.cacheKey &&
|
|
388
|
+
left.router === right.router && left.access === right.access &&
|
|
389
|
+
left.taskProvider === right.taskProvider && left.taskArtifactProvider === right.taskArtifactProvider &&
|
|
390
|
+
left.taskName === right.taskName && left.retryTaskName === right.retryTaskName &&
|
|
391
|
+
left.taskIdempotencyKey === right.taskIdempotencyKey;
|
|
392
|
+
}
|
|
148
393
|
|
|
149
|
-
|
|
150
|
-
|
|
394
|
+
async function retryFailedRows(): Promise<void> {
|
|
395
|
+
const receipt = retryReceipt;
|
|
396
|
+
const parentTaskId = ownedTaskId;
|
|
397
|
+
if (!open || !allowed || !receipt || !parentTaskId || !retryTaskName?.trim()
|
|
398
|
+
|| !activeTaskProvider || durableSubmitAttempt || taskQuery.isError) return;
|
|
399
|
+
const origin = scope;
|
|
400
|
+
const token = parseToken;
|
|
401
|
+
const attempt = {};
|
|
402
|
+
durableSubmitAttempt = attempt;
|
|
403
|
+
importError = null;
|
|
404
|
+
try {
|
|
405
|
+
// 同一父任务和不可变回执始终使用同一键;未知提交结果不得转成新写入。
|
|
406
|
+
const handle = await submitTask.mutation.mutateAsync({
|
|
407
|
+
taskName: retryTaskName.trim(),
|
|
408
|
+
taskProvider: activeTaskProvider,
|
|
409
|
+
successNotification: false,
|
|
410
|
+
errorNotification: false,
|
|
411
|
+
options: {
|
|
412
|
+
idempotencyKey: JSON.stringify(['import-failed-rows', parentTaskId, receipt.receiptId]),
|
|
413
|
+
body: {
|
|
414
|
+
protocolVersion: 1,
|
|
415
|
+
operation: 'retry-failed-rows',
|
|
416
|
+
resource: binding.resource.name,
|
|
417
|
+
parentTaskId,
|
|
418
|
+
receiptId: receipt.receiptId,
|
|
419
|
+
rows: [...receipt.rows].sort((a, b) => a - b),
|
|
420
|
+
},
|
|
421
|
+
...(binding.meta === undefined ? {} : { meta: binding.meta }),
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
if (durableSubmitAttempt !== attempt || !current(origin, token)) return;
|
|
425
|
+
if (handle.id === parentTaskId) throw new Error('Retry must create a child task');
|
|
426
|
+
durableTaskId = handle.id;
|
|
427
|
+
durableTaskScope = scope;
|
|
428
|
+
durableHandledTaskId = undefined;
|
|
429
|
+
durablePolling = true;
|
|
430
|
+
importResult = null;
|
|
431
|
+
try { void Promise.resolve(onTaskSubmitted?.(handle.id)).catch(() => {}); }
|
|
432
|
+
catch { /* 通知失败不重复提交。 */ }
|
|
433
|
+
} catch {
|
|
434
|
+
if (durableSubmitAttempt === attempt && current(origin, token)) importError = i18n.t('import.taskUnconfirmed');
|
|
435
|
+
} finally {
|
|
436
|
+
if (durableSubmitAttempt === attempt) durableSubmitAttempt = undefined;
|
|
437
|
+
}
|
|
151
438
|
}
|
|
152
439
|
|
|
153
440
|
function downloadErrors() {
|
|
@@ -161,22 +448,88 @@
|
|
|
161
448
|
const url = URL.createObjectURL(blob);
|
|
162
449
|
const link = document.createElement('a');
|
|
163
450
|
link.href = url;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
451
|
+
const safeResourceName = resourceName.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
452
|
+
.replace(/^[._-]+/, '').slice(0, 100).replace(/[._-]+$/, '') || 'resource';
|
|
453
|
+
link.download = `${safeResourceName}-import-errors.csv`;
|
|
454
|
+
try {
|
|
455
|
+
link.click();
|
|
456
|
+
} finally {
|
|
457
|
+
// 浏览器需要在 click 返回后仍能读取对象 URL,延迟回收避免空下载。
|
|
458
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
459
|
+
}
|
|
167
460
|
}
|
|
168
461
|
|
|
169
462
|
function reset() {
|
|
463
|
+
artifactUploadController?.abort();
|
|
464
|
+
artifactUploadController = undefined;
|
|
465
|
+
parseToken += 1;
|
|
170
466
|
currentStep = 1;
|
|
171
467
|
fileName = '';
|
|
468
|
+
selectedFile = null;
|
|
172
469
|
rawHeaders = [];
|
|
173
470
|
rawRows = [];
|
|
174
471
|
columnMapping = {};
|
|
175
|
-
processedCount = 0;
|
|
176
472
|
succeededCount = 0;
|
|
177
473
|
failedRecords = [];
|
|
178
|
-
|
|
474
|
+
importResult = null;
|
|
475
|
+
fileError = null;
|
|
476
|
+
importError = null;
|
|
477
|
+
cancelled = false;
|
|
478
|
+
durableSubmitAttempt = undefined;
|
|
479
|
+
durableHandledTaskId = undefined;
|
|
480
|
+
durablePolling = true;
|
|
481
|
+
importer.reset();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function cancelImport() {
|
|
485
|
+
if (durableMode && durableTaskId && durableActive) return;
|
|
486
|
+
if (!isImporting) return;
|
|
487
|
+
// 废弃 UI 回执与核心执行令牌;已经发出的请求并不因此回滚。
|
|
488
|
+
parseToken++;
|
|
489
|
+
artifactUploadController?.abort();
|
|
490
|
+
artifactUploadController = undefined;
|
|
491
|
+
durableSubmitAttempt = undefined;
|
|
492
|
+
importer.reset();
|
|
493
|
+
cancelled = true;
|
|
494
|
+
importResult = null;
|
|
495
|
+
importError = null;
|
|
179
496
|
}
|
|
497
|
+
|
|
498
|
+
let observedScope = untrack(() => scope);
|
|
499
|
+
let observedOpen = untrack(() => open);
|
|
500
|
+
$effect.pre(() => {
|
|
501
|
+
const changed = !sameScope(observedScope, scope) || (observedScope.allowed && !allowed);
|
|
502
|
+
if (changed) {
|
|
503
|
+
durableTaskId = undefined;
|
|
504
|
+
durableTaskScope = undefined;
|
|
505
|
+
durableHandledTaskId = undefined;
|
|
506
|
+
}
|
|
507
|
+
if (changed || observedOpen !== open) untrack(reset);
|
|
508
|
+
observedScope = scope;
|
|
509
|
+
observedOpen = open;
|
|
510
|
+
});
|
|
511
|
+
$effect(() => {
|
|
512
|
+
const task = taskQuery.data;
|
|
513
|
+
if (!open || !allowed || !durableTaskId || !task || durableHandledTaskId === durableTaskId) return;
|
|
514
|
+
if (!['completed', 'failed', 'cancelled'].includes(durableStatus)) return;
|
|
515
|
+
durableHandledTaskId = durableTaskId;
|
|
516
|
+
durablePolling = false;
|
|
517
|
+
if (durableStatus === 'completed') {
|
|
518
|
+
const result = snapshotImportTaskResult(task.result ?? task.result_data);
|
|
519
|
+
if (result) {
|
|
520
|
+
succeededCount = result.succeeded;
|
|
521
|
+
importResult = { succeeded: result.succeeded, failed: result.failed };
|
|
522
|
+
try { void Promise.resolve(onSuccess?.(importResult)).catch(() => {}); }
|
|
523
|
+
catch { /* 任务完成通知失败不改变服务端任务结果。 */ }
|
|
524
|
+
} else {
|
|
525
|
+
importError = i18n.t('import.invalidTaskResult');
|
|
526
|
+
}
|
|
527
|
+
} else if (durableStatus === 'failed' || durableStatus === 'cancelled') {
|
|
528
|
+
importError = i18n.t('common.operationFailed');
|
|
529
|
+
importResult = null;
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
$effect(() => () => { mounted = false; parseToken += 1; artifactUploadController?.abort(); });
|
|
180
533
|
</script>
|
|
181
534
|
|
|
182
535
|
{#if open}
|
|
@@ -199,7 +552,44 @@
|
|
|
199
552
|
</div>
|
|
200
553
|
|
|
201
554
|
<!-- Step 1: Upload -->
|
|
202
|
-
{#if
|
|
555
|
+
{#if !allowed}
|
|
556
|
+
<p role="status">{i18n.t('common.operationFailed', { defaultValue: 'Import is unavailable.' })}</p>
|
|
557
|
+
{/if}
|
|
558
|
+
{#if durableMode && (durableTaskId || durableSubmitAttempt || currentStep === 3)}
|
|
559
|
+
{#if durableTaskId}
|
|
560
|
+
<p>{durableTaskId}</p>
|
|
561
|
+
{#if taskQuery.isError}
|
|
562
|
+
<p role="alert">{i18n.t('task.fetchFailed')}</p>
|
|
563
|
+
<Button type="button" onclick={() => taskQuery.refetch()}>{i18n.t('common.retry')}</Button>
|
|
564
|
+
{:else if taskQuery.data && allowed}
|
|
565
|
+
<TaskStatusBadge status={taskQuery.data.status ?? 'pending'} />
|
|
566
|
+
{@const progress = resolveTaskProgress(taskQuery.data)}
|
|
567
|
+
{#if progress !== undefined}
|
|
568
|
+
<Progress value={progress} />
|
|
569
|
+
{/if}
|
|
570
|
+
{#if importResult}
|
|
571
|
+
<p role="status">{i18n.t('common.succeeded')}: {importResult.succeeded};
|
|
572
|
+
{i18n.t('common.failed')}: {importResult.failed}</p>
|
|
573
|
+
{/if}
|
|
574
|
+
{#if retryReceipt && retryTaskName?.trim()}
|
|
575
|
+
<Button type="button" disabled={!allowed || !!durableSubmitAttempt}
|
|
576
|
+
onclick={retryFailedRows}><RotateCw aria-hidden="true" />{i18n.t('import.retryFailedRows')}</Button>
|
|
577
|
+
{/if}
|
|
578
|
+
{#if activeTaskProvider?.cancel && canCancelTask(taskQuery.data)}
|
|
579
|
+
<CancelTaskButton taskId={durableTaskId} taskProvider={activeTaskProvider}
|
|
580
|
+
disabled={!allowed || !open}
|
|
581
|
+
onSuccess={() => { void taskQuery.refetch(); }}
|
|
582
|
+
onError={() => { importError = i18n.t('common.operationFailed'); }} />
|
|
583
|
+
{/if}
|
|
584
|
+
{:else}
|
|
585
|
+
<p role="status">{i18n.t('common.loading')}</p>
|
|
586
|
+
{/if}
|
|
587
|
+
{:else if durableSubmitAttempt}
|
|
588
|
+
<p role="status">{i18n.t('common.processing')}</p>
|
|
589
|
+
{/if}
|
|
590
|
+
{#if importError}<p role="alert">{importError}</p>{/if}
|
|
591
|
+
<Button type="button" onclick={() => { open = false; }}>{i18n.t('common.close')}</Button>
|
|
592
|
+
{:else if currentStep === 1}
|
|
203
593
|
<div
|
|
204
594
|
class="svadmin-u-65935df577ba svadmin-u-a29b7a649c77 svadmin-u-c9ed8c5f79ae svadmin-u-8b9d5d768973 svadmin-u-a217b4eaa918 svadmin-u-845f53365c8d svadmin-u-ca6bf63030aa svadmin-u-ceb69a6b0e5f svadmin-u-34516836730d svadmin-u-967d113a1451"
|
|
205
595
|
ondragover={(e) => e.preventDefault()}
|
|
@@ -218,6 +608,7 @@
|
|
|
218
608
|
<input
|
|
219
609
|
type="file"
|
|
220
610
|
accept=".csv,.json"
|
|
611
|
+
disabled={!allowed}
|
|
221
612
|
class="svadmin-u-99d72c7fc3e2"
|
|
222
613
|
onchange={(e) => {
|
|
223
614
|
const target = e.currentTarget;
|
|
@@ -228,12 +619,16 @@
|
|
|
228
619
|
{i18n.t('common.selectFile', { defaultValue: 'Browse File' })}
|
|
229
620
|
</span>
|
|
230
621
|
</label>
|
|
622
|
+
{#if fileError}
|
|
623
|
+
<p role="alert">{fileError}</p>
|
|
624
|
+
{/if}
|
|
231
625
|
</div>
|
|
232
626
|
{/if}
|
|
233
627
|
|
|
234
628
|
<!-- Step 2: Column Mapping -->
|
|
235
|
-
{#if currentStep === 2}
|
|
629
|
+
{#if currentStep === 2 && !durableTaskId}
|
|
236
630
|
<div class="svadmin-u-3e7ce58d64fa">
|
|
631
|
+
{#if fileError}<p role="alert">{fileError}</p>{/if}
|
|
237
632
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-2859c861d7de svadmin-u-9fe52d5d506c svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-05faf5c801ff">
|
|
238
633
|
<span>{i18n.t('common.detectedRows', { defaultValue: 'Detected' })}: <strong class="svadmin-u-d4108abe6359">{rawRows.length}</strong> {i18n.t('common.records', { defaultValue: 'records' })}</span>
|
|
239
634
|
<Badge variant="secondary" class="svadmin-u-d058ca6de60f svadmin-u-0e65706bcccd">{fileName}</Badge>
|
|
@@ -252,8 +647,13 @@
|
|
|
252
647
|
</div>
|
|
253
648
|
<div class="svadmin-u-74b2435a1d40 svadmin-u-012fbd121f37">
|
|
254
649
|
<select
|
|
650
|
+
aria-label={`Map ${header}`}
|
|
255
651
|
class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
|
|
256
|
-
|
|
652
|
+
value={columnMapping[header]}
|
|
653
|
+
onchange={(event) => {
|
|
654
|
+
columnMapping = { ...columnMapping, [header]: event.currentTarget.value };
|
|
655
|
+
fileError = null;
|
|
656
|
+
}}
|
|
257
657
|
>
|
|
258
658
|
<option value="">— {i18n.t('common.ignoreColumn', { defaultValue: 'Ignore' })} —</option>
|
|
259
659
|
{#each availableFields as field (field.key)}
|
|
@@ -272,7 +672,7 @@
|
|
|
272
672
|
<ArrowLeft class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-618162408e7a" />
|
|
273
673
|
{i18n.t('common.back', { defaultValue: 'Back' })}
|
|
274
674
|
</Button>
|
|
275
|
-
<Button size="sm" onclick={startImport}>
|
|
675
|
+
<Button size="sm" disabled={!allowed || isImporting} onclick={startImport}>
|
|
276
676
|
{i18n.t('common.startImport', { defaultValue: 'Start Import' })}
|
|
277
677
|
<ArrowRight class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-f58b02572ab2" />
|
|
278
678
|
</Button>
|
|
@@ -281,16 +681,19 @@
|
|
|
281
681
|
{/if}
|
|
282
682
|
|
|
283
683
|
<!-- Step 3: Progress & Summary -->
|
|
284
|
-
{#if currentStep === 3}
|
|
684
|
+
{#if currentStep === 3 && !durableMode}
|
|
285
685
|
<div class="svadmin-u-b43b4c086d9a svadmin-u-03b4dd7f172b">
|
|
286
686
|
<div class="svadmin-u-6f7e013d6499">
|
|
287
687
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-359090c2d529">
|
|
288
|
-
<span class="svadmin-u-bfa603190748">{isImporting ? i18n.t('common.importing', { defaultValue: 'Importing...' }) : i18n.t('common.completed', { defaultValue: 'Completed' })}</span>
|
|
289
|
-
<span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359 svadmin-u-3032cae0badb">{progressPercent}%</span>
|
|
688
|
+
<span class="svadmin-u-bfa603190748">{cancelled ? i18n.t('import.stopped') : isImporting ? i18n.t('common.importing', { defaultValue: 'Importing...' }) : importError ? i18n.t('common.operationFailed', { defaultValue: 'Import failed' }) : i18n.t('common.completed', { defaultValue: 'Completed' })}</span>
|
|
689
|
+
{#if !cancelled}<span class="svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359 svadmin-u-3032cae0badb">{progressPercent}%</span>{/if}
|
|
290
690
|
</div>
|
|
291
|
-
<Progress value={progressPercent} class="svadmin-u-2f2a842e50fa" />
|
|
691
|
+
{#if !cancelled}<Progress value={progressPercent} class="svadmin-u-2f2a842e50fa" />{/if}
|
|
292
692
|
</div>
|
|
293
693
|
|
|
694
|
+
{#if cancelled}
|
|
695
|
+
<p role="status">{i18n.t('import.stoppedHint')}</p>
|
|
696
|
+
{:else}
|
|
294
697
|
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-8e75e3db482b svadmin-u-1004c0c3954c svadmin-u-ca6bf63030aa">
|
|
295
698
|
<div class="svadmin-u-eb6e8b881acd svadmin-u-5f22e64f2282 svadmin-u-17a9f7af2265 svadmin-u-ca6bcd4b6f3f svadmin-u-95b7dea5a67f">
|
|
296
699
|
<span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-0214b4b355d1">{i18n.t('common.succeeded', { defaultValue: 'Succeeded' })}</span>
|
|
@@ -301,6 +704,11 @@
|
|
|
301
704
|
<span class="svadmin-u-d5c9b0001e7e svadmin-u-69450ef1487e svadmin-u-811148b13d1e svadmin-u-3032cae0badb">{failedRecords.length}</span>
|
|
302
705
|
</div>
|
|
303
706
|
</div>
|
|
707
|
+
{/if}
|
|
708
|
+
|
|
709
|
+
{#if importError}
|
|
710
|
+
<p role="alert">{importError}</p>
|
|
711
|
+
{/if}
|
|
304
712
|
|
|
305
713
|
{#if failedRecords.length > 0}
|
|
306
714
|
<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-eb6e8b881acd svadmin-u-5f22e64f2282 svadmin-u-7a0854fdbc30 svadmin-u-ca6bcd4b6f3f svadmin-u-e09a917c4c3b svadmin-u-359090c2d529">
|
|
@@ -324,6 +732,7 @@
|
|
|
324
732
|
{i18n.t('common.close', { defaultValue: 'Close' })}
|
|
325
733
|
</Button>
|
|
326
734
|
{:else}
|
|
735
|
+
<Button variant="outline" size="sm" onclick={cancelImport}>{i18n.t('common.cancel')}</Button>
|
|
327
736
|
<Button size="sm" disabled class="svadmin-u-77a2a20e90d4">
|
|
328
737
|
<Loader2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />
|
|
329
738
|
{i18n.t('common.processing', { defaultValue: 'Processing...' })}
|