From e3911a019860716dc3dca5163b1ab90f0eca822d Mon Sep 17 00:00:00 2001 From: Eve <92513238+1320209572@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:52:48 +0800 Subject: [PATCH] fix(import): pick existing tables from database scope --- .../components/import/TableImportDialog.vue | 108 +++++- .../TableImportDialog.existingTarget.spec.ts | 348 ++++++++++++++++++ apps/desktop/src/i18n/locales/en.ts | 3 + apps/desktop/src/i18n/locales/es.ts | 3 + apps/desktop/src/i18n/locales/it.ts | 3 + apps/desktop/src/i18n/locales/ja.ts | 3 + apps/desktop/src/i18n/locales/ko.ts | 3 + apps/desktop/src/i18n/locales/pt-BR.ts | 3 + apps/desktop/src/i18n/locales/zh-CN.ts | 3 + apps/desktop/src/i18n/locales/zh-TW.ts | 3 + 10 files changed, 464 insertions(+), 16 deletions(-) create mode 100644 apps/desktop/src/components/import/__tests__/TableImportDialog.existingTarget.spec.ts diff --git a/apps/desktop/src/components/import/TableImportDialog.vue b/apps/desktop/src/components/import/TableImportDialog.vue index 6c23b05405..bf561394d5 100644 --- a/apps/desktop/src/components/import/TableImportDialog.vue +++ b/apps/desktop/src/components/import/TableImportDialog.vue @@ -7,6 +7,7 @@ import { Dialog, DialogHeader, DialogTitle, DialogFooter, DialogScrollContent } import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { SearchableSelect } from "@/components/ui/searchable-select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { AlertTriangle, ArrowLeft, ArrowRight, Check, CheckCircle2, FileCode, FileJson, FileSpreadsheet, FileText, FileUp, Loader2, RefreshCw, Square, Upload, X } from "@lucide/vue"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -28,7 +29,7 @@ import { type TableImportWizardStep, } from "@/lib/table/tableImport"; import { getDataTypeOptions } from "@/lib/table/tableStructureEditorState"; -import { tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { metadataSchemaForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import type { ColumnInfo } from "@/types/database"; import * as api from "@/lib/backend/api"; @@ -64,6 +65,9 @@ interface BatchImportTask { const SKIP_VALUE = "__skip__"; const targetColumns = ref([]); +const loadedTargetTableName = ref(""); +const existingTableNames = ref([]); +const selectedExistingTable = ref(props.prefillTable || ""); const targetMode = ref(props.prefillTable ? "existing" : "create"); const newTableName = ref(""); const selectedSource = ref(null); @@ -75,6 +79,7 @@ const columnMapping = ref>({}); const columnDataTypes = ref>({}); const dynamicDataTypeOptions = ref([]); const loadingDataTypeOptions = ref(false); +const loadingExistingTables = ref(false); const loadingTarget = ref(false); const loadingPreview = ref(false); const importMode = ref("append"); @@ -101,6 +106,8 @@ let previewReloadTimer: ReturnType | null = null; let importElapsedTimer: ReturnType | null = null; let importStartedAt = 0; let dataTypeOptionsRequestId = 0; +let existingTablesRequestId = 0; +let targetColumnsRequestId = 0; let previewRequestId = 0; let batchEncodingRequestId = 0; @@ -131,9 +138,11 @@ const wizardSteps: Array<{ value: TableImportWizardStep; labelKey: string }> = [ const selectedConnection = computed(() => (props.prefillConnectionId ? store.getConfig(props.prefillConnectionId) : undefined)); const structureDatabaseType = computed(() => tableStructureDatabaseTypeForConnection(selectedConnection.value)); +const targetSchema = computed(() => metadataSchemaForConnection(selectedConnection.value, props.prefillDatabase || "", props.prefillSchema)); const dataTypeOptions = computed(() => mergeDataTypeOptions(dynamicDataTypeOptions.value, getDataTypeOptions(structureDatabaseType.value), Object.values(columnDataTypes.value))); -const hasExistingTarget = computed(() => !!props.prefillTable); -const targetTableName = computed(() => (targetMode.value === "create" ? newTableName.value.trim() : props.prefillTable || "")); +const hasExistingTarget = computed(() => !!props.prefillTable || loadingExistingTables.value || existingTableNames.value.length > 0); +const targetTableName = computed(() => (targetMode.value === "create" ? newTableName.value.trim() : selectedExistingTable.value)); +const existingTargetMetadataReady = computed(() => targetMode.value !== "existing" || (!loadingTarget.value && !!selectedExistingTable.value && loadedTargetTableName.value === selectedExistingTable.value)); const targetColumnNames = computed(() => targetColumns.value.map((column) => column.name)); const mappedColumns = computed(() => { const currentPreview = preview.value; @@ -159,7 +168,7 @@ const requiredUnmappedColumns = computed(() => ); const isBatchImport = computed(() => targetMode.value === "create" && batchTasks.value.length > 1); const canImport = computed(() => { - if (running.value || !props.prefillConnectionId) return false; + 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; @@ -171,8 +180,8 @@ 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 (wizardStep.value === "options") return !!preview.value && !!targetTableName.value; - if (wizardStep.value === "mapping") return mappingValidation.value.valid; + if (wizardStep.value === "options") return !!preview.value && !!targetTableName.value && existingTargetMetadataReady.value; + if (wizardStep.value === "mapping") return existingTargetMetadataReady.value && mappingValidation.value.valid; return false; }); const rawProgressPercent = computed(() => tableImportProgressPercent(progress.value)); @@ -236,11 +245,16 @@ function resetState() { liveElapsedMs.value = 0; previewRequestId++; batchEncodingRequestId++; + existingTablesRequestId++; + targetColumnsRequestId++; if (previewReloadTimer) { clearTimeout(previewReloadTimer); previewReloadTimer = null; } targetColumns.value = []; + loadedTargetTableName.value = ""; + existingTableNames.value = []; + selectedExistingTable.value = props.prefillTable || ""; targetMode.value = props.prefillTable ? "existing" : "create"; newTableName.value = ""; selectedSource.value = null; @@ -263,6 +277,8 @@ function resetState() { importMode.value = "append"; batchSize.value = 500; loadingPreview.value = false; + loadingExistingTables.value = false; + loadingTarget.value = false; running.value = false; cancelling.value = false; importId.value = ""; @@ -465,18 +481,58 @@ async function loadDataTypeOptions() { } } +async function loadExistingTables() { + if (props.prefillTable || loadingExistingTables.value || existingTableNames.value.length || !props.prefillConnectionId || !props.prefillDatabase) return; + const requestId = ++existingTablesRequestId; + loadingExistingTables.value = true; + errorMessage.value = ""; + try { + await store.ensureConnected(props.prefillConnectionId); + const tables = await api.listTables(props.prefillConnectionId, props.prefillDatabase, targetSchema.value, undefined, undefined, undefined, ["TABLE"]); + if (requestId !== existingTablesRequestId) return; + existingTableNames.value = tables.map((table) => table.name); + } catch (e: any) { + if (requestId === existingTablesRequestId) { + existingTableNames.value = []; + errorMessage.value = String(e?.message || e); + } + } finally { + if (requestId === existingTablesRequestId) loadingExistingTables.value = false; + } +} + async function loadTargetColumns() { - if (targetMode.value !== "existing" || !props.prefillConnectionId || !props.prefillDatabase || !props.prefillTable) return; + const tableName = selectedExistingTable.value; + if (targetMode.value !== "existing" || !props.prefillConnectionId || !props.prefillDatabase || !tableName) { + targetColumnsRequestId++; + targetColumns.value = []; + loadedTargetTableName.value = ""; + columnMapping.value = {}; + loadingTarget.value = false; + return; + } + const requestId = ++targetColumnsRequestId; + targetColumns.value = []; + loadedTargetTableName.value = ""; + columnMapping.value = {}; loadingTarget.value = true; errorMessage.value = ""; try { await store.ensureConnected(props.prefillConnectionId); - targetColumns.value = await api.getColumns(props.prefillConnectionId, props.prefillDatabase, props.prefillSchema || props.prefillDatabase, props.prefillTable); + const columns = await api.getColumns(props.prefillConnectionId, props.prefillDatabase, targetSchema.value, tableName); + if (requestId !== targetColumnsRequestId) return; + targetColumns.value = columns; + loadedTargetTableName.value = tableName; applyAutoMapping(); } catch (e: any) { - errorMessage.value = String(e?.message || e); + if (requestId === targetColumnsRequestId) { + targetColumns.value = []; + loadedTargetTableName.value = ""; + columnMapping.value = {}; + errorMessage.value = String(e?.message || e); + } } finally { - loadingTarget.value = false; + if (requestId === targetColumnsRequestId) loadingTarget.value = false; } } @@ -671,8 +727,8 @@ function canOpenStep(step: TableImportWizardStep) { if (running.value || step === "execution") return false; if (step === "source") return true; if (step === "options") return !!selectedSource.value; - if (step === "mapping") return !!preview.value; - if (step === "review") return !!preview.value && mappingValidation.value.valid; + if (step === "mapping") return !!preview.value && existingTargetMetadataReady.value; + if (step === "review") return !!preview.value && existingTargetMetadataReady.value && mappingValidation.value.valid; return false; } @@ -749,7 +805,7 @@ async function startImport() { importId: importId.value, connectionId: props.prefillConnectionId, database: props.prefillDatabase || "", - schema: props.prefillSchema || "", + schema: targetSchema.value, table: tableName, filePath: currentPreview.filePath, sourceRef: currentPreview.sourceRef || null, @@ -835,7 +891,7 @@ async function startBatchImport() { importId: importId.value, connectionId: props.prefillConnectionId, database: props.prefillDatabase || "", - schema: props.prefillSchema || "", + schema: targetSchema.value, table: task.tableName, filePath: task.preview.filePath, sourceRef: task.preview.sourceRef || null, @@ -992,6 +1048,7 @@ watch(textEncoding, schedulePreviewReloadAfterEncodingChange); watch([newTableName, columnMapping, columnDataTypes], saveActiveBatchTask, { deep: true }); watch(wizardStep, (step) => { if (step !== "mapping") closeDataTypePicker(); + if (step === "options") void loadExistingTables(); }); watch(targetMode, (mode) => { if (mode === "existing") { @@ -999,13 +1056,19 @@ watch(targetMode, (mode) => { dynamicDataTypeOptions.value = []; void loadTargetColumns(); } else { + targetColumnsRequestId++; targetColumns.value = []; + loadedTargetTableName.value = ""; + loadingTarget.value = false; importMode.value = "append"; applyAutoMapping(); applySuggestedColumnDataTypes(); void loadDataTypeOptions(); } }); +watch(selectedExistingTable, () => { + if (targetMode.value === "existing") void loadTargetColumns(); +}); watch(rawProgressPercent, (percent) => { if (progress.value?.status === "running") { @@ -1144,7 +1207,7 @@ watch(rawProgressPercent, (percent) => {