diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index 7458dd961c..3c05d6e50e 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -124,6 +124,7 @@ import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { copyToClipboard } from "@/lib/clipboard";
+import { formatShortcut } from "@/lib/shortcutRegistry";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorIndicator.vue";
import VisibleDatabasesDialog from "@/components/sidebar/VisibleDatabasesDialog.vue";
@@ -518,8 +519,20 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean {
function onKeydown(event: KeyboardEvent) {
if ((!isSelected.value && !isMultiSelected.value) || isEditableShortcutTarget(event.target)) return;
+ if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F2") {
+ if (!requestRenameSelectedNode()) return;
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+ if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F5") {
+ if (!requestRefreshSelectedNode()) return;
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && isDeleteTreeNodeShortcut(event)) {
- if (!requestDropSelectedNodes()) return;
+ if (!requestDeleteSelectedNode()) return;
event.preventDefault();
event.stopPropagation();
return;
@@ -535,6 +548,73 @@ function isDeleteTreeNodeShortcut(event: KeyboardEvent): boolean {
return event.key === "Delete" || event.key === "Backspace";
}
+function requestRefreshSelectedNode(): boolean {
+ if (!canRefreshTreeNodeShortcut()) return false;
+ void refresh();
+ return true;
+}
+
+function canRefreshTreeNodeShortcut(): boolean {
+ const type = props.node.type;
+ if (type === "connection" || type === "database" || type === "schema" || type === "table" || type === "view") {
+ return true;
+ }
+ return (
+ isGroupLabel(props.node) && type !== "saved-sql-root" && type !== "saved-sql-folder" && type !== "group-partitions"
+ );
+}
+
+function requestRenameSelectedNode(): boolean {
+ const selected = selectedTreeNodesInVisibleOrder();
+ if (selected.length > 1 && selected.some((node) => node.id === props.node.id)) return false;
+ if (canRenameObject.value) {
+ openRenameObjectDialog();
+ return true;
+ }
+ if (props.node.type === "connection-group") {
+ startRenameGroup();
+ return true;
+ }
+ if (props.node.type === "saved-sql-folder") {
+ openRenameSavedSqlFolder();
+ return true;
+ }
+ if (props.node.type === "saved-sql-file") {
+ openRenameSavedSqlFile();
+ return true;
+ }
+ return false;
+}
+
+function requestDeleteSelectedNode(): boolean {
+ if (requestDropSelectedNodes()) return true;
+ if (props.node.type === "connection") {
+ deleteConnection();
+ return true;
+ }
+ if (props.node.type === "connection-group") {
+ deleteConnectionGroup();
+ return true;
+ }
+ if (props.node.type === "saved-sql-file") {
+ deleteSavedSqlFile();
+ return true;
+ }
+ if (props.node.type === "saved-sql-folder") {
+ deleteSavedSqlFolder();
+ return true;
+ }
+ if (canDropDatabase.value) {
+ dropDatabase();
+ return true;
+ }
+ if (canDropSchema.value) {
+ dropSchema();
+ return true;
+ }
+ return false;
+}
+
function onDoubleClick() {
const action = treeNodeRowDoubleClickAction(
props.node.type,
@@ -2538,6 +2618,11 @@ onBeforeUnmount(() => finishTableReferenceDrag());
// ---- CustomContextMenu ----
+const shortcutCopyName = computed(() => formatShortcut("Mod+C"));
+const shortcutRename = "F2";
+const shortcutRefresh = "F5";
+const shortcutDelete = "Delete";
+
function exportDataSubmenu(): ContextMenuItem {
return {
label: t("contextMenu.exportData"),
@@ -2615,7 +2700,12 @@ function treeItemMenuItems(): ContextMenuItem[] {
} else {
items.push({ label: t("connectionGroup.moveToNewGroup"), action: moveToNewGroup, icon: FolderPlus });
}
- items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw });
+ items.push({
+ label: t("contextMenu.refreshChildren"),
+ action: refresh,
+ icon: RefreshCw,
+ shortcut: shortcutRefresh,
+ });
if (canConfigureVisibleDatabases.value) {
items.push({
label: t("contextMenu.selectVisibleDatabases"),
@@ -2630,6 +2720,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
label: t("contextMenu.deleteConnection"),
action: deleteConnection,
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
return items;
@@ -2639,12 +2730,18 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (node.type === "connection-group") {
items.push({ label: t("toolbar.newConnection"), action: newConnectionInGroup, icon: Plus });
items.push({ label: "", separator: true });
- items.push({ label: t("connectionGroup.renameGroup"), action: startRenameGroup, icon: Pencil });
+ items.push({
+ label: t("connectionGroup.renameGroup"),
+ action: startRenameGroup,
+ icon: Pencil,
+ shortcut: shortcutRename,
+ });
items.push({ label: "", separator: true });
items.push({
label: t("connectionGroup.deleteGroup"),
action: deleteConnectionGroup,
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
return items;
@@ -2678,7 +2775,12 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (canOpenDatabaseSearch.value) {
items.push({ label: t("databaseSearch.open"), action: openDatabaseSearch, icon: Search });
}
- items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw });
+ items.push({
+ label: t("contextMenu.refreshChildren"),
+ action: refresh,
+ icon: RefreshCw,
+ shortcut: shortcutRefresh,
+ });
items.push({ label: "", separator: true });
items.push({ label: t("transfer.dataTransfer"), action: openTransfer, icon: ArrowRightLeft });
items.push({ label: t("diff.title"), action: openSchemaDiff, icon: ArrowRightLeft });
@@ -2696,6 +2798,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
label: t("contextMenu.dropDatabase"),
action: dropDatabase,
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
@@ -2704,6 +2807,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
label: t("contextMenu.dropSchema"),
action: dropSchema,
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
@@ -2727,7 +2831,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
// 6. Table / View
if (node.type === "table" || node.type === "view") {
- items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
+ items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: openData, icon: TableProperties });
if (node.type === "view") {
@@ -2739,13 +2843,19 @@ function treeItemMenuItems(): ContextMenuItem[] {
items.push({ label: t("contextMenu.editStructure"), action: openStructureEditor, icon: PencilRuler });
}
if (canRenameObject.value) {
- items.push({ label: t("contextMenu.renameObject"), action: openRenameObjectDialog, icon: Pencil });
+ items.push({
+ label: t("contextMenu.renameObject"),
+ action: openRenameObjectDialog,
+ icon: Pencil,
+ shortcut: shortcutRename,
+ });
}
if (node.type === "view") {
items.push({
label: deleteMenuLabel(t("contextMenu.dropView")),
action: deleteMenuAction(requestDropObject),
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
@@ -2786,17 +2896,23 @@ function treeItemMenuItems(): ContextMenuItem[] {
label: deleteMenuLabel(t("contextMenu.dropTable")),
action: deleteMenuAction(dropTable),
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
items.push({ label: "", separator: true });
- items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw });
+ items.push({
+ label: t("contextMenu.refreshChildren"),
+ action: refresh,
+ icon: RefreshCw,
+ shortcut: shortcutRefresh,
+ });
return items;
}
// 7. Column
if (node.type === "column") {
- items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
+ items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
if (canOpenFieldLineage.value) {
items.push({ label: "", separator: true });
items.push({ label: t("lineage.open"), action: openFieldLineage, icon: Network });
@@ -2807,6 +2923,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
label: deleteMenuLabel(dropTableChildObjectMenuLabel()),
action: deleteMenuAction(requestDropTableChildObject),
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
@@ -2814,13 +2931,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
}
if (node.type === "index" || node.type === "fkey" || node.type === "trigger") {
- items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
+ items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
if (canDropTableChildObject.value) {
items.push({ label: "", separator: true });
items.push({
label: deleteMenuLabel(dropTableChildObjectMenuLabel()),
action: deleteMenuAction(requestDropTableChildObject),
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
}
@@ -2834,7 +2952,12 @@ function treeItemMenuItems(): ContextMenuItem[] {
}
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
if (canRenameObject.value) {
- items.push({ label: t("contextMenu.renameObject"), action: openRenameObjectDialog, icon: Pencil });
+ items.push({
+ label: t("contextMenu.renameObject"),
+ action: openRenameObjectDialog,
+ icon: Pencil,
+ shortcut: shortcutRename,
+ });
}
items.push({ label: "", separator: true });
items.push({
@@ -2843,6 +2966,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
),
action: deleteMenuAction(requestDropObject),
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
return items;
@@ -2851,7 +2975,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (node.type === "package" || node.type === "package-body") {
items.push({ label: t("contextMenu.viewSource"), action: viewObjectSource, icon: Code2 });
items.push({ label: "", separator: true });
- items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
+ items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
return items;
}
@@ -2861,17 +2985,28 @@ function treeItemMenuItems(): ContextMenuItem[] {
items.push({ label: t("savedSql.newFolder"), action: openCreateSavedSqlFolder, icon: FolderPlus });
}
if (node.type === "saved-sql-folder") {
- items.push({ label: t("savedSql.renameFolder"), action: openRenameSavedSqlFolder, icon: Pencil });
+ items.push({
+ label: t("savedSql.renameFolder"),
+ action: openRenameSavedSqlFolder,
+ icon: Pencil,
+ shortcut: shortcutRename,
+ });
items.push({
label: t("savedSql.deleteFolder"),
action: deleteSavedSqlFolder,
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
items.push({ label: "", separator: true });
}
if (node.type !== "saved-sql-root" && node.type !== "saved-sql-folder" && node.type !== "group-partitions") {
- items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw });
+ items.push({
+ label: t("contextMenu.refreshChildren"),
+ action: refresh,
+ icon: RefreshCw,
+ shortcut: shortcutRefresh,
+ });
}
return items;
}
@@ -2879,12 +3014,18 @@ function treeItemMenuItems(): ContextMenuItem[] {
// 10. Saved SQL File
if (node.type === "saved-sql-file") {
items.push({ label: t("savedSql.open"), action: openSavedSqlFile, icon: FileText });
- items.push({ label: t("savedSql.renameFile"), action: openRenameSavedSqlFile, icon: Pencil });
+ items.push({
+ label: t("savedSql.renameFile"),
+ action: openRenameSavedSqlFile,
+ icon: Pencil,
+ shortcut: shortcutRename,
+ });
items.push({ label: "", separator: true });
items.push({
label: t("savedSql.deleteFile"),
action: deleteSavedSqlFile,
icon: Trash2,
+ shortcut: shortcutDelete,
variant: "destructive" as const,
});
return items;
@@ -2893,7 +3034,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
// 11. Universal Copy Name (for all types except connection)
if (hasTypeMenu.value) {
items.push({ label: "", separator: true });
- items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy });
+ items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
}
return items;
diff --git a/apps/desktop/src/components/ui/CustomContextMenu.vue b/apps/desktop/src/components/ui/CustomContextMenu.vue
index 40c5b0012b..306ff0705e 100644
--- a/apps/desktop/src/components/ui/CustomContextMenu.vue
+++ b/apps/desktop/src/components/ui/CustomContextMenu.vue
@@ -9,6 +9,7 @@ export interface ContextMenuItem {
separator?: boolean;
icon?: Component;
iconClass?: string;
+ shortcut?: string;
variant?: "default" | "destructive";
visible?: boolean;
children?: ContextMenuItem[];
@@ -210,6 +211,22 @@ function itemButtonClass(variant?: "default" | "destructive") {
];
}
+function shortcutKeyLabel(part: string): string {
+ if (part === "Cmd") return "⌘";
+ if (part === "Meta") return "⌘";
+ if (part === "Alt") return "⌥";
+ if (part === "Shift") return "⇧";
+ if (part === "Delete") return "Del";
+ if (part === "Backspace") return "⌫";
+ if (part === "Enter") return "↵";
+ if (part === "Escape") return "Esc";
+ return part;
+}
+
+function shortcutKeys(shortcut?: string): string[] {
+ return shortcut?.split("+").filter(Boolean).map(shortcutKeyLabel) || [];
+}
+
onBeforeUnmount(() => {
openMenus.delete(close);
document.removeEventListener("click", onClickOutside, true);
@@ -244,7 +261,15 @@ onBeforeUnmount(() => {
@mouseleave="onItemMouseLeave"
>
- {{ item.label }}
+ {{ item.label }}
+
+ {{ key }}
+
@@ -271,7 +296,15 @@ onBeforeUnmount(() => {
@click="handleSubItemClick(child)"
>
- {{ child.label }}
+ {{ child.label }}
+
+ {{ key }}
+