feat(sidebar): show connection group paths

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-14 19:03:27 +08:00
1 parent 69dd363417
commit c2b8a1795a
14 files changed
+251 -10

No files matched your search

@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout";
import type { SidebarLayout } from "@/types/database";
const layout: SidebarLayout = {
groups: [
{ id: "project", name: "Project", collapsed: false },
{ id: "staging", name: "Staging", collapsed: false },
],
order: [
{
type: "group",
id: "project",
children: [
{
type: "group",
id: "staging",
children: [{ type: "connection", id: "nested" }],
},
{ type: "connection", id: "grouped" },
],
},
{ type: "connection", id: "root" },
],
};
describe("findConnectionGroupPath", () => {
it("returns every containing group from root to leaf", () => {
expect(findConnectionGroupPath(layout, "nested")).toEqual(["Project", "Staging"]);
expect(findConnectionGroupPath(layout, "grouped")).toEqual(["Project"]);
});
it("distinguishes a top-level connection from a missing connection", () => {
expect(findConnectionGroupPath(layout, "root")).toEqual([]);
expect(findConnectionGroupPath(layout, "missing")).toBeNull();
});
});
+63 -3
View File
@@ -1,6 +1,28 @@
import { describe, expect, it } from "vitest";
import { middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultSourceRange, tabularResultItems } from "@/lib/tabs/tabPresentation";
import type { QueryTab } from "@/types/database";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { useConnectionStore } from "@/stores/connectionStore";
import { connectionGroupDisplayName, middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultSourceRange, tabTooltipLines, tabularResultItems } from "@/lib/tabs/tabPresentation";
import type { ConnectionConfig, QueryTab } from "@/types/database";
const translations: Record<string, string> = {
"tabs.tooltipConnection": "Connection:",
"tabs.tooltipGroup": "Group:",
"tabs.tooltipDatabase": "Database:",
"connectionGroup.ungroupedLabel": "Ungrouped",
"editor.noDatabase": "No database",
};
const translate = (key: string) => translations[key] ?? key;
beforeEach(() => {
const values = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => values.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => values.set(key, value)),
removeItem: vi.fn((key: string) => values.delete(key)),
});
setActivePinia(createPinia());
});
function queryTab(overrides: Partial<QueryTab>): QueryTab {
return {
@@ -110,6 +132,44 @@ describe("query result labels", () => {
});
});
describe("tab group presentation", () => {
it("adds the full, live group path to tab tooltips", () => {
const store = useConnectionStore();
store.connections = [{ id: "conn-1", name: "PostgreSQL", db_type: "postgres", database: "app" } as ConnectionConfig];
store.sidebarLayout = {
groups: [
{ id: "project", name: "Project", collapsed: false },
{ id: "staging", name: "Staging", collapsed: false },
],
order: [
{
type: "group",
id: "project",
children: [{ type: "group", id: "staging", children: [{ type: "connection", id: "conn-1" }] }],
},
],
};
expect(connectionGroupDisplayName("conn-1", translate)).toBe("Project / Staging");
expect(tabTooltipLines(queryTab({ database: "app" }), translate)).toEqual([
{ label: "Connection:", value: "PostgreSQL" },
{ label: "Group:", value: "Project / Staging" },
{ label: "Database:", value: "app" },
]);
});
it("labels a top-level connection as ungrouped", () => {
const store = useConnectionStore();
store.connections = [{ id: "conn-1", name: "PostgreSQL", db_type: "postgres", database: "app" } as ConnectionConfig];
store.sidebarLayout = {
groups: [],
order: [{ type: "connection", id: "conn-1" }],
};
expect(connectionGroupDisplayName("conn-1", translate)).toBe("Ungrouped");
});
});
describe("query result source ranges", () => {
it("prefers the preserved editor range for a selected duplicate statement", () => {
const sql = "SELECT * FROM users;\nSELECT * FROM users;";
@@ -135,6 +135,53 @@ export function findConnectionLocation(layout: SidebarLayout, connectionId: stri
return visit(layout.order);
}
/**
* Returns the display-name path for a connection's containing groups.
* A top-level connection returns an empty path; an absent connection returns null.
*/
export function findConnectionGroupPath(layout: SidebarLayout, connectionId: string): string[] | null {
const groupMap = new Map(layout.groups.map((group) => [group.id, group]));
const visit = (entries: SidebarOrderEntry[], path: string[]): string[] | null => {
for (const entry of entries) {
if (entry.type === "connection") {
if (entry.id === connectionId) return path;
continue;
}
const group = groupMap.get(entry.id);
if (!group) continue;
const found = visit(entryChildren(entry), [...path, group.name]);
if (found) return found;
}
return null;
};
return visit(layout.order, []);
}
/** Build all connection group paths in one traversal for list rendering. */
export function buildConnectionGroupPathMap(layout: SidebarLayout): Map<string, string[]> {
const groupMap = new Map(layout.groups.map((group) => [group.id, group]));
const paths = new Map<string, string[]>();
const visit = (entries: SidebarOrderEntry[], path: string[]) => {
for (const entry of entries) {
if (entry.type === "connection") {
paths.set(entry.id, path);
continue;
}
const group = groupMap.get(entry.id);
if (!group) continue;
visit(entryChildren(entry), [...path, group.name]);
}
};
visit(layout.order, []);
return paths;
}
function findGroupEntry(entries: SidebarOrderEntry[], groupId: string): Extract<SidebarOrderEntry, { type: "group" }> | null {
for (const entry of entries) {
if (entry.type !== "group") continue;
+10 -4
View File
@@ -1,5 +1,6 @@
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout";
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
import { executableStatementRanges, splitSqlStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import type { ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database";
@@ -12,6 +13,13 @@ export function connectionDisplayName(connectionId: string): string {
return connectionStore.getConfig(connectionId)?.name || connectionId;
}
export function connectionGroupDisplayName(connectionId: string, t: Translate): string | undefined {
const connectionStore = useConnectionStore();
const path = findConnectionGroupPath(connectionStore.sidebarLayout, connectionId);
if (path === null) return undefined;
return path.join(" / ") || t("connectionGroup.ungroupedLabel");
}
export function connectionColor(connectionId: string): string {
const connectionStore = useConnectionStore();
return connectionStore.getConfig(connectionId)?.color || "";
@@ -111,11 +119,9 @@ export function tabDisplayTitle(tab: QueryTab, t: Translate): string {
export function tabTooltipLines(tab: QueryTab, t: Translate): { label: string; value: string }[] {
const connName = connectionDisplayName(tab.connectionId);
const groupName = connectionGroupDisplayName(tab.connectionId, t);
const database = databaseDisplayNameForTab(tab.connectionId, tab.database, t);
const lines: { label: string; value: string }[] = [
{ label: t("tabs.tooltipConnection"), value: connName },
{ label: t("tabs.tooltipDatabase"), value: database },
];
const lines: { label: string; value: string }[] = [{ label: t("tabs.tooltipConnection"), value: connName }, ...(groupName ? [{ label: t("tabs.tooltipGroup"), value: groupName }] : []), { label: t("tabs.tooltipDatabase"), value: database }];
if (tab.mode === "query" && queryTitle(tab)) {
lines.unshift({ label: t("tabs.tooltipTitle"), value: tab.title });
}