fix(connection): share connection group path map

This commit is contained in:
vrustx 2026-07-15 19:29:38 +08:00 committed by GitHub
parent 8311a88b07
commit 1c34b2b568
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 114 additions and 15 deletions

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
import { useConnectionGroupLabel } from "@/composables/useConnectionGroupLabel";
const props = defineProps<{ connectionId: string }>();
const { connectionGroupLabel } = useConnectionGroupLabel();
</script>
<template>
<span class="min-w-0 max-w-28 shrink-0 truncate rounded-sm bg-muted/70 px-1.5 py-0.5 text-[10px] leading-none text-muted-foreground" :title="connectionGroupLabel(props.connectionId)">
{{ connectionGroupLabel(props.connectionId) }}
</span>
</template>

View File

@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import * as api from "@/lib/backend/api";
import { DIAGRAM_SQL_TYPES, isSchemaAware as isSchemaAwareDatabase } from "@/lib/database/databaseCapabilities";
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
@ -814,9 +815,10 @@ onUnmounted(stopDrag);
</SelectTrigger>
<SelectContent>
<SelectItem v-for="connection in sqlConnections" :key="connection.id" :value="connection.id">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="connection.driver_profile || connection.db_type" class="w-3.5 h-3.5" />
{{ connection.name }}
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connection.driver_profile || connection.db_type" class="w-3.5 h-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="connection.id" />
<span class="min-w-0 flex-1 truncate">{{ connection.name }}</span>
</div>
</SelectItem>
</SelectContent>

View File

@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
@ -915,6 +916,7 @@ watch(
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
@ -1001,6 +1003,7 @@ watch(
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(option)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>

View File

@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/backend/api";
@ -269,6 +270,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
@ -353,6 +355,7 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>

View File

@ -50,6 +50,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { connectionIconType } from "@/lib/connection/connectionPresentation";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import { useQueryStore } from "@/stores/queryStore";
import { useToast } from "@/composables/useToast";
import { useNavigationTargets } from "@/composables/useNavigationTargets";
@ -1995,6 +1996,7 @@ async function openExternalUrl(url: string) {
<SelectItem v-for="conn in connectionStore.connections" :key="conn.id" :value="conn.id">
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="connectionIconType(conn)" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="conn.id" />
<span class="truncate">{{ conn.name }}</span>
</div>
</SelectItem>

View File

@ -7,6 +7,7 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import * as api from "@/lib/backend/api";
import type { ExportProgress } from "@/lib/backend/api";
import { isSchemaAware } from "@/lib/database/databaseFeatureSupport";
@ -572,9 +573,10 @@ watch(
</SelectTrigger>
<SelectContent position="popper" align="start">
<SelectItem v-for="c in sqlConnections" :key="c.id" :value="c.id">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="c.driver_profile || c.db_type" class="w-3.5 h-3.5" />
{{ c.name }}
<div class="flex min-w-0 items-center gap-2">
<DatabaseIcon :db-type="c.driver_profile || c.db_type" class="w-3.5 h-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="c.id" />
<span class="min-w-0 flex-1 truncate">{{ c.name }}</span>
</div>
</SelectItem>
</SelectContent>

View File

@ -14,7 +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 { useConnectionGroupLabel } from "@/composables/useConnectionGroupLabel";
import { isSingleDatabase, supportsClearableQuerySchema, supportsSqlInListPaste, supportsTransaction as supportsTransactionFeature } from "@/lib/database/databaseCapabilities";
import { hexToRgba } from "@/lib/common/color";
import { productionContextForDatabase } from "@/lib/database/productionSafety";
@ -67,7 +67,7 @@ const activeDatabaseOptions = computed(() => {
});
const connectionOptionIds = computed(() => connectionStore.connections.map((connection) => connection.id));
const connectionGroupPaths = computed(() => buildConnectionGroupPathMap(connectionStore.sidebarLayout));
const { connectionGroupLabel } = useConnectionGroupLabel();
const activeDatabaseValue = computed(() => props.activeTab.database || "");
const activeProductionContext = computed(() => productionContextForDatabase(props.activeConnection, props.activeTab.database));
const showConnectionProductionBadge = computed(() => activeProductionContext.value.reason === "connection");
@ -156,10 +156,6 @@ 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";

View File

@ -10,6 +10,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import { useToast } from "@/composables/useToast";
import { useConnectionStore } from "@/stores/connectionStore";
import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
@ -502,9 +503,10 @@ watch(
</SelectTrigger>
<SelectContent position="popper">
<SelectItem v-for="c in sqlConnections" :key="c.id" :value="c.id">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="c.driver_profile || c.db_type" class="w-3.5 h-3.5" />
{{ c.name }}
<div class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="c.driver_profile || c.db_type" class="w-3.5 h-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="c.id" />
<span class="min-w-0 flex-1 truncate">{{ c.name }}</span>
</div>
</SelectItem>
</SelectContent>

View File

@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/backend/api";
@ -382,6 +383,7 @@ function getConnectionName(id: string) {
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>
@ -445,6 +447,7 @@ function getConnectionName(id: string) {
<template #option-label="{ option, label }">
<div class="flex min-w-0 items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="h-3.5 w-3.5 shrink-0" />
<ConnectionGroupBadge :connection-id="option" />
<span class="min-w-0 flex-1 truncate">{{ label }}</span>
</div>
</template>

View File

@ -0,0 +1,16 @@
import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
import { useConnectionStore } from "@/stores/connectionStore";
/** Resolves the sidebar group path label for connection dropdown options. */
export function useConnectionGroupLabel() {
const connectionStore = useConnectionStore();
const { connectionGroupPaths } = storeToRefs(connectionStore);
const { t } = useI18n();
function connectionGroupLabel(connectionId: string): string {
return connectionGroupPaths.value.get(connectionId)?.join(" / ") || t("connectionGroup.ungroupedLabel");
}
return { connectionGroupPaths, connectionGroupLabel };
}

View File

@ -0,0 +1,54 @@
import { createPinia, setActivePinia, storeToRefs } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { SidebarLayout } from "@/types/database";
const groupedLayout: SidebarLayout = {
groups: [{ id: "project", name: "Project", collapsed: false }],
order: [
{
type: "group",
id: "project",
children: [{ type: "connection", id: "grouped" }],
},
{ type: "connection", id: "root" },
],
};
describe("connectionStore connection group paths", () => {
beforeEach(() => {
vi.stubGlobal("localStorage", {
getItem: vi.fn(() => null),
setItem: vi.fn(),
removeItem: vi.fn(),
});
setActivePinia(createPinia());
});
it("shares one cached path map across store consumers", async () => {
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.sidebarLayout = groupedLayout;
const firstConsumer = storeToRefs(store).connectionGroupPaths;
const secondConsumer = storeToRefs(store).connectionGroupPaths;
expect(firstConsumer.value).toBe(secondConsumer.value);
expect(firstConsumer.value.get("grouped")).toEqual(["Project"]);
expect(firstConsumer.value.get("root")).toEqual([]);
});
it("rebuilds the shared map when the sidebar layout changes", async () => {
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.sidebarLayout = groupedLayout;
const previousPaths = store.connectionGroupPaths;
store.sidebarLayout = {
groups: [],
order: [{ type: "connection", id: "grouped" }],
};
expect(store.connectionGroupPaths).not.toBe(previousPaths);
expect(store.connectionGroupPaths.get("grouped")).toEqual([]);
});
});

View File

@ -34,6 +34,7 @@ import {
moveConnectionToGroup as moveConnectionToGroupOp,
remapSidebarLayoutConnectionIds,
reorderEntry as reorderEntryOp,
buildConnectionGroupPathMap,
type DropPosition,
} from "@/lib/sidebar/sidebarLayout";
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
@ -327,6 +328,7 @@ export const useConnectionStore = defineStore("connection", () => {
allDatabases?: boolean;
} | null>(null);
const sidebarLayout = ref<SidebarLayout>(emptyLayout());
const connectionGroupPaths = computed(() => buildConnectionGroupPathMap(sidebarLayout.value));
let layoutPersistTimer: ReturnType<typeof setTimeout> | null = null;
const staleTreeRefreshIds = new Set<string>();
const metadataLoadCoordinator = new MetadataLoadCoordinator((event) => {
@ -5318,6 +5320,7 @@ export const useConnectionStore = defineStore("connection", () => {
markConnectionLost,
recordConnectionLostError,
sidebarLayout,
connectionGroupPaths,
getConfig,
connectionIdentifierQuote,
isTreeNodePinned,