diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 67364e60d1..810016bdbe 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1478,6 +1478,7 @@ async function closeDatabaseConnection() { if (node.type !== "database" || !node.connectionId || node.database == null) return; try { await connectionStore.closeDatabaseConnection(node.connectionId, node.database); + queryStore.closeDatabaseTabs(node.connectionId, node.database); toast(t("connection.databaseConnectionClosed", { name: node.label }), 2000); } catch (e: any) { toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 4bf6fabd39..c71bc16177 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -303,6 +303,32 @@ export const useQueryStore = defineStore("query", () => { activeTabId.value = next.activeTabId; } + function closeDatabaseTabs(connectionId: string, database: string) { + const closingModes = new Set(["data", "objects", "structure", "mongo"]); + const closingIds = new Set( + tabs.value + .filter((tab) => tab.connectionId === connectionId && tab.database === database && closingModes.has(tab.mode)) + .map((tab) => tab.id), + ); + if (closingIds.size === 0) return; + + tabs.value + .filter((tab) => closingIds.has(tab.id)) + .forEach((tab) => { + if (tab.isExecuting) void cancelTabExecution(tab.id); + if (tab.isExplaining) void cancelTabExplain(tab.id); + void closeResultSession(tab); + void closeClientConnectionSession(tab); + clearResultPayload(tab); + }); + + const activeClosingIndex = tabs.value.findIndex((tab) => tab.id === activeTabId.value && closingIds.has(tab.id)); + tabs.value = tabs.value.filter((tab) => !closingIds.has(tab.id)); + if (activeClosingIndex >= 0) { + activeTabId.value = tabs.value[Math.min(activeClosingIndex, tabs.value.length - 1)]?.id ?? null; + } + } + function updateSql(id: string, sql: string) { const tab = tabs.value.find((t) => t.id === id); if (tab) { @@ -1221,6 +1247,7 @@ export const useQueryStore = defineStore("query", () => { closeTab, closeOtherTabs, closeAllTabs, + closeDatabaseTabs, updateSql, renameTab, openObjectBrowser, diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 45903a86d6..d9549b20a6 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -310,6 +310,56 @@ test("closing tabs clears removed result payloads before dropping tab references } }); +test("closing database tabs removes browser tabs for that database only", async () => { + const restoreStorage = installMemoryStorage(); + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => { + return new Response(JSON.stringify(true), { status: 200, headers: { "Content-Type": "application/json" } }); + }) as typeof fetch; + + try { + setActivePinia(createPinia()); + const store = useQueryStore(); + const dataId = store.createTab("conn-1", "db", "users", "data", "public"); + const objectsId = store.openObjectBrowser("conn-1", "db", "public"); + const structureId = store.openTableStructure("conn-1", "db", "public", "users"); + const mongoId = store.createTab("conn-1", "db", "orders", "mongo"); + const queryId = store.createTab("conn-1", "db", "draft query", "query"); + const otherDbId = store.createTab("conn-1", "analytics", "users", "data", "public"); + const otherConnectionId = store.createTab("conn-2", "db", "users", "data", "public"); + const structureTab = store.tabs.find((item) => item.id === structureId); + + assert.ok(structureTab); + structureTab.result = { + columns: ["payload"], + rows: [["structure"]], + affected_rows: 0, + execution_time_ms: 1, + session_id: "session-structure", + }; + structureTab.resultSessionId = "session-structure"; + store.activeTabId = structureId; + + store.closeDatabaseTabs("conn-1", "db"); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.deepEqual( + store.tabs.map((tab) => tab.id), + [queryId, otherDbId, otherConnectionId], + ); + assert.equal(store.activeTabId, otherConnectionId); + assert.equal( + store.tabs.some((tab) => [dataId, objectsId, structureId, mongoId].includes(tab.id)), + false, + ); + assert.equal(structureTab.result, undefined); + assert.equal(structureTab.resultSessionId, undefined); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("starting a new query clears the previous result payload immediately", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia());