diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index c0857ea8..565bffd3 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -35,7 +35,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo }); }, [dialog, activeEnvironment]); - useHotkey('environmentEditor.toggle', showEnvironmentDialog); + useHotkey('environmentEditor.toggle', showEnvironmentDialog, { enable: environments.length > 0 }); const items: DropdownItem[] = useMemo( () => [ diff --git a/src-web/components/RequestResponse.tsx b/src-web/components/RequestResponse.tsx index 0d382e74..9654cd48 100644 --- a/src-web/components/RequestResponse.tsx +++ b/src-web/components/RequestResponse.tsx @@ -3,8 +3,12 @@ import classNames from 'classnames'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import { useLocalStorage } from 'react-use'; +import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; +import { useCreateRequest } from '../hooks/useCreateRequest'; +import { useRequests } from '../hooks/useRequests'; import { clamp } from '../lib/clamp'; +import { HotKeyList } from './core/HotKeyList'; import { RequestPane } from './RequestPane'; import { ResizeHandle } from './ResizeHandle'; import { ResponsePane } from './ResponsePane'; @@ -24,6 +28,9 @@ const STACK_VERTICAL_WIDTH = 600; export const RequestResponse = memo(function RequestResponse({ style }: Props) { const containerRef = useRef(null); + const activeRequest = useActiveRequest(); + const createRequest = useCreateRequest(); + const requests = useRequests(); const [vertical, setVertical] = useState(false); const [widthRaw, setWidth] = useLocalStorage(`body_width::${useActiveWorkspaceId()}`); const [heightRaw, setHeight] = useLocalStorage(`body_height::${useActiveWorkspaceId()}`); @@ -114,6 +121,24 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) { [width, height, vertical, setHeight, setWidth], ); + if (activeRequest === null && requests.length > 0) { + return ( +
+

No Selected Request

+
+ ); + } else if (requests.length === 0) { + return ( + + ); + } + return (
diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 6dada0d2..1997836e 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import type { CSSProperties } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { createGlobalState } from 'react-use'; -import { useActiveRequestId } from '../hooks/useActiveRequestId'; +import { useActiveRequest } from '../hooks/useActiveRequest'; import { useLatestResponse } from '../hooks/useLatestResponse'; import { useResponseContentType } from '../hooks/useResponseContentType'; import { useResponses } from '../hooks/useResponses'; @@ -12,6 +12,7 @@ import { isResponseLoading } from '../lib/models'; import { Banner } from './core/Banner'; import { CountBadge } from './core/CountBadge'; import { DurationTag } from './core/DurationTag'; +import { HotKeyList } from './core/HotKeyList'; import { SizeTag } from './core/SizeTag'; import { HStack } from './core/Stacks'; import { StatusTag } from './core/StatusTag'; @@ -34,9 +35,9 @@ const useActiveTab = createGlobalState('body'); export const ResponsePane = memo(function ResponsePane({ style, className }: Props) { const [pinnedResponseId, setPinnedResponseId] = useState(null); - const activeRequestId = useActiveRequestId(); - const latestResponse = useLatestResponse(activeRequestId); - const responses = useResponses(activeRequestId); + const activeRequest = useActiveRequest(); + const latestResponse = useLatestResponse(activeRequest?.id ?? null); + const responses = useResponses(activeRequest?.id ?? null); const activeResponse: HttpResponse | null = pinnedResponseId ? responses.find((r) => r.id === pinnedResponseId) ?? null : latestResponse ?? null; @@ -84,6 +85,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro [activeResponse?.headers, contentType, setViewMode, viewMode], ); + if (activeRequest === null) { + return null; + } + return (
{activeResponse?.error && {activeResponse.error}} + {!activeResponse && ( + <> + + + + )} {activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && ( <> invoke('check_for_updates'), leftSlot: , }, + { + key: 'feedback', + label: 'Feedback', + onSelect: () => shell.open('https://yaak.canny.io'), + leftSlot: , + }, ]} > diff --git a/src-web/components/core/HotKey.tsx b/src-web/components/core/HotKey.tsx index f162c883..bacf438a 100644 --- a/src-web/components/core/HotKey.tsx +++ b/src-web/components/core/HotKey.tsx @@ -5,9 +5,11 @@ import { useOsInfo } from '../../hooks/useOsInfo'; interface Props { action: HotkeyAction | null; + className?: string; + variant?: 'text' | 'with-bg'; } -export function HotKey({ action }: Props) { +export function HotKey({ action, className, variant }: Props) { const osInfo = useOsInfo(); const label = useFormattedHotkey(action); if (label === null || osInfo == null) { @@ -15,6 +17,14 @@ export function HotKey({ action }: Props) { } return ( - {label} + + {label} + ); } diff --git a/src-web/components/core/HotKeyList.tsx b/src-web/components/core/HotKeyList.tsx new file mode 100644 index 00000000..d0170e81 --- /dev/null +++ b/src-web/components/core/HotKeyList.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import type { HotkeyAction } from '../../hooks/useHotkey'; +import { HotKey } from './HotKey'; + +interface Props { + hotkeys: { action: HotkeyAction; label: string }[]; +} + +export const HotKeyList = ({ hotkeys }: Props) => { + return ( +
+
+ {hotkeys.map((hotkey) => ( +
+

{hotkey.label}

+
+ +
+
+ ))} +
+
+ ); +}; diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index ce43843c..9e320857 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -8,6 +8,7 @@ import { ReactComponent as LeftPanelVisibleIcon } from '../../assets/icons/LeftP const icons = { archive: ReactIcons.ArchiveIcon, camera: ReactIcons.CameraIcon, + chat: ReactIcons.ChatBubbleIcon, check: ReactIcons.CheckIcon, checkbox: ReactIcons.CheckboxIcon, clock: ReactIcons.ClockIcon, diff --git a/src-web/hooks/useHotkey.ts b/src-web/hooks/useHotkey.ts index cef32974..7ebd1a99 100644 --- a/src-web/hooks/useHotkey.ts +++ b/src-web/hooks/useHotkey.ts @@ -22,15 +22,26 @@ const hotkeys: Record = { 'environmentEditor.toggle': ['CmdCtrl+e'], }; -export function useHotkey(action: HotkeyAction | null, callback: (e: KeyboardEvent) => void) { +interface Options { + enable?: boolean; +} + +export function useHotkey( + action: HotkeyAction | null, + callback: (e: KeyboardEvent) => void, + options: Options = {}, +) { useAnyHotkey((hkAction, e) => { if (hkAction === action) { callback(e); } - }); + }, options); } -export function useAnyHotkey(callback: (action: HotkeyAction, e: KeyboardEvent) => void) { +export function useAnyHotkey( + callback: (action: HotkeyAction, e: KeyboardEvent) => void, + options: Options, +) { const currentKeys = useRef>(new Set()); const callbackRef = useRef(callback); const osInfo = useOsInfo(); @@ -45,6 +56,10 @@ export function useAnyHotkey(callback: (action: HotkeyAction, e: KeyboardEvent) const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 1000); const down = (e: KeyboardEvent) => { + if (options.enable === false) { + return; + } + currentKeys.current.add(normalizeKey(e.key, os)); for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) { @@ -64,6 +79,9 @@ export function useAnyHotkey(callback: (action: HotkeyAction, e: KeyboardEvent) clearCurrentKeys(); }; const up = (e: KeyboardEvent) => { + if (options.enable === false) { + return; + } currentKeys.current.delete(normalizeKey(e.key, os)); }; window.addEventListener('keydown', down);