From 678033c82e45d79658eef261227c5f7318b05ca0 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 21 Jul 2026 18:43:46 +0800 Subject: [PATCH] fix(sidebar): preserve existing table data tabs --- .../sidebar/SidebarTreeRuntimeHost.vue | 22 +++---- apps/desktop/src/lib/sidebar/treeNodeClick.ts | 6 +- .../desktop/src/lib/tabs/dataTabActivation.ts | 12 ++-- packages/app-tests/dataTabActivation.test.ts | 59 ++++--------------- packages/app-tests/treeNodeClick.test.ts | 9 ++- 5 files changed, 33 insertions(+), 75 deletions(-) diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index eb9506248..be4c70b7d 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -89,7 +89,7 @@ import { import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick"; import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy"; import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts"; -import { canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation"; +import { dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation"; import { attachedDatabaseNameFromPath, buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, buildSqliteAttachDatabaseSql, supportsCreateDatabaseCharset, uniqueAttachedDatabaseName } from "@/lib/database/createDatabaseSql"; import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints"; import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection"; @@ -671,9 +671,6 @@ function runRowClickAction(clickDetail: number) { const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation); if (!shouldRunTreeNodeRowAction(action, clickDetail)) return; if (action === "open-data") { - if (node.type === "table") { - singleActivationDoubleClickRefreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(findExistingSameTableDataTab()); - } scheduleOpenData(node); } else if (isDocumentBrowserTreeNode(node.type)) { openMongoTreeData(node); @@ -684,8 +681,6 @@ function runRowClickAction(clickDetail: number) { } } -let singleActivationDoubleClickRefreshAllowed = false; - function refreshActiveKvBrowserAfterOpen(mode: "etcd" | "zookeeper", connectionId: string) { void nextTick(() => { window.dispatchEvent(new CustomEvent("dbx-refresh-active-kv-browser", { detail: { mode, connectionId } })); @@ -919,8 +914,8 @@ function onDoubleClick(event: MouseEvent) { if (!activeNode.value.isExpanded) void toggle(); } else if (action === "open-data") { openDataImmediately(activeNode.value); - } else if (action === "refresh-data") { - void refreshData(); + } else if (action === "activate-data") { + activateDataTableFromDoubleClick(); } else if (action === "open-source") { openObjectSourceDialog(false); } else if (action === "open-saved-sql") { @@ -932,24 +927,21 @@ function onDoubleClick(event: MouseEvent) { } } -async function refreshData() { +function activateDataTableFromDoubleClick() { const node = activeNode.value; if (node.type !== "table" || !hasNodeDatabaseContext(node)) return; - const singleActivationRefreshAllowed = singleActivationDoubleClickRefreshAllowed; - singleActivationDoubleClickRefreshAllowed = false; const activation = settingsStore.editorSettings.sidebarActivation; - if (activation === "single" && !singleActivationRefreshAllowed) return; const existingSameTableTab = findExistingSameTableDataTab(); - const action = dataTableDoubleClickAction(existingSameTableTab, activation, singleActivationRefreshAllowed); + const action = dataTableDoubleClickAction(existingSameTableTab, activation); if (action === "none") return; if (action === "open") { openDataImmediately(node); return; } if (!existingSameTableTab) return; + // Reopening an available table follows DBeaver's editor reuse model: only + // activate the existing tab so filters, result rows, and in-flight work stay intact. queryStore.switchTab(existingSameTableTab.id); - if (action === "activate") return; - await queryStore.refreshDataTab(existingSameTableTab.id); } function findExistingSameTableDataTab() { diff --git a/apps/desktop/src/lib/sidebar/treeNodeClick.ts b/apps/desktop/src/lib/sidebar/treeNodeClick.ts index 4ff5d46d0..c86b6f1fb 100644 --- a/apps/desktop/src/lib/sidebar/treeNodeClick.ts +++ b/apps/desktop/src/lib/sidebar/treeNodeClick.ts @@ -2,7 +2,7 @@ import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database" import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts"; export type TreeNodeRowAction = "open-data" | "toggle" | "none"; -export type TreeNodeRowDoubleClickAction = "open-data" | "refresh-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none"; +export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-saved-sql" | "toggle" | "none"; export type SidebarSelectionCopyAction = "copy-name" | "none"; export type SidebarActivation = "single" | "double"; @@ -48,7 +48,9 @@ export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetai } export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false): TreeNodeRowDoubleClickAction { - if (type === "table") return "refresh-data"; + // Single-click activation already handles the first click in a dblclick + // sequence. Only double-click activation needs a second-stage table action. + if (type === "table") return activation === "double" ? "activate-data" : "none"; if (activation === "double") { if (dataNodeTypes.has(type)) return "open-data"; if (sourceNodeTypes.has(type)) return "open-source"; diff --git a/apps/desktop/src/lib/tabs/dataTabActivation.ts b/apps/desktop/src/lib/tabs/dataTabActivation.ts index 551045909..e4268e2be 100644 --- a/apps/desktop/src/lib/tabs/dataTabActivation.ts +++ b/apps/desktop/src/lib/tabs/dataTabActivation.ts @@ -1,6 +1,6 @@ import type { QueryResult, QueryTab } from "@/types/database"; -export type DataTableDoubleClickAction = "activate" | "open" | "refresh" | "none"; +export type DataTableDoubleClickAction = "activate" | "open" | "none"; function isErrorResult(result: QueryResult | undefined): boolean { return result?.columns.length === 1 && result.columns[0] === "Error"; @@ -12,13 +12,9 @@ export function canActivateExistingDataTableTab(tab: QueryTab, options: { activa return !!tab.result || !!tab.results?.length; } -export function canRefreshDataTableFromSingleActivationDoubleClick(tab: QueryTab | undefined): boolean { - return !!tab && !tab.isExecuting && canActivateExistingDataTableTab(tab); -} - -export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double", singleActivationRefreshAllowed = false): DataTableDoubleClickAction { - if (activation === "single" && !singleActivationRefreshAllowed) return "none"; +export function dataTableDoubleClickAction(tab: QueryTab | undefined, activation: "single" | "double"): DataTableDoubleClickAction { + if (activation === "single") return "none"; if (!tab) return activation === "double" ? "open" : "none"; if (!canActivateExistingDataTableTab(tab)) return "open"; - return tab.isExecuting ? "activate" : "refresh"; + return "activate"; } diff --git a/packages/app-tests/dataTabActivation.test.ts b/packages/app-tests/dataTabActivation.test.ts index 6658d73ab..cae75fce2 100644 --- a/packages/app-tests/dataTabActivation.test.ts +++ b/packages/app-tests/dataTabActivation.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { canActivateExistingDataTableTab, canRefreshDataTableFromSingleActivationDoubleClick, dataTableDoubleClickAction } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts"; +import { canActivateExistingDataTableTab, dataTableDoubleClickAction } from "../../apps/desktop/src/lib/tabs/dataTabActivation.ts"; import type { QueryTab } from "../../apps/desktop/src/types/database.ts"; function dataTab(overrides: Partial = {}): QueryTab { @@ -62,25 +62,11 @@ test("reloads existing data table tabs showing an error result", () => { ); }); -test("single activation snapshots only successful idle tabs as refreshable", () => { - assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(undefined), false); - assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab()), false); - assert.equal(canRefreshDataTableFromSingleActivationDoubleClick(dataTab({ isExecuting: true })), false); +test("single activation leaves double click handling to the first click", () => { + assert.equal(dataTableDoubleClickAction(undefined, "single"), "none"); + assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "single"), "none"); assert.equal( - canRefreshDataTableFromSingleActivationDoubleClick( - dataTab({ - result: { - columns: ["Error"], - rows: [["connection failed"]], - affected_rows: 0, - execution_time_ms: 0, - }, - }), - ), - false, - ); - assert.equal( - canRefreshDataTableFromSingleActivationDoubleClick( + dataTableDoubleClickAction( dataTab({ result: { columns: ["id"], @@ -89,41 +75,17 @@ test("single activation snapshots only successful idle tabs as refreshable", () execution_time_ms: 1, }, }), + "single", ), - true, + "none", ); }); -test("single activation uses missing, restored, error, and busy first-click snapshots even if the tab succeeds before dblclick", () => { - const successfulAtDoubleClick = dataTab({ - result: { - columns: ["id"], - rows: [[1]], - affected_rows: 0, - execution_time_ms: 1, - }, - }); - const errorAtFirstClick = dataTab({ - result: { - columns: ["Error"], - rows: [["connection failed"]], - affected_rows: 0, - execution_time_ms: 0, - }, - }); - for (const initialTab of [undefined, dataTab(), errorAtFirstClick, dataTab({ isExecuting: true })]) { - const refreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(initialTab); - assert.equal(dataTableDoubleClickAction(successfulAtDoubleClick, "single", refreshAllowed), "none"); - } - const refreshAllowed = canRefreshDataTableFromSingleActivationDoubleClick(successfulAtDoubleClick); - assert.equal(dataTableDoubleClickAction(successfulAtDoubleClick, "single", refreshAllowed), "refresh"); -}); - test("double activation opens a missing table without a first-click snapshot", () => { assert.equal(dataTableDoubleClickAction(undefined, "double"), "open"); }); -test("double activation decisions preserve loading, refresh, and recovery behavior", () => { +test("double activation reuses loading and successful tabs without refreshing", () => { assert.equal(dataTableDoubleClickAction(dataTab({ isExecuting: true }), "double"), "activate"); assert.equal( dataTableDoubleClickAction( @@ -137,8 +99,11 @@ test("double activation decisions preserve loading, refresh, and recovery behavi }), "double", ), - "refresh", + "activate", ); +}); + +test("double activation preserves restored and error recovery behavior", () => { assert.equal(dataTableDoubleClickAction(dataTab(), "double"), "open"); assert.equal( dataTableDoubleClickAction( diff --git a/packages/app-tests/treeNodeClick.test.ts b/packages/app-tests/treeNodeClick.test.ts index be471d233..a8adb9fd6 100644 --- a/packages/app-tests/treeNodeClick.test.ts +++ b/packages/app-tests/treeNodeClick.test.ts @@ -14,9 +14,12 @@ test("double click navigation mode selects rows on single click", () => { assert.equal(treeNodeRowAction("saved-sql-file", false, "double"), "none"); }); -test("table rows refresh on double click in both navigation modes", () => { - assert.equal(treeNodeRowDoubleClickAction("table", true, "single"), "refresh-data"); - assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "refresh-data"); +test("table double click avoids a second action in single activation mode", () => { + assert.equal(treeNodeRowDoubleClickAction("table", true, "single"), "none"); +}); + +test("table double click activates data in double activation mode", () => { + assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "activate-data"); }); test("double click navigation mode opens other actionable rows on double click", () => {