diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 9aa7b50a7..da82c5bc3 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -2151,12 +2151,16 @@ function editConnection() { } } -function disconnectConnection() { +async function disconnectConnection() { if (props.node.connectionId) { - connectionStore.disconnect(props.node.connectionId); - props.node.isExpanded = false; - props.node.children = []; - toast(t("connection.disconnected"), 2000); + try { + await connectionStore.disconnect(props.node.connectionId); + props.node.isExpanded = false; + props.node.children = []; + toast(t("connection.disconnected"), 2000); + } catch (e: any) { + toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000); + } } } @@ -2165,7 +2169,6 @@ 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/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index d7faddd33..b41891923 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -675,6 +675,8 @@ export const useConnectionStore = defineStore("connection", () => { async function disconnect(connectionId: string) { const shouldRemoveOneTimeConnection = getConfig(connectionId)?.one_time === true; await api.disconnectDb(connectionId); + const { useQueryStore } = await import("@/stores/queryStore"); + useQueryStore().closeConnectionTabs(connectionId); connectedIds.value.delete(connectionId); const node = findNode(treeNodes.value, connectionId); if (node) { @@ -693,6 +695,8 @@ export const useConnectionStore = defineStore("connection", () => { async function closeDatabaseConnection(connectionId: string, database: string) { await api.closeDatabaseConnection(connectionId, database); + const { useQueryStore } = await import("@/stores/queryStore"); + useQueryStore().closeDatabaseTabs(connectionId, database); const node = findDatabaseTreeNode(treeNodes.value, connectionId, database); if (node) { node.isExpanded = false; diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index e892432b4..34275501b 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -313,13 +313,8 @@ 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), - ); + function closeTabsWhere(predicate: (tab: QueryTab) => boolean) { + const closingIds = new Set(tabs.value.filter((tab) => predicate(tab)).map((tab) => tab.id)); if (closingIds.size === 0) return; tabs.value @@ -339,6 +334,14 @@ export const useQueryStore = defineStore("query", () => { } } + function closeConnectionTabs(connectionId: string) { + closeTabsWhere((tab) => tab.connectionId === connectionId); + } + + function closeDatabaseTabs(connectionId: string, database: string) { + closeTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database); + } + function updateSql(id: string, sql: string) { const tab = tabs.value.find((t) => t.id === id); if (tab) { @@ -1257,6 +1260,7 @@ export const useQueryStore = defineStore("query", () => { closeTab, closeOtherTabs, closeAllTabs, + closeConnectionTabs, closeDatabaseTabs, updateSql, renameTab, diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index abd68f839..fd624b8a5 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -345,11 +345,11 @@ test("closing database tabs removes browser tabs for that database only", async assert.deepEqual( store.tabs.map((tab) => tab.id), - [queryId, otherDbId, otherConnectionId], + [otherDbId, otherConnectionId], ); assert.equal(store.activeTabId, otherConnectionId); assert.equal( - store.tabs.some((tab) => [dataId, objectsId, structureId, mongoId].includes(tab.id)), + store.tabs.some((tab) => [dataId, objectsId, structureId, mongoId, queryId].includes(tab.id)), false, ); assert.equal(structureTab.result, undefined); @@ -360,6 +360,89 @@ test("closing database tabs removes browser tabs for that database only", async } }); +test("closing connection tabs removes every tab for that connection 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 queryId = store.createTab("conn-1", "db", "draft query", "query"); + const dataId = store.createTab("conn-1", "db", "users", "data", "public"); + const objectsId = store.openObjectBrowser("conn-1", "db", "public"); + const otherConnectionId = store.createTab("conn-2", "db", "users", "data", "public"); + const queryTab = store.tabs.find((item) => item.id === queryId); + + assert.ok(queryTab); + queryTab.result = { + columns: ["payload"], + rows: [["query"]], + affected_rows: 0, + execution_time_ms: 1, + session_id: "session-query", + }; + queryTab.resultSessionId = "session-query"; + store.activeTabId = queryId; + + store.closeConnectionTabs("conn-1"); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.deepEqual( + store.tabs.map((tab) => tab.id), + [otherConnectionId], + ); + assert.equal(store.activeTabId, otherConnectionId); + assert.equal( + store.tabs.some((tab) => [queryId, dataId, objectsId].includes(tab.id)), + false, + ); + assert.equal(queryTab.result, undefined); + assert.equal(queryTab.resultSessionId, undefined); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + +test("disconnecting a connection closes every tab for that connection", 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 connectionStore = useConnectionStore(); + const queryStore = useQueryStore(); + connectionStore.addEphemeralConnection(conn("conn-1")); + connectionStore.addEphemeralConnection(conn("conn-2")); + const queryId = queryStore.createTab("conn-1", "db", "draft query", "query"); + const dataId = queryStore.createTab("conn-1", "db", "users", "data", "public"); + const otherConnectionId = queryStore.createTab("conn-2", "db", "users", "data", "public"); + + queryStore.activeTabId = dataId; + await connectionStore.disconnect("conn-1"); + await new Promise((resolve) => setTimeout(resolve, 0)); + + assert.deepEqual( + queryStore.tabs.map((tab) => tab.id), + [otherConnectionId], + ); + assert.equal(queryStore.activeTabId, otherConnectionId); + assert.equal( + queryStore.tabs.some((tab) => [queryId, dataId].includes(tab.id)), + false, + ); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("starting a new query clears the previous result payload immediately", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia());