feat(desktop): add tree menu shortcuts
This commit is contained in:
1 parent
891dfd67ed
commit
e6b8cf8a5b
2 files changed
+191
-17
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<component :is="item.icon" v-if="item.icon" :class="['size-4 shrink-0', item.iconClass]" />
|
||||
{{ item.label }}
|
||||
<span class="min-w-0 flex-1 truncate">{{ item.label }}</span>
|
||||
<span v-if="item.shortcut" class="ml-8 inline-flex shrink-0 items-center gap-1 text-muted-foreground">
|
||||
<kbd
|
||||
v-for="key in shortcutKeys(item.shortcut)"
|
||||
:key="key"
|
||||
class="min-w-5 rounded border border-border/70 bg-muted/60 px-1 py-0.5 text-center font-mono text-[10px] leading-none text-muted-foreground shadow-xs"
|
||||
>{{ key }}</kbd
|
||||
>
|
||||
</span>
|
||||
<ChevronRight v-if="item.children?.length" class="ml-auto size-4" />
|
||||
</button>
|
||||
</template>
|
||||
@@ -271,7 +296,15 @@ onBeforeUnmount(() => {
|
||||
@click="handleSubItemClick(child)"
|
||||
>
|
||||
<component :is="child.icon" v-if="child.icon" :class="['size-4 shrink-0', child.iconClass]" />
|
||||
{{ child.label }}
|
||||
<span class="min-w-0 flex-1 truncate">{{ child.label }}</span>
|
||||
<span v-if="child.shortcut" class="ml-8 inline-flex shrink-0 items-center gap-1 text-muted-foreground">
|
||||
<kbd
|
||||
v-for="key in shortcutKeys(child.shortcut)"
|
||||
:key="key"
|
||||
class="min-w-5 rounded border border-border/70 bg-muted/60 px-1 py-0.5 text-center font-mono text-[10px] leading-none text-muted-foreground shadow-xs"
|
||||
>{{ key }}</kbd
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in new issue
Block a user