feat(sidebar): show connection group paths
This commit is contained in:
parent
69dd363417
commit
c2b8a1795a
|
|
@ -14,6 +14,7 @@ import { useSchemaOptions } from "@/composables/useSchemaOptions";
|
|||
import { connectionIconType } from "@/lib/connection/connectionPresentation";
|
||||
import { formatDatabaseLabel, isDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { connectionDisplayName } from "@/lib/tabs/tabPresentation";
|
||||
import { buildConnectionGroupPathMap } from "@/lib/sidebar/sidebarLayout";
|
||||
import { isSingleDatabase, supportsSqlInListPaste, supportsTransaction as supportsTransactionFeature } from "@/lib/database/databaseCapabilities";
|
||||
import { hexToRgba } from "@/lib/common/color";
|
||||
import { productionContextForDatabase } from "@/lib/database/productionSafety";
|
||||
|
|
@ -66,6 +67,7 @@ const activeDatabaseOptions = computed(() => {
|
|||
});
|
||||
|
||||
const connectionOptionIds = computed(() => connectionStore.connections.map((connection) => connection.id));
|
||||
const connectionGroupPaths = computed(() => buildConnectionGroupPathMap(connectionStore.sidebarLayout));
|
||||
const activeDatabaseValue = computed(() => props.activeTab.database || "");
|
||||
const activeProductionContext = computed(() => productionContextForDatabase(props.activeConnection, props.activeTab.database));
|
||||
const showConnectionProductionBadge = computed(() => activeProductionContext.value.reason === "connection");
|
||||
|
|
@ -154,6 +156,10 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
|
|||
return connectionStore.getConfig(connectionId);
|
||||
}
|
||||
|
||||
function connectionGroupLabel(connectionId: string): string {
|
||||
return connectionGroupPaths.value.get(connectionId)?.join(" / ") || t("connectionGroup.ungroupedLabel");
|
||||
}
|
||||
|
||||
function databaseOptionIsProduction(database: string): boolean {
|
||||
if (!database || props.activeConnection?.is_production) return false;
|
||||
return productionContextForDatabase(props.activeConnection, database).reason === "database";
|
||||
|
|
@ -326,6 +332,8 @@ function databaseOptionIsProduction(database: string): boolean {
|
|||
:loading-text="t('common.loading')"
|
||||
trigger-class="font-medium text-foreground"
|
||||
:display-name="connectionDisplayName"
|
||||
list-class="w-96 max-w-[calc(100vw-2rem)]"
|
||||
item-class="h-9"
|
||||
@update:model-value="(connectionId) => emit('changeConnection', connectionId)"
|
||||
>
|
||||
<template #trigger-label="{ label }">
|
||||
|
|
@ -339,7 +347,10 @@ function databaseOptionIsProduction(database: string): boolean {
|
|||
<template #option-label="{ option, label }">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<DatabaseIcon :db-type="connectionIconType(connectionById(option))" class="h-3.5 w-3.5 shrink-0" />
|
||||
<TruncatedTextTooltip :text="label" class="min-w-0 flex-1" side="left" :side-offset="8" />
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
<TruncatedTextTooltip :text="connectionGroupLabel(option)" class="block min-w-0 max-w-48 shrink-0 rounded-sm bg-muted/70 px-1.5 py-0.5 text-[11px] text-muted-foreground" side="left" :side-offset="8" />
|
||||
<TruncatedTextTooltip :text="label" class="block min-w-[7rem] flex-1 text-sm font-medium" side="left" :side-offset="8" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const props = withDefaults(
|
|||
triggerClass?: HTMLAttributes["class"];
|
||||
triggerIconClass?: HTMLAttributes["class"];
|
||||
contentClass?: HTMLAttributes["class"];
|
||||
listClass?: HTMLAttributes["class"];
|
||||
itemClass?: HTMLAttributes["class"];
|
||||
displayName?: (option: string) => string;
|
||||
optionTooltip?: (option: string) => string | undefined;
|
||||
|
|
@ -236,13 +237,13 @@ function handleKeydown(event: KeyboardEvent) {
|
|||
</PopoverTrigger>
|
||||
<PopoverContent :align="SEARCHABLE_SELECT_HELP_PANEL_ALIGN" :class="cn('w-auto max-w-[calc(100vw-1rem)] border-0 bg-transparent p-0 shadow-none ring-0', contentClass)">
|
||||
<div class="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-start">
|
||||
<div ref="listCard" class="w-52 shrink-0 rounded-md border bg-popover p-1.5 shadow-md">
|
||||
<div ref="listCard" :class="cn('w-52 shrink-0 rounded-md border bg-popover p-1.5 shadow-md', listClass)">
|
||||
<div class="relative rounded-sm border bg-background">
|
||||
<Search class="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" />
|
||||
<span v-if="!searchText" class="pointer-events-none absolute left-[25px] top-1/2 -translate-y-1/2 text-sm text-muted-foreground">{{ searchPlaceholder }}</span>
|
||||
<Input ref="searchInput" :model-value="searchText" class="h-6 border-0 pl-6 pr-2 text-sm caret-foreground shadow-none focus-visible:ring-0" @update:model-value="(value) => (searchText = String(value))" @keydown="handleKeydown" />
|
||||
</div>
|
||||
<div ref="listContainer" class="max-h-64 overflow-y-auto py-1" @scroll="updateHelpPanelOffset">
|
||||
<div ref="listContainer" class="dbx-searchable-select-list max-h-64 overflow-y-auto py-1" @scroll="updateHelpPanelOffset">
|
||||
<div v-if="loading" class="px-2 py-2 text-sm text-muted-foreground">
|
||||
{{ loadingText }}
|
||||
</div>
|
||||
|
|
@ -348,4 +349,43 @@ function handleKeydown(event: KeyboardEvent) {
|
|||
border-color: rgb(147, 197, 253) !important;
|
||||
box-shadow: 0 0 0 2px rgba(147, 197, 253, 0.24) !important;
|
||||
}
|
||||
|
||||
.dbx-searchable-select-list {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in oklch, var(--foreground) 30%, transparent) transparent;
|
||||
}
|
||||
|
||||
.dbx-searchable-select-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.dbx-searchable-select-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dbx-searchable-select-list::-webkit-scrollbar-thumb {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--foreground) 30%, transparent);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.dbx-searchable-select-list:hover::-webkit-scrollbar-thumb {
|
||||
border: 0;
|
||||
background: color-mix(in oklch, var(--foreground) 48%, transparent);
|
||||
}
|
||||
|
||||
.dark .dbx-searchable-select-list {
|
||||
scrollbar-color: rgb(82, 82, 91) transparent;
|
||||
}
|
||||
|
||||
.dark .dbx-searchable-select-list::-webkit-scrollbar-thumb {
|
||||
background: rgb(82, 82, 91);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.dark .dbx-searchable-select-list:hover::-webkit-scrollbar-thumb {
|
||||
background: rgb(113, 113, 122);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -595,6 +595,7 @@ export default {
|
|||
tooltipTitle: "Title:",
|
||||
tooltipFilePath: "File Path:",
|
||||
tooltipConnection: "Connection:",
|
||||
tooltipGroup: "Group:",
|
||||
tooltipDatabase: "Database:",
|
||||
tooltipTable: "Table:",
|
||||
tooltipCollection: "Collection:",
|
||||
|
|
@ -1191,6 +1192,7 @@ export default {
|
|||
moveToGroup: "Move to Group",
|
||||
moveToNewGroup: "Move to New Group",
|
||||
ungrouped: "Ungrouped",
|
||||
ungroupedLabel: "Ungrouped",
|
||||
newGroup: "New Group...",
|
||||
newGroupDefault: "New Group",
|
||||
deleteGroupConfirmTitle: "Delete Group",
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ export default withEnglishFallback({
|
|||
objects: "Objetos",
|
||||
tooltipTitle: "Título:",
|
||||
tooltipConnection: "Conexión:",
|
||||
tooltipGroup: "Grupo:",
|
||||
tooltipDatabase: "Base de datos:",
|
||||
tooltipTable: "Tabla:",
|
||||
tooltipCollection: "Colección:",
|
||||
|
|
@ -1130,6 +1131,7 @@ export default withEnglishFallback({
|
|||
moveToGroup: "Mover al grupo",
|
||||
moveToNewGroup: "Mover a nuevo grupo",
|
||||
ungrouped: "Sin grupo",
|
||||
ungroupedLabel: "Sin grupo",
|
||||
newGroup: "Nuevo grupo...",
|
||||
newGroupDefault: "Nuevo grupo",
|
||||
deleteGroupConfirmTitle: "Eliminar grupo",
|
||||
|
|
|
|||
|
|
@ -574,6 +574,7 @@ export default withEnglishFallback({
|
|||
executionSummary: "Riepilogo",
|
||||
tooltipTitle: "Titolo:",
|
||||
tooltipConnection: "Connessione:",
|
||||
tooltipGroup: "Gruppo:",
|
||||
tooltipDatabase: "Database:",
|
||||
tooltipTable: "Tabella:",
|
||||
tooltipCollection: "Collezione:",
|
||||
|
|
@ -1128,6 +1129,7 @@ export default withEnglishFallback({
|
|||
moveToGroup: "Sposta nel Gruppo",
|
||||
moveToNewGroup: "Sposta in un Nuovo Gruppo",
|
||||
ungrouped: "Non raggruppati",
|
||||
ungroupedLabel: "Non raggruppati",
|
||||
newGroup: "Nuovo Gruppo...",
|
||||
newGroupDefault: "Nuovo Gruppo",
|
||||
deleteGroupConfirmTitle: "Elimina Gruppo",
|
||||
|
|
|
|||
|
|
@ -573,6 +573,7 @@ export default withEnglishFallback({
|
|||
tooltipTitle: "タイトル:",
|
||||
tooltipFilePath: "ファイルパス:",
|
||||
tooltipConnection: "接続:",
|
||||
tooltipGroup: "グループ:",
|
||||
tooltipDatabase: "データベース:",
|
||||
tooltipTable: "テーブル:",
|
||||
tooltipCollection: "コレクション:",
|
||||
|
|
@ -1129,6 +1130,7 @@ export default withEnglishFallback({
|
|||
moveToGroup: "グループに移動",
|
||||
moveToNewGroup: "新しいグループに移動",
|
||||
ungrouped: "グループなし",
|
||||
ungroupedLabel: "グループなし",
|
||||
newGroup: "新しいグループ...",
|
||||
newGroupDefault: "新しいグループ",
|
||||
deleteGroupConfirmTitle: "グループを削除",
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ export default withEnglishFallback({
|
|||
tooltipTitle: "Título:",
|
||||
tooltipFilePath: "Caminho do arquivo:",
|
||||
tooltipConnection: "Conexão:",
|
||||
tooltipGroup: "Grupo:",
|
||||
tooltipDatabase: "Banco de dados:",
|
||||
tooltipTable: "Tabela:",
|
||||
tooltipCollection: "Coleção:",
|
||||
|
|
@ -1130,6 +1131,7 @@ export default withEnglishFallback({
|
|||
moveToGroup: "Mover para Grupo",
|
||||
moveToNewGroup: "Mover para Novo Grupo",
|
||||
ungrouped: "Sem grupo",
|
||||
ungroupedLabel: "Sem grupo",
|
||||
newGroup: "Novo Grupo...",
|
||||
newGroupDefault: "Novo Grupo",
|
||||
deleteGroupConfirmTitle: "Excluir Grupo",
|
||||
|
|
|
|||
|
|
@ -597,6 +597,7 @@ export default withEnglishFallback({
|
|||
tooltipTitle: "标题:",
|
||||
tooltipFilePath: "文件路径:",
|
||||
tooltipConnection: "连接:",
|
||||
tooltipGroup: "分组:",
|
||||
tooltipDatabase: "数据库:",
|
||||
tooltipTable: "表:",
|
||||
tooltipCollection: "集合:",
|
||||
|
|
@ -1193,6 +1194,7 @@ export default withEnglishFallback({
|
|||
moveToGroup: "移至分组",
|
||||
moveToNewGroup: "移至新分组",
|
||||
ungrouped: "取消分组",
|
||||
ungroupedLabel: "未分组",
|
||||
newGroup: "新建分组...",
|
||||
newGroupDefault: "新分组",
|
||||
deleteGroupConfirmTitle: "删除分组",
|
||||
|
|
|
|||
|
|
@ -576,6 +576,7 @@ export default withEnglishFallback({
|
|||
tooltipTitle: "標題:",
|
||||
tooltipFilePath: "檔案路徑:",
|
||||
tooltipConnection: "連線:",
|
||||
tooltipGroup: "群組:",
|
||||
tooltipDatabase: "資料庫:",
|
||||
tooltipTable: "資料表:",
|
||||
tooltipCollection: "集合:",
|
||||
|
|
@ -1130,6 +1131,7 @@ export default withEnglishFallback({
|
|||
moveToGroup: "移至群組",
|
||||
moveToNewGroup: "移至新群組",
|
||||
ungrouped: "取消群組",
|
||||
ungroupedLabel: "未分組",
|
||||
newGroup: "新增群組……",
|
||||
newGroupDefault: "新群組",
|
||||
deleteGroupConfirmTitle: "刪除群組",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
emptyLayout,
|
||||
remapSidebarLayoutConnectionIds,
|
||||
collapseAllGroups,
|
||||
buildConnectionGroupPathMap,
|
||||
} from "../../apps/desktop/src/lib/sidebar/sidebarLayout.ts";
|
||||
import type { ConnectionConfig, SidebarLayout } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
|
|
@ -34,6 +35,31 @@ function groupConnectionIds(entry: SidebarLayout["order"][number]): string[] {
|
|||
return (entry.children ?? []).filter((child) => child.type === "connection").map((child) => child.id);
|
||||
}
|
||||
|
||||
test("builds all connection group paths in one layout traversal", () => {
|
||||
const paths = buildConnectionGroupPathMap({
|
||||
groups: [
|
||||
{ id: "project", name: "Project", collapsed: false },
|
||||
{ id: "staging", name: "Staging", collapsed: false },
|
||||
],
|
||||
order: [
|
||||
{ type: "connection", id: "ungrouped" },
|
||||
{
|
||||
type: "group",
|
||||
id: "project",
|
||||
children: [
|
||||
{ type: "connection", id: "project-db" },
|
||||
{ type: "group", id: "staging", children: [{ type: "connection", id: "staging-db" }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(paths.get("ungrouped"), []);
|
||||
assert.deepEqual(paths.get("project-db"), ["Project"]);
|
||||
assert.deepEqual(paths.get("staging-db"), ["Project", "Staging"]);
|
||||
assert.equal(paths.has("missing"), false);
|
||||
});
|
||||
|
||||
// --- reconcileLayout ---
|
||||
|
||||
test("reconcileLayout returns all connections ungrouped when layout is null", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue