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, + }; }); }