From 0edd6554d561b45f0d626c1afd022a3a4ee25210 Mon Sep 17 00:00:00 2001 From: cowking <3200017240@qq.com> Date: Tue, 22 Sep 2026 01:21:46 +0800 Subject: [PATCH] feat(import): select worksheets for batch table imports --- .../components/import/TableImportDialog.vue | 83 ++-- .../TableImportDialog.batchSelection.spec.ts | 427 ++++++++++++++++++ apps/desktop/src/i18n/locales/en.ts | 3 + apps/desktop/src/i18n/locales/zh-CN.ts | 3 + 4 files changed, 484 insertions(+), 32 deletions(-) create mode 100644 apps/desktop/src/components/import/__tests__/TableImportDialog.batchSelection.spec.ts diff --git a/apps/desktop/src/components/import/TableImportDialog.vue b/apps/desktop/src/components/import/TableImportDialog.vue index 7c71bc7396..1955634835 100644 --- a/apps/desktop/src/components/import/TableImportDialog.vue +++ b/apps/desktop/src/components/import/TableImportDialog.vue @@ -53,6 +53,7 @@ type ImportSource = string | File; interface BatchImportTask { id: string; + selected: boolean; source: ImportSource; format: api.TableImportSourceFormat; sheetName: string; @@ -182,12 +183,16 @@ const requiredUnmappedColumns = computed(() => ), ); const isBatchImport = computed(() => targetMode.value === "create" && batchTasks.value.length > 1); +const selectedBatchTasks = computed(() => batchTasks.value.filter((task) => task.selected)); +const batchTargetNamesValid = computed(() => { + const tableNames = selectedBatchTasks.value.map((task) => task.tableName.trim().toLowerCase()); + return tableNames.length > 0 && tableNames.every(Boolean) && new Set(tableNames).size === tableNames.length; +}); const canImport = computed(() => { if (running.value || !props.prefillConnectionId || !existingTargetMetadataReady.value) return false; if (!isBatchImport.value) return !!preview.value && !!targetTableName.value && mappingValidation.value.valid; - const tableNames = batchTasks.value.map((task) => task.tableName.trim().toLowerCase()); - if (new Set(tableNames).size !== tableNames.length) return false; - return batchTasks.value.every((task) => { + if (!batchTargetNamesValid.value) return false; + return selectedBatchTasks.value.every((task) => { const mappings = task.preview.columns.map((sourceColumn) => ({ sourceColumn, targetColumn: task.columnMapping[sourceColumn] ?? "", targetDataType: task.columnDataTypes[sourceColumn] ?? "" })).filter((mapping) => mapping.targetColumn); return !!task.tableName.trim() && validateImportMappings(mappings).valid; }); @@ -195,6 +200,10 @@ const canImport = computed(() => { const canGoBack = computed(() => wizardStep.value !== "source" && wizardStep.value !== "execution" && !running.value); const canGoNext = computed(() => { if (wizardStep.value === "source") return !!selectedSource.value && !!sourceFormat.value; + if (isBatchImport.value) { + if (wizardStep.value === "options") return batchTargetNamesValid.value; + if (wizardStep.value === "mapping") return canImport.value; + } if (wizardStep.value === "options") return !!preview.value && !!targetTableName.value && existingTargetMetadataReady.value; if (wizardStep.value === "mapping") return existingTargetMetadataReady.value && mappingValidation.value.valid; return false; @@ -657,6 +666,7 @@ async function prepareBatchSources(sources: ImportSource[]) { const tableBase = sheetName ? `${suggestedTableName(sourceName(source))}_${sheetName}` : sourceName(source); tasks.push({ id: uuid(), + selected: true, source, format, sheetName: effectiveSheetName, @@ -742,6 +752,10 @@ function canOpenStep(step: TableImportWizardStep) { if (running.value || step === "execution") return false; if (step === "source") return true; if (step === "options") return !!selectedSource.value; + if (isBatchImport.value) { + if (step === "mapping") return selectedBatchTasks.value.length > 0; + if (step === "review") return canImport.value; + } if (step === "mapping") return !!preview.value && existingTargetMetadataReady.value; if (step === "review") return !!preview.value && existingTargetMetadataReady.value && mappingValidation.value.valid; return false; @@ -785,6 +799,7 @@ async function goNext() { async function startImport() { saveActiveBatchTask(); + if (!canImport.value) return; if (!(await ensureReadOnlyWriteAccess({ connection: store.getConfig(props.prefillConnectionId ?? ""), source: t("readOnlyUnlock.sourceImport"), treatAsMutation: true }))) { return; } @@ -866,15 +881,18 @@ async function startImport() { } async function startBatchImport() { - if (!props.prefillConnectionId || !batchTasks.value.length || running.value) return; + if (!props.prefillConnectionId || !canImport.value) return; + // Keep original indices for the active preview and error state, even when + // the import queue excludes worksheets between selected tasks. + const tasks = batchTasks.value.map((task, index) => ({ task, index })).filter(({ task }) => task.selected); running.value = true; progressPercentFloor.value = 0; cancelling.value = false; errorMessage.value = ""; wizardStep.value = "execution"; - const totalRowsExact = batchTasks.value.every((task) => task.preview.totalRowsExact !== false); - const totalRows = totalRowsExact ? batchTasks.value.reduce((sum, task) => sum + task.preview.totalRows, 0) : 0; - const totalBytes = batchTasks.value.reduce((sum, task) => sum + task.preview.sizeBytes, 0); + const totalRowsExact = tasks.every(({ task }) => task.preview.totalRowsExact !== false); + const totalRows = totalRowsExact ? tasks.reduce((sum, { task }) => sum + task.preview.totalRows, 0) : 0; + const totalBytes = tasks.reduce((sum, { task }) => sum + task.preview.sizeBytes, 0); let completedRows = 0; let completedBytes = 0; importId.value = uuid(); @@ -882,10 +900,9 @@ async function startBatchImport() { progress.value = { importId: importId.value, status: "running", phase: "preparing", rowsImported: 0, totalRows, totalRowsExact, bytesRead: 0, totalBytes, elapsedMs: 0 }; try { - const tableNames = batchTasks.value.map((task) => task.tableName.trim().toLowerCase()); + const tableNames = tasks.map(({ task }) => task.tableName.trim().toLowerCase()); if (new Set(tableNames).size !== tableNames.length) throw new Error("Target table names must be unique"); - for (let index = 0; index < batchTasks.value.length; index++) { - const task = batchTasks.value[index]; + for (const [queueIndex, { task, index }] of tasks.entries()) { activeTaskIndex.value = index; task.status = "running"; importId.value = uuid(); @@ -922,7 +939,7 @@ async function startBatchImport() { }, (nextProgress) => { task.rowsImported = nextProgress.rowsImported; - const hasRemainingTasks = index < batchTasks.value.length - 1; + const hasRemainingTasks = queueIndex < tasks.length - 1; const aggregateStatus = nextProgress.status === "done" && hasRemainingTasks ? "running" : nextProgress.status; const aggregatePhase = nextProgress.status === "done" && hasRemainingTasks ? "preparing" : nextProgress.phase; progress.value = { @@ -1122,30 +1139,31 @@ watch(rawProgressPercent, (percent) => {
- {{ batchTasks.length }} {{ t("tableImport.file") }} + {{ t("tableImport.selectedTasks", { selected: selectedBatchTasks.length, total: batchTasks.length }) }} {{ selectedSourceName }}
- +
+ + +
+

{{ t("tableImport.noTasksSelected") }}