From f2ce70dd685fb71cac9cd069d95f3b711b064611 Mon Sep 17 00:00:00 2001 From: Terry Yao <69723+yaoxinghuo@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:48:52 +0800 Subject: [PATCH] feat(connection): add copy connection name --- .../src/components/sidebar/ConnectionTree.vue | 11 +++++----- .../sidebar/SidebarTreeRuntimeHost.vue | 12 +++++++++- .../sidebarConnectionSelection.spec.ts | 22 +++++++++++++++++-- .../lib/sidebar/sidebarConnectionSelection.ts | 13 +++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 7367fabd3..48456a1c4 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -11,7 +11,7 @@ import { filterSidebarSearchRootsByConnectionState, filterSidebarTree, filterSid import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts"; import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick"; import { copyToClipboard } from "@/lib/common/clipboard"; -import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection"; +import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection"; import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch"; import { usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; @@ -1304,11 +1304,10 @@ function requestSelectedConnectionEdit(): boolean { function copySelectedSidebarNames(): boolean { const nodes = selectedSidebarNodesInVisibleOrder(); if (nodes.length === 0) return false; - const connectionNodes = selectedConnectionClipboardNodes(nodes); - if (connectionNodes.length > 0) { - const copiedCount = store.copyConnectionsToTreeClipboard(connectionNodes.map((node) => node.connectionId)); - if (copiedCount > 0) toast(t("connection.copied"), 2000); - return copiedCount > 0; + const copiedCount = copySelectedConnectionsToClipboards(nodes, (connectionIds) => store.copyConnectionsToTreeClipboard(connectionIds), copyToClipboard); + if (copiedCount > 0) { + toast(t("connection.copied"), 2000); + return true; } const tableNodes = nodes.filter((node) => node.type === "table" && !!node.connectionId && !!node.database); store.treeClipboard = diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 3a3a53068..d43dd2bda 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -1285,7 +1285,14 @@ async function copySelectedNames() { const connectionTargets = selectedConnectionClipboardTargets(activeNode.value, nodes); if (connectionTargets.length > 0) { const copiedCount = connectionStore.copyConnectionsToTreeClipboard(connectionTargets.map((node) => node.connectionId)); - if (copiedCount > 0) toast(t("connection.copied"), 2000); + if (copiedCount > 0) { + try { + await copyToClipboard(connectionTargets.map(copyNameForTreeNode).join("\n")); + } catch { + /* system clipboard copy is best-effort */ + } + toast(t("connection.copied"), 2000); + } return; } updateTreeClipboardForNodes(nodes); @@ -3504,6 +3511,9 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean } else { items.push({ label: t("contextMenu.closeConnection"), action: disconnectConnection, icon: Unplug }); } + items.push({ label: "", separator: true }); + items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value }); + items.push({ label: "", separator: true }); items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare }); if (currentDatabaseType() === "redis") { items.push({ label: t("contextMenu.instanceInfo"), action: openRedisInstanceInfo, icon: Info }); diff --git a/apps/desktop/src/lib/__tests__/sidebar/sidebarConnectionSelection.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/sidebarConnectionSelection.spec.ts index 7c1068f00..76c3587f1 100644 --- a/apps/desktop/src/lib/__tests__/sidebar/sidebarConnectionSelection.spec.ts +++ b/apps/desktop/src/lib/__tests__/sidebar/sidebarConnectionSelection.spec.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebar/sidebarConnectionSelection"; +import { describe, expect, it, vi } from "vitest"; +import { copySelectedConnectionsToClipboards, selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebar/sidebarConnectionSelection"; import type { TreeNode } from "@/types/database"; function node(id: string, type: TreeNode["type"] = "connection"): TreeNode { @@ -32,4 +32,22 @@ describe("sidebar connection selection", () => { expect(selectedConnectionDuplicateTargets(current, selected)).toEqual(selectedConnectionDeleteTargets(current, selected)); }); + + it("copies selected connection names to the system clipboard for the sidebar copy shortcut", () => { + const copyConnectionsToTreeClipboard = vi.fn(() => 2); + const copyToSystemClipboard = vi.fn(() => Promise.resolve()); + + expect(copySelectedConnectionsToClipboards([node("conn-1"), node("conn-2")], copyConnectionsToTreeClipboard, copyToSystemClipboard)).toBe(2); + expect(copyConnectionsToTreeClipboard).toHaveBeenCalledWith(["conn-1", "conn-2"]); + expect(copyToSystemClipboard).toHaveBeenCalledWith("conn-1\nconn-2"); + }); + + it("keeps the tree clipboard copy when system clipboard access is denied", async () => { + const copyConnectionsToTreeClipboard = vi.fn(() => 1); + const copyToSystemClipboard = vi.fn(() => Promise.reject(new Error("denied"))); + + expect(copySelectedConnectionsToClipboards([node("conn-1")], copyConnectionsToTreeClipboard, copyToSystemClipboard)).toBe(1); + await Promise.resolve(); + expect(copyConnectionsToTreeClipboard).toHaveBeenCalledWith(["conn-1"]); + }); }); diff --git a/apps/desktop/src/lib/sidebar/sidebarConnectionSelection.ts b/apps/desktop/src/lib/sidebar/sidebarConnectionSelection.ts index 871476cd3..04d376338 100644 --- a/apps/desktop/src/lib/sidebar/sidebarConnectionSelection.ts +++ b/apps/desktop/src/lib/sidebar/sidebarConnectionSelection.ts @@ -1,4 +1,5 @@ import type { TreeNode } from "@/types/database"; +import { copyNameForTreeNode } from "@/lib/sidebar/treeNodeClick"; type ConnectionTreeNode = TreeNode & { connectionId: string }; @@ -39,6 +40,18 @@ export function selectedConnectionClipboardNodes(selectedNodes: TreeNode[]): Con return selectedNodes; } +export function copySelectedConnectionsToClipboards(selectedNodes: TreeNode[], copyConnectionsToTreeClipboard: (connectionIds: string[]) => number, copyToSystemClipboard: (text: string) => Promise): number { + const connectionNodes = selectedConnectionClipboardNodes(selectedNodes); + if (connectionNodes.length === 0) return 0; + + const copiedCount = copyConnectionsToTreeClipboard(connectionNodes.map((node) => node.connectionId)); + if (copiedCount > 0) { + // Connection duplication uses the tree clipboard, so keep it available even if OS clipboard access is denied. + void copyToSystemClipboard(connectionNodes.map(copyNameForTreeNode).join("\n")).catch(() => {}); + } + return copiedCount; +} + export function connectionPasteTargetGroupId(node: TreeNode | null | undefined, groupIdForConnection: (connectionId: string) => string | null): string | null { if (!node) return null; if (node.type === "connection-group") return node.id;