diff --git a/src-web/components/DialogContext.tsx b/src-web/components/DialogContext.tsx index d03086ee..1c2b272d 100644 --- a/src-web/components/DialogContext.tsx +++ b/src-web/components/DialogContext.tsx @@ -20,7 +20,7 @@ interface Actions { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -const DialogContext = createContext({} as any); +const DialogContext = createContext({} as State); export const DialogProvider = ({ children }: { children: React.ReactNode }) => { const [dialogs, setDialogs] = useState([]); diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx index 189abcd3..83be560c 100644 --- a/src-web/components/GrpcConnectionSetupPane.tsx +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -119,6 +119,11 @@ export function GrpcConnectionSetupPane({ onGo(); }, [activeRequest, onGo]); + const handleSend = useCallback(async () => { + if (activeRequest == null) return; + onSend({ message: activeRequest.message }); + }, [activeRequest, onGo]); + const tabs: TabItem[] = useMemo( () => [ { value: 'message', label: 'Message' }, @@ -212,52 +217,52 @@ export function GrpcConnectionSetupPane({ {select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'} - {!isStreaming && ( + {methodType === 'client_streaming' || methodType === 'streaming' ? ( + <> + {isStreaming && ( + <> + + + + )} + + + ) : ( )} - {isStreaming && ( - - )} - {(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && ( - <> - - onSend({ message: activeRequest.message ?? '' })} - icon="sendHorizontal" - /> - - )} { - if (activeRequest?.model === 'http_request') { - await duplicateHttpRequest.mutateAsync(); - } else { - await duplicateGrpcRequest.mutateAsync(); - } - }); + useHotKey( + 'http_request.duplicate', + async () => { + if (activeRequest?.model === 'http_request') { + await duplicateHttpRequest.mutateAsync(); + } else { + await duplicateGrpcRequest.mutateAsync(); + } + }, + { enable: !hidden }, + ); const isCollapsed = useCallback( (id: string) => collapsed.value?.[id] ?? false, @@ -242,25 +246,29 @@ export function Sidebar({ className }: Props) { useKeyPressEvent('Backspace', handleDeleteKey); useKeyPressEvent('Delete', handleDeleteKey); - useHotKey('sidebar.focus', async () => { - // Hide the sidebar if it's already focused - if (!hidden && hasFocus) { - await hide(); - return; - } + useHotKey( + 'sidebar.focus', + async () => { + // Hide the sidebar if it's already focused + if (!hidden && hasFocus) { + await hide(); + return; + } - // Show the sidebar if it's hidden - if (hidden) { - await show(); - } + // Show the sidebar if it's hidden + if (hidden) { + await show(); + } - // Select 0 index on focus if none selected - focusActiveRequest( - selectedTree != null && selectedId != null - ? { forced: { id: selectedId, tree: selectedTree } } - : undefined, - ); - }); + // Select 0 index on focus if none selected + focusActiveRequest( + selectedTree != null && selectedId != null + ? { forced: { id: selectedId, tree: selectedTree } } + : undefined, + ); + }, + { enable: !hidden }, + ); useKeyPressEvent('Enter', (e) => { if (!hasFocus) return; diff --git a/src-web/hooks/useCreateFolder.ts b/src-web/hooks/useCreateFolder.ts index 1e87b617..624bcec5 100644 --- a/src-web/hooks/useCreateFolder.ts +++ b/src-web/hooks/useCreateFolder.ts @@ -2,12 +2,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; import type { Folder } from '../lib/models'; +import { useActiveRequest } from './useActiveRequest'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { foldersQueryKey } from './useFolders'; import { usePrompt } from './usePrompt'; export function useCreateFolder() { const workspaceId = useActiveWorkspaceId(); + const activeRequest = useActiveRequest(); const queryClient = useQueryClient(); const prompt = usePrompt(); @@ -28,6 +30,7 @@ export function useCreateFolder() { placeholder: 'Name', })); patch.sortPriority = patch.sortPriority || -Date.now(); + patch.folderId = patch.folderId || activeRequest?.folderId; return invoke('cmd_create_folder', { workspaceId, ...patch }); }, onSettled: () => trackEvent('folder', 'create'), diff --git a/src-web/hooks/useCreateGrpcRequest.ts b/src-web/hooks/useCreateGrpcRequest.ts index f73bc983..560847d3 100644 --- a/src-web/hooks/useCreateGrpcRequest.ts +++ b/src-web/hooks/useCreateGrpcRequest.ts @@ -3,13 +3,14 @@ import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; import type { GrpcRequest } from '../lib/models'; import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveRequest } from './useActiveRequest'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useAppRoutes } from './useAppRoutes'; export function useCreateGrpcRequest() { const workspaceId = useActiveWorkspaceId(); const activeEnvironmentId = useActiveEnvironmentId(); - const activeRequest = null; + const activeRequest = useActiveRequest(); const routes = useAppRoutes(); return useMutation< @@ -24,13 +25,13 @@ export function useCreateGrpcRequest() { if (patch.sortPriority === undefined) { if (activeRequest != null) { // Place above currently-active request - // patch.sortPriority = activeRequest.sortPriority + 0.0001; + patch.sortPriority = activeRequest.sortPriority + 0.0001; } else { // Place at the very top patch.sortPriority = -Date.now(); } } - // patch.folderId = patch.folderId; // TODO: || activeRequest?.folderId; + patch.folderId = patch.folderId || activeRequest?.folderId; return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch }); }, onSettled: () => trackEvent('grpc_request', 'create'), diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index 7b284eb1..7cd87bbe 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -38,7 +38,7 @@ export function useGrpc( mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'), }); - const debouncedUrl = useDebouncedValue(req?.url ?? 'n/a', 1000); + const debouncedUrl = useDebouncedValue(req?.url ?? 'n/a', 500); const reflect = useQuery({ enabled: req != null, queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl], diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts index 908e93e7..2aadc9d0 100644 --- a/src-web/hooks/useHotKey.ts +++ b/src-web/hooks/useHotKey.ts @@ -4,6 +4,8 @@ import { capitalize } from '../lib/capitalize'; import { debounce } from '../lib/debounce'; import { useOsInfo } from './useOsInfo'; +const HOLD_KEYS = ['Shift', 'CmdCtrl', 'Alt', 'Meta']; + export type HotkeyAction = | 'popup.close' | 'environmentEditor.toggle' @@ -61,21 +63,6 @@ export function useHotKey( action: HotkeyAction | null, callback: (e: KeyboardEvent) => void, options: Options = {}, -) { - useAnyHotkey((hkAction, e) => { - // Triggered hotkey! - if (hkAction === action) { - e.preventDefault(); - e.stopPropagation(); - console.log('TRIGGERED HOTKEY', hkAction, options); - callback(e); - } - }, options); -} - -function useAnyHotkey( - callback: (action: HotkeyAction, e: KeyboardEvent) => void, - options: Options, ) { const currentKeys = useRef>(new Set()); const callbackRef = useRef(callback); @@ -87,7 +74,7 @@ function useAnyHotkey( }, [callback]); useEffect(() => { - // Sometimes the keyup event doesn't fire, so we clear the keys after a timeout + // Sometimes the keyup event doesn't fire (eg, cmd+Tab), so we clear the keys after a timeout const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000); const down = (e: KeyboardEvent) => { @@ -95,26 +82,41 @@ function useAnyHotkey( return; } - currentKeys.current.add(normalizeKey(e.key, os)); + const key = normalizeKey(e.key, os); + + currentKeys.current.add(normalizeKey(key, os)); for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) { for (const hkKey of hkKeys) { const keys = hkKey.split('+'); if ( keys.length === currentKeys.current.size && - keys.every((key) => currentKeys.current.has(key)) + keys.every((key) => currentKeys.current.has(key)) && + hkAction === action ) { - callbackRef.current(hkAction, e); + e.preventDefault(); + e.stopPropagation(); + callbackRef.current(e); } } } + clearCurrentKeys(); }; const up = (e: KeyboardEvent) => { if (options.enable === false) { return; } - currentKeys.current.delete(normalizeKey(e.key, os)); + const key = normalizeKey(e.key, os); + currentKeys.current.delete(normalizeKey(key, os)); + + // Clear all keys if no longer holding modifier + // HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ; + // As you see, the ":" is not removed because it turned into ";" when shift was released + const isHoldingModifier = HOLD_KEYS.some((k) => currentKeys.current.has(k)); + if (!isHoldingModifier) { + currentKeys.current.clear(); + } }; document.addEventListener('keydown', down, { capture: true }); document.addEventListener('keyup', up, { capture: true }); diff --git a/src-web/main.tsx b/src-web/main.tsx index 12f3f754..fdc6c5e4 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -20,9 +20,10 @@ if (osType !== 'Darwin') { const settings = await getSettings(); setAppearanceOnDocument(settings.appearance as Appearance); -window.addEventListener('keypress', (e) => { - // Don't go back in history on backspace - if (e.key === 'Backspace') e.preventDefault(); +window.addEventListener('keydown', (e) => { + // Hack to not go back in history on backspace. Check for document body + // or else it will prevent backspace in input fields. + if (e.key === 'Backspace' && e.target === document.body) e.preventDefault(); }); createRoot(document.getElementById('root') as HTMLElement).render(