feat(sidebar): add connection copy paste shortcuts

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-04 12:19:56 +08:00
1 parent d8047a77b4
commit 90d2ede740
19 files changed
+509 -20

No files matched your search

@@ -1019,7 +1019,7 @@ function copySingleTableToClipboard(row: ObjectBrowserRow) {
function openPasteTableDialog() {
const clipboard = connectionStore.treeClipboard;
if (!canPasteTableClipboard() || !clipboard) {
if (!canPasteTableClipboard() || clipboard?.kind !== "table-copy") {
toast(t("contextMenu.noTableToPaste"), 2000);
return;
}
@@ -8,9 +8,10 @@ import { useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
import type { TreeNode, TreeNodeType } from "@/types/database";
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree } from "@/lib/sidebarSearchTree";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/keyboardShortcuts";
import { copyNameForTreeNode } from "@/lib/treeNodeClick";
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/clipboard";
import { copyToClipboard } from "@/lib/clipboard";
import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selectedConnectionEditTarget } from "@/lib/sidebarConnectionSelection";
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebarTypeSearch";
import { usesTreeSchemaMode } from "@/lib/databaseFeatureSupport";
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
@@ -781,15 +782,22 @@ function focusSearchAtEnd() {
function onWindowKeydown(event: KeyboardEvent) {
if (event.defaultPrevented) return;
if (sidebarShortcutTargetIsActive(event.target)) {
if (eventTargetAllowsAppClipboardShortcut(event, "c")) {
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isEditConnectionShortcut(event)) {
if (requestSelectedConnectionEdit()) {
event.preventDefault();
event.stopPropagation();
}
return;
}
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isCopySidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts)) {
if (copySelectedSidebarNames()) {
event.preventDefault();
event.stopPropagation();
}
return;
}
if (eventTargetAllowsAppClipboardShortcut(event, "v")) {
if (requestSelectedSidebarPasteTable()) {
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isPasteSidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts)) {
if (requestSelectedSidebarPaste()) {
event.preventDefault();
event.stopPropagation();
}
@@ -820,14 +828,39 @@ function sidebarShortcutTargetIsActive(target: EventTarget | null): boolean {
return pointerInsideTree.value && (!active || active === document.body || root.contains(active));
}
function sidebarShortcutTargetAllowsAppShortcut(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return true;
return !(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable || !!target.closest("[contenteditable='true'], [role='textbox']"));
}
function selectedSidebarNodesInVisibleOrder(): TreeNode[] {
const selectedIds = new Set(store.selectedTreeNodeIds);
return visibleNodes.value.filter((node) => selectedIds.has(node.id));
}
function isEditConnectionShortcut(event: KeyboardEvent): boolean {
return isEditSidebarConnectionShortcut(event, settingsStore.editorSettings.shortcuts);
}
function requestSelectedConnectionEdit(): boolean {
const selectedNodeId = store.selectedTreeNodeId;
const currentNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null;
if (!currentNode) return false;
const editTarget = selectedConnectionEditTarget(currentNode, selectedSidebarNodesInVisibleOrder());
if (!editTarget) return false;
store.startEditing(editTarget.connectionId);
return true;
}
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 tableNodes = nodes.filter((node) => node.type === "table" && !!node.connectionId && !!node.database);
store.treeClipboard =
tableNodes.length > 0
@@ -847,9 +880,20 @@ function copySelectedSidebarNames(): boolean {
return true;
}
function requestSelectedSidebarPasteTable(): boolean {
function requestSelectedSidebarPaste(): boolean {
const clipboard = store.treeClipboard;
const selectedNodeId = store.selectedTreeNodeId;
if (clipboard?.kind === "connection-copy") {
const selectedNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null;
const targetGroupId = connectionPasteTargetGroupId(selectedNode, (connectionId) => store.groupIdForConnection(connectionId));
void store
.pasteConnectionClipboard(targetGroupId)
.then((count) => {
if (count > 0) toast(count > 1 ? t("connection.duplicatedSelected", { count }) : t("connection.duplicated"), 2000);
})
.catch((e: any) => toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000));
return true;
}
if (clipboard?.kind !== "table-copy" || clipboard.tables.length === 0 || !selectedNodeId) return false;
window.dispatchEvent(new CustomEvent("dbx:sidebar-request-paste-table", { detail: { nodeId: selectedNodeId } }));
return true;
@@ -79,7 +79,8 @@ import { clearActiveTableReferencePayload, createTableReferencePayload, createTa
import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import { tableOpenPageLimit } from "@/lib/tableOpenPageLimit";
import { supportsDatabaseCreation, supportsDatabaseSearch, supportsFieldLineage, supportsObjectBrowserTreeNode, supportsSchemaDiagram, supportsSqlFileExecution, supportsTableImport, supportsTableTruncate, supportsTableStructureEditing, usesTreeSchemaMode } from "@/lib/databaseCapabilities";
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, sidebarSelectionCopyAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/treeNodeClick";
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/treeNodeClick";
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/keyboardShortcuts";
import { formatSqlInsert } from "@/lib/exportFormats";
import { joinExportedDdls } from "@/lib/ddlExport";
import { fetchTableDataForExport } from "@/lib/tableDataExport";
@@ -118,7 +119,7 @@ import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableD
import { sidebarDisplayTableName } from "@/lib/sidebarTableNameDisplay";
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebarLabelTooltip";
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebarTreeSelection";
import { selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebarConnectionSelection";
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets, selectedConnectionEditTarget } from "@/lib/sidebarConnectionSelection";
import { supportsDatabaseUserAdmin } from "@/lib/databaseUserAdmin";
import { canCloseSidebarDatabaseConnection, isSidebarDatabaseOpened } from "@/lib/sidebarDatabaseOpenState";
import { sidebarTreeContextKey } from "@/lib/sidebarTreeContext";
@@ -842,6 +843,12 @@ function onKeydown(event: KeyboardEvent) {
event.stopPropagation();
return;
}
if (isEditConnectionShortcut(event)) {
if (!requestEditSelectedConnection()) return;
event.preventDefault();
event.stopPropagation();
return;
}
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F2") {
if (!requestRenameSelectedNode()) return;
event.preventDefault();
@@ -860,8 +867,7 @@ function onKeydown(event: KeyboardEvent) {
event.stopPropagation();
return;
}
const action = sidebarSelectionCopyAction(event);
if (action !== "copy-name") return;
if (!isCopyTreeSelectionShortcut(event)) return;
event.preventDefault();
event.stopPropagation();
copySelectedNames();
@@ -872,7 +878,15 @@ function isDeleteTreeNodeShortcut(event: KeyboardEvent): boolean {
}
function isPasteTreeClipboardShortcut(event: KeyboardEvent): boolean {
return (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "v";
return isPasteSidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
}
function isEditConnectionShortcut(event: KeyboardEvent): boolean {
return isEditSidebarConnectionShortcut(event, settingsStore.editorSettings.shortcuts);
}
function isCopyTreeSelectionShortcut(event: KeyboardEvent): boolean {
return isCopySidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
}
function pasteTableTargetContext(): TableClipboardContext | null {
@@ -891,6 +905,16 @@ function canPasteTreeClipboardToCurrentNode(): boolean {
function requestPasteTreeClipboard(): boolean {
const clipboard = connectionStore.treeClipboard;
if (clipboard?.kind === "connection-copy") {
const targetGroupId = connectionPasteTargetGroupId(props.node, (connectionId) => connectionStore.groupIdForConnection(connectionId));
void connectionStore
.pasteConnectionClipboard(targetGroupId)
.then((count) => {
if (count > 0) toast(count > 1 ? t("connection.duplicatedSelected", { count }) : t("connection.duplicated"), 2000);
})
.catch((e: any) => toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000));
return true;
}
if (clipboard?.kind !== "table-copy" || !canPasteTreeClipboardToCurrentNode()) return false;
pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());
pasteTableEntries.value = clipboard.tables.map((entry) => ({
@@ -927,6 +951,11 @@ function canRefreshTreeNodeShortcut(): boolean {
function requestRenameSelectedNode(): boolean {
const selected = selectedTreeNodesInVisibleOrder();
if (selected.length > 1 && selected.some((node) => node.id === props.node.id)) return false;
const editTarget = selectedConnectionEditTarget(props.node, selected);
if (editTarget) {
connectionStore.startEditing(editTarget.connectionId);
return true;
}
if (canRenameObject.value) {
openRenameObjectDialog();
return true;
@@ -938,6 +967,13 @@ function requestRenameSelectedNode(): boolean {
return false;
}
function requestEditSelectedConnection(): boolean {
const editTarget = selectedConnectionEditTarget(props.node, selectedTreeNodesInVisibleOrder());
if (!editTarget) return false;
connectionStore.startEditing(editTarget.connectionId);
return true;
}
function requestDeleteSelectedNode(): boolean {
if (requestDropSelectedNodes()) return true;
if (props.node.type === "connection") {
@@ -1568,6 +1604,12 @@ async function copyFinalProxyPort() {
async function copySelectedNames() {
const selectedNodes = selectedTreeNodesInVisibleOrder();
const nodes = selectedNodes.length > 1 && selectedNodes.some((node) => node.id === props.node.id) ? selectedNodes : [props.node];
const connectionTargets = selectedConnectionClipboardTargets(props.node, nodes);
if (connectionTargets.length > 0) {
const copiedCount = connectionStore.copyConnectionsToTreeClipboard(connectionTargets.map((node) => node.connectionId));
if (copiedCount > 0) toast(t("connection.copied"), 2000);
return;
}
updateTreeClipboardForNodes(nodes);
try {
await copyToClipboard(nodes.map(copyNameForTreeNode).join("\n"));
@@ -3917,7 +3959,8 @@ onBeforeUnmount(() => {
// ---- CustomContextMenu ----
const shortcutCopyName = computed(() => formatShortcut("Mod+C"));
const shortcutCopyName = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.copySidebarSelection));
const shortcutEditConnection = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.editSidebarConnection));
const shortcutRename = "F2";
const shortcutRefresh = "F5";
const shortcutDelete = "Delete";
@@ -4108,7 +4151,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
icon: ListFilter,
});
}
items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil });
items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil, shortcut: shortcutEditConnection.value });
if (revealConnectionFilePath.value) {
items.push({
label: t("contextMenu.revealDatabaseFile"),