mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:28:35 +02:00
Show hotkeys on empty views
This commit is contained in:
@@ -35,7 +35,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
|||||||
});
|
});
|
||||||
}, [dialog, activeEnvironment]);
|
}, [dialog, activeEnvironment]);
|
||||||
|
|
||||||
useHotkey('environmentEditor.toggle', showEnvironmentDialog);
|
useHotkey('environmentEditor.toggle', showEnvironmentDialog, { enable: environments.length > 0 });
|
||||||
|
|
||||||
const items: DropdownItem[] = useMemo(
|
const items: DropdownItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import classNames from 'classnames';
|
|||||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocalStorage } from 'react-use';
|
import { useLocalStorage } from 'react-use';
|
||||||
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||||
|
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||||
|
import { useRequests } from '../hooks/useRequests';
|
||||||
import { clamp } from '../lib/clamp';
|
import { clamp } from '../lib/clamp';
|
||||||
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
import { RequestPane } from './RequestPane';
|
import { RequestPane } from './RequestPane';
|
||||||
import { ResizeHandle } from './ResizeHandle';
|
import { ResizeHandle } from './ResizeHandle';
|
||||||
import { ResponsePane } from './ResponsePane';
|
import { ResponsePane } from './ResponsePane';
|
||||||
@@ -24,6 +28,9 @@ const STACK_VERTICAL_WIDTH = 600;
|
|||||||
|
|
||||||
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeRequest = useActiveRequest();
|
||||||
|
const createRequest = useCreateRequest();
|
||||||
|
const requests = useRequests();
|
||||||
const [vertical, setVertical] = useState<boolean>(false);
|
const [vertical, setVertical] = useState<boolean>(false);
|
||||||
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
|
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
|
||||||
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
|
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
|
||||||
@@ -114,6 +121,24 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
|||||||
[width, height, vertical, setHeight, setWidth],
|
[width, height, vertical, setHeight, setWidth],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (activeRequest === null && requests.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex items-center justify-center opacity-disabled">
|
||||||
|
<p>No Selected Request</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (requests.length === 0) {
|
||||||
|
return (
|
||||||
|
<HotKeyList
|
||||||
|
hotkeys={[
|
||||||
|
{ action: 'request.create', label: 'New Request' },
|
||||||
|
{ action: 'sidebar.toggle', label: 'Toggle Sidebar' },
|
||||||
|
{ action: 'urlBar.focus', label: 'Focus URL' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="grid gap-1.5 w-full h-full p-3" style={styles}>
|
<div ref={containerRef} className="grid gap-1.5 w-full h-full p-3" style={styles}>
|
||||||
<RequestPane style={rqst} fullHeight={!vertical} />
|
<RequestPane style={rqst} fullHeight={!vertical} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { createGlobalState } from 'react-use';
|
import { createGlobalState } from 'react-use';
|
||||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||||
import { useResponses } from '../hooks/useResponses';
|
import { useResponses } from '../hooks/useResponses';
|
||||||
@@ -12,6 +12,7 @@ import { isResponseLoading } from '../lib/models';
|
|||||||
import { Banner } from './core/Banner';
|
import { Banner } from './core/Banner';
|
||||||
import { CountBadge } from './core/CountBadge';
|
import { CountBadge } from './core/CountBadge';
|
||||||
import { DurationTag } from './core/DurationTag';
|
import { DurationTag } from './core/DurationTag';
|
||||||
|
import { HotKeyList } from './core/HotKeyList';
|
||||||
import { SizeTag } from './core/SizeTag';
|
import { SizeTag } from './core/SizeTag';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
import { StatusTag } from './core/StatusTag';
|
import { StatusTag } from './core/StatusTag';
|
||||||
@@ -34,9 +35,9 @@ const useActiveTab = createGlobalState<string>('body');
|
|||||||
|
|
||||||
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
|
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
|
||||||
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
|
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
|
||||||
const activeRequestId = useActiveRequestId();
|
const activeRequest = useActiveRequest();
|
||||||
const latestResponse = useLatestResponse(activeRequestId);
|
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
|
||||||
const responses = useResponses(activeRequestId);
|
const responses = useResponses(activeRequest?.id ?? null);
|
||||||
const activeResponse: HttpResponse | null = pinnedResponseId
|
const activeResponse: HttpResponse | null = pinnedResponseId
|
||||||
? responses.find((r) => r.id === pinnedResponseId) ?? null
|
? responses.find((r) => r.id === pinnedResponseId) ?? null
|
||||||
: latestResponse ?? null;
|
: latestResponse ?? null;
|
||||||
@@ -84,6 +85,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (activeRequest === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
@@ -95,6 +100,19 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
|
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
|
||||||
|
{!activeResponse && (
|
||||||
|
<>
|
||||||
|
<span />
|
||||||
|
<HotKeyList
|
||||||
|
hotkeys={[
|
||||||
|
{ action: 'request.send', label: 'Send Request' },
|
||||||
|
{ action: 'request.create', label: 'New Request' },
|
||||||
|
{ action: 'sidebar.toggle', label: 'Toggle Sidebar' },
|
||||||
|
{ action: 'urlBar.focus', label: 'Focus URL' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
|
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
|
||||||
<>
|
<>
|
||||||
<HStack
|
<HStack
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke, shell } from '@tauri-apps/api';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useAppVersion } from '../hooks/useAppVersion';
|
import { useAppVersion } from '../hooks/useAppVersion';
|
||||||
import { useExportData } from '../hooks/useExportData';
|
import { useExportData } from '../hooks/useExportData';
|
||||||
@@ -79,6 +79,12 @@ export function SettingsDropdown() {
|
|||||||
onSelect: () => invoke('check_for_updates'),
|
onSelect: () => invoke('check_for_updates'),
|
||||||
leftSlot: <Icon icon="update" />,
|
leftSlot: <Icon icon="update" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'feedback',
|
||||||
|
label: 'Feedback',
|
||||||
|
onSelect: () => shell.open('https://yaak.canny.io'),
|
||||||
|
leftSlot: <Icon icon="chat" />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<IconButton size="sm" title="Request Options" icon="gear" className="pointer-events-auto" />
|
<IconButton size="sm" title="Request Options" icon="gear" className="pointer-events-auto" />
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { useOsInfo } from '../../hooks/useOsInfo';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
action: HotkeyAction | null;
|
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 osInfo = useOsInfo();
|
||||||
const label = useFormattedHotkey(action);
|
const label = useFormattedHotkey(action);
|
||||||
if (label === null || osInfo == null) {
|
if (label === null || osInfo == null) {
|
||||||
@@ -15,6 +17,14 @@ export function HotKey({ action }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={classNames('text-sm text-gray-1000 text-opacity-disabled')}>{label}</span>
|
<span
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
variant === 'with-bg' && 'rounded border',
|
||||||
|
'text-sm text-gray-1000 text-opacity-disabled',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
24
src-web/components/core/HotKeyList.tsx
Normal file
24
src-web/components/core/HotKeyList.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="mx-auto h-full flex items-center text-gray-800 text-sm">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{hotkeys.map((hotkey) => (
|
||||||
|
<div key={hotkey.action} className="grid grid-cols-2">
|
||||||
|
<p>{hotkey.label}</p>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<HotKey action={hotkey.action} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import { ReactComponent as LeftPanelVisibleIcon } from '../../assets/icons/LeftP
|
|||||||
const icons = {
|
const icons = {
|
||||||
archive: ReactIcons.ArchiveIcon,
|
archive: ReactIcons.ArchiveIcon,
|
||||||
camera: ReactIcons.CameraIcon,
|
camera: ReactIcons.CameraIcon,
|
||||||
|
chat: ReactIcons.ChatBubbleIcon,
|
||||||
check: ReactIcons.CheckIcon,
|
check: ReactIcons.CheckIcon,
|
||||||
checkbox: ReactIcons.CheckboxIcon,
|
checkbox: ReactIcons.CheckboxIcon,
|
||||||
clock: ReactIcons.ClockIcon,
|
clock: ReactIcons.ClockIcon,
|
||||||
|
|||||||
@@ -22,15 +22,26 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
|||||||
'environmentEditor.toggle': ['CmdCtrl+e'],
|
'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) => {
|
useAnyHotkey((hkAction, e) => {
|
||||||
if (hkAction === action) {
|
if (hkAction === action) {
|
||||||
callback(e);
|
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<Set<string>>(new Set());
|
const currentKeys = useRef<Set<string>>(new Set());
|
||||||
const callbackRef = useRef(callback);
|
const callbackRef = useRef(callback);
|
||||||
const osInfo = useOsInfo();
|
const osInfo = useOsInfo();
|
||||||
@@ -45,6 +56,10 @@ export function useAnyHotkey(callback: (action: HotkeyAction, e: KeyboardEvent)
|
|||||||
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 1000);
|
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 1000);
|
||||||
|
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
|
if (options.enable === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
currentKeys.current.add(normalizeKey(e.key, os));
|
currentKeys.current.add(normalizeKey(e.key, os));
|
||||||
|
|
||||||
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||||
@@ -64,6 +79,9 @@ export function useAnyHotkey(callback: (action: HotkeyAction, e: KeyboardEvent)
|
|||||||
clearCurrentKeys();
|
clearCurrentKeys();
|
||||||
};
|
};
|
||||||
const up = (e: KeyboardEvent) => {
|
const up = (e: KeyboardEvent) => {
|
||||||
|
if (options.enable === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentKeys.current.delete(normalizeKey(e.key, os));
|
currentKeys.current.delete(normalizeKey(e.key, os));
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', down);
|
window.addEventListener('keydown', down);
|
||||||
|
|||||||
Reference in New Issue
Block a user