fix(connection): share connection group path map
This commit is contained in:
parent
8311a88b07
commit
1c34b2b568
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue