From f145ea5d7bed37c07bbc8c7649a45c6f623ee0dc Mon Sep 17 00:00:00 2001 From: Mr0Bread <55754540+Mr0Bread@users.noreply.github.com> Date: Fri, 13 Nov 2026 03:22:27 +0200 Subject: [PATCH] fix(database): select the neighbouring result when closing the active tab (#3017) Closing the active database result selected the rightmost remaining result, jumping away from where the user was. It now selects the left neighbour, then the right one, and Output when none remain. Closing an inactive or missing result keeps the current selection. Co-authored-by: Mr0Bread <55754540+Mr0Bread@users.noreply.github.com> --- docs/en/guide/database.mdx | 2 + docs/zh/guide/database.mdx | 2 + src/features/database/database.test.ts | 61 +++++++++++++++++++++++++- src/features/database/store.ts | 7 ++- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/en/guide/database.mdx b/docs/en/guide/database.mdx index bd3c423be..1aa3d9fae 100644 --- a/docs/en/guide/database.mdx +++ b/docs/en/guide/database.mdx @@ -120,6 +120,8 @@ SQL completion knows the connected schema: tables after `FROM`/`JOIN` (with join Results appear in the **Services** tool window under the **Database** tab: an **Output** tab logs every statement, and each result set gets a **Result N** tab (up to 12). +Closing a selected result activates its left neighbor, or its right neighbor if there is no result on the left. When no results remain, **Output** is selected. Closing an inactive result keeps the current selection. + - **Table** or **Chart** view (bar or line, with X/Y columns and an aggregation). - Paging with page sizes 10 to 1,000; click the row count to count all rows. - **Search in results** and a per-column local filter on the loaded rows. diff --git a/docs/zh/guide/database.mdx b/docs/zh/guide/database.mdx index bf10c9d52..a744cfbdc 100644 --- a/docs/zh/guide/database.mdx +++ b/docs/zh/guide/database.mdx @@ -120,6 +120,8 @@ SQL 补全基于已连接的架构:`FROM`/`JOIN` 后补全表(并根据外 结果显示在 **服务** 工具窗口的 **数据库** 标签页中:**输出** 标签页记录每条语句,每个结果集一个 **Result N** 标签页(最多 12 个)。 +关闭当前选中的结果标签页时,优先选中左侧相邻的结果;没有左侧结果时,选中右侧相邻结果。没有剩余结果时,选中 **输出**。关闭未选中的结果不会改变当前选择。 + - **表** 或 **图表** 视图(柱状图或折线图,可选 X/Y 列和聚合方式)。 - 分页,每页 10 到 1,000 行;点击行数可以统计总行数。 - **在结果中搜索**,以及按列对已加载行进行本地筛选。 diff --git a/src/features/database/database.test.ts b/src/features/database/database.test.ts index 1ffa4d8b8..b41493d9d 100644 --- a/src/features/database/database.test.ts +++ b/src/features/database/database.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { DataSource } from "@/platform/ipc"; import { exportData, type ExportData } from "./export"; import { isNumericColumn } from "./Grid"; @@ -22,7 +22,7 @@ import { toCsv, toTsv, } from "./model"; -import { listsDatabases } from "./store"; +import { OUTPUT_TAB, closeResult, listsDatabases, useDatabase, type ResultTab } from "./store"; import { VENDORS, sourceUrl, vendorById, vendorOf, vendorPreset } from "./vendors"; describe("clipboard formats", () => { @@ -443,3 +443,60 @@ describe("Options tab", () => { expect(colorVar(null)).toBeUndefined(); }); }); + +describe("closing result tabs", () => { + let previous: ReturnType; + const tabs = [11, 35, 72, 109].map((id): ResultTab => { + const text = `SELECT ${id}`; + return { + id, + title: `Result ${id}`, + path: "console.sql", + sourceId: "pg", + outcome: { + statement: { from: 0, to: text.length, text }, + error: null, + result: { + statement: text, + columns: [{ name: "value", typeName: "int4" }], + rows: [[String(id)]], + affected: null, + hasMore: false, + elapsedMs: 1, + executionMs: 1, + }, + }, + }; + }); + + beforeEach(() => { + previous = useDatabase.getState(); + }); + + afterEach(() => useDatabase.setState(previous, true)); + + it("selects the immediate left neighbor rather than the rightmost result", () => { + useDatabase.setState({ results: tabs, activeResult: 35 }); + closeResult(35); + expect(useDatabase.getState().activeResult).toBe(11); + expect(useDatabase.getState().results.map((r) => r.id)).toEqual([11, 72, 109]); + }); + + it.each([ + ["selects the right neighbor of the first result", [11, 35, 72, 109], 11, 11, 35, [35, 72, 109]], + ["selects the left neighbor of the last result", [11, 35, 72, 109], 109, 109, 72, [11, 35, 72]], + ["returns to Output after the only result closes", [11], 11, 11, OUTPUT_TAB, []], + ["keeps selection when an inactive result on the left closes", [11, 35, 72, 109], 72, 35, 72, [11, 72, 109]], + ["keeps selection when an inactive result on the right closes", [11, 35, 72, 109], 11, 109, 11, [11, 35, 72]], + ["keeps Output selected when a result closes", [11, 35, 72, 109], OUTPUT_TAB, 35, OUTPUT_TAB, [11, 72, 109]], + ["keeps an unset selection when a result closes", [11, 35, 72, 109], null, 11, null, [35, 72, 109]], + ["ignores an already closed result", [11, 35, 72, 109], 35, 999, 35, [11, 35, 72, 109]], + ["does not change a stale selection for a missing result", [11, 35, 72, 109], 999, 999, 999, [11, 35, 72, 109]], + ["does not close Output", [11, 35, 72, 109], OUTPUT_TAB, OUTPUT_TAB, OUTPUT_TAB, [11, 35, 72, 109]], + ] as const)("%s", (_name, ids, active, closed, expected, remaining) => { + useDatabase.setState({ results: ids.map((id) => tabs.find((r) => r.id === id)!), activeResult: active }); + closeResult(closed); + expect(useDatabase.getState().activeResult).toBe(expected); + expect(useDatabase.getState().results.map((r) => r.id)).toEqual(remaining); + }); +}); diff --git a/src/features/database/store.ts b/src/features/database/store.ts index 0d681e145..2e88437b4 100644 --- a/src/features/database/store.ts +++ b/src/features/database/store.ts @@ -1230,7 +1230,12 @@ export function cancelConsole(path: string) { export function closeResult(id: number) { useDatabase.setState((s) => { + const index = s.results.findIndex((r) => r.id === id); + if (index < 0) return s; const results = s.results.filter((r) => r.id !== id); - return { results, activeResult: s.activeResult === id ? (results.at(-1)?.id ?? OUTPUT_TAB) : s.activeResult }; + return { + results, + activeResult: s.activeResult === id ? ((results[index - 1] ?? results[index])?.id ?? OUTPUT_TAB) : s.activeResult, + }; }); }