From 1198aa7d875ab14f54a5b94779416f71c4c7ba6d Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 24 Oct 2025 08:01:38 -0700 Subject: [PATCH] Add tree rename (on Enter) and global rename hotkeys (#279) --- src-web/components/CommandPaletteDialog.tsx | 7 +- src-web/components/GlobalHooks.tsx | 15 ++- src-web/components/NewSidebar.tsx | 67 +++++++++----- src-web/components/RecentRequestsDropdown.tsx | 6 +- src-web/components/core/Dropdown.tsx | 7 +- src-web/components/core/tree/Tree.tsx | 92 ++++++++++++++----- src-web/components/core/tree/TreeItem.tsx | 46 ++++++++-- src-web/components/core/tree/TreeItemList.tsx | 7 +- src-web/hooks/useHotKey.ts | 64 ++++++++----- 9 files changed, 218 insertions(+), 93 deletions(-) diff --git a/src-web/components/CommandPaletteDialog.tsx b/src-web/components/CommandPaletteDialog.tsx index 1daa35e2..8a6bfa06 100644 --- a/src-web/components/CommandPaletteDialog.tsx +++ b/src-web/components/CommandPaletteDialog.tsx @@ -85,6 +85,11 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { action: 'settings.show', onSelect: () => openSettings.mutate(null), }, + { + key: 'folder.create', + label: 'Create Folder', + onSelect: () => createFolder.mutate({}), + }, { key: 'app.create', label: 'Create Workspace', @@ -177,7 +182,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { }); commands.push({ - key: 'sidebar.delete_selected_item', + key: 'sidebar.selected.delete', label: 'Delete Request', onSelect: () => deleteModelWithConfirm(activeRequest), }); diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index c6936add..f98d2c7f 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -1,11 +1,14 @@ +import { activeRequestAtom } from '../hooks/useActiveRequest'; import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; -import { useSubscribeHotKeys } from '../hooks/useHotKey'; +import { useHotKey, useSubscribeHotKeys } from '../hooks/useHotKey'; import { useSubscribeHttpAuthentication } from '../hooks/useHttpAuthentication'; import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions'; +import { jotaiStore } from '../lib/jotai'; +import { renameModelWithPrompt } from '../lib/renameModelWithPrompt'; export function GlobalHooks() { useSyncZoomSetting(); @@ -21,5 +24,15 @@ export function GlobalHooks() { useActiveWorkspaceChangedToast(); useSubscribeHotKeys(); + useHotKey( + 'request.rename', + async () => { + const model = jotaiStore.get(activeRequestAtom); + if (model == null) return; + await renameModelWithPrompt(model); + }, + { allowDefault: true }, + ); + return null; } diff --git a/src-web/components/NewSidebar.tsx b/src-web/components/NewSidebar.tsx index 8f10cae0..61ac1c6f 100644 --- a/src-web/components/NewSidebar.tsx +++ b/src-web/components/NewSidebar.tsx @@ -256,38 +256,53 @@ const sidebarTreeAtom = atom((get) => { }); const actions = { - 'sidebar.delete_selected_item': async function (items: SidebarModel[]) { - await deleteModelWithConfirm(items); + 'sidebar.selected.delete': { + enable: isSidebarFocused, + cb: async function (_: TreeHandle, items: SidebarModel[]) { + await deleteModelWithConfirm(items); + }, }, - 'model.duplicate': async function (items: SidebarModel[]) { - if (items.length === 1) { - const item = items[0]!; - const newId = await duplicateModel(item); - navigateToRequestOrFolderOrWorkspace(newId, item.model); - } else { - await Promise.all(items.map(duplicateModel)); - } + 'sidebar.selected.rename': { + enable: isSidebarFocused, + allowDefault: true, + cb: async function (tree: TreeHandle, items: SidebarModel[]) { + const item = items[0]; + if (items.length === 1 && item != null) { + tree.renameItem(item.id); + } + }, }, - 'request.send': async function (items: SidebarModel[]) { - await Promise.all( - items.filter((i) => i.model === 'http_request').map((i) => sendAnyHttpRequest.mutate(i.id)), - ); + 'sidebar.selected.duplicate': { + priority: 999, + enable: isSidebarFocused, + cb: async function (_: TreeHandle, items: SidebarModel[]) { + if (items.length === 1) { + const item = items[0]!; + const newId = await duplicateModel(item); + navigateToRequestOrFolderOrWorkspace(newId, item.model); + } else { + await Promise.all(items.map(duplicateModel)); + } + }, + }, + 'request.send': { + enable: isSidebarFocused, + cb: async function (_: TreeHandle, items: SidebarModel[]) { + await Promise.all( + items.filter((i) => i.model === 'http_request').map((i) => sendAnyHttpRequest.mutate(i.id)), + ); + }, }, } as const; -const hotkeys: TreeProps['hotkeys'] = { - priority: 10, // So these ones take precedence over global hotkeys when the sidebar is focused - actions, - enable: () => isSidebarFocused(), -}; +const hotkeys: TreeProps['hotkeys'] = { actions }; -async function getContextMenu(items: SidebarModel[]): Promise { +async function getContextMenu(tree: TreeHandle, items: SidebarModel[]): Promise { const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const child = items[0]; // No children means we're in the root if (child == null) { - console.log('HELLO', child); return getCreateDropdownItems({ workspaceId, activeRequest: null, folderId: null }); } @@ -321,7 +336,7 @@ async function getContextMenu(items: SidebarModel[]): Promise { hotKeyLabelOnly: true, hidden: !onlyHttpRequests, leftSlot: , - onSelect: () => actions['request.send'](items), + onSelect: () => actions['request.send'].cb(tree, items), }, ...(items.length === 1 && child.model === 'http_request' ? await getHttpRequestActions() @@ -362,6 +377,8 @@ async function getContextMenu(items: SidebarModel[]): Promise { label: 'Rename', leftSlot: , hidden: items.length > 1, + hotKeyAction: 'sidebar.selected.rename', + hotKeyLabelOnly: true, onSelect: async () => { const request = getModel( ['folder', 'http_request', 'grpc_request', 'websocket_request'], @@ -375,7 +392,7 @@ async function getContextMenu(items: SidebarModel[]): Promise { hotKeyAction: 'model.duplicate', hotKeyLabelOnly: true, // Would trigger for every request (bad) leftSlot: , - onSelect: () => actions['model.duplicate'](items), + onSelect: () => actions['sidebar.selected.duplicate'].cb(tree, items), }, { label: 'Move', @@ -393,10 +410,10 @@ async function getContextMenu(items: SidebarModel[]): Promise { { color: 'danger', label: 'Delete', - hotKeyAction: 'sidebar.delete_selected_item', + hotKeyAction: 'sidebar.selected.delete', hotKeyLabelOnly: true, leftSlot: , - onSelect: () => actions['sidebar.delete_selected_item'](items), + onSelect: () => actions['sidebar.selected.delete'].cb(tree, items), }, ...modelCreationItems, ]; diff --git a/src-web/components/RecentRequestsDropdown.tsx b/src-web/components/RecentRequestsDropdown.tsx index 1046eda7..9bc134da 100644 --- a/src-web/components/RecentRequestsDropdown.tsx +++ b/src-web/components/RecentRequestsDropdown.tsx @@ -32,7 +32,7 @@ export function RecentRequestsDropdown({ className }: Props) { } }); - useHotKey('request_switcher.prev', () => { + useHotKey('switcher.prev', () => { if (!dropdownRef.current?.isOpen) { // Select the second because the first is the current request dropdownRef.current?.open(1); @@ -41,7 +41,7 @@ export function RecentRequestsDropdown({ className }: Props) { } }); - useHotKey('request_switcher.next', () => { + useHotKey('switcher.next', () => { if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(); dropdownRef.current?.prev?.(); }); @@ -87,7 +87,7 @@ export function RecentRequestsDropdown({ className }: Props) {