mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-20 15:51:23 +02:00
Many hotkey improvements
This commit is contained in:
@@ -20,7 +20,7 @@ interface Actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const DialogContext = createContext<State>({} as any);
|
const DialogContext = createContext<State>({} as State);
|
||||||
|
|
||||||
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
|
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
|
||||||
|
|||||||
@@ -119,6 +119,11 @@ export function GrpcConnectionSetupPane({
|
|||||||
onGo();
|
onGo();
|
||||||
}, [activeRequest, onGo]);
|
}, [activeRequest, onGo]);
|
||||||
|
|
||||||
|
const handleSend = useCallback(async () => {
|
||||||
|
if (activeRequest == null) return;
|
||||||
|
onSend({ message: activeRequest.message });
|
||||||
|
}, [activeRequest, onGo]);
|
||||||
|
|
||||||
const tabs: TabItem[] = useMemo(
|
const tabs: TabItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: 'message', label: 'Message' },
|
{ value: 'message', label: 'Message' },
|
||||||
@@ -212,52 +217,52 @@ export function GrpcConnectionSetupPane({
|
|||||||
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
|
{select.options.find((o) => o.value === select.value)?.label ?? 'No Schema'}
|
||||||
</Button>
|
</Button>
|
||||||
</RadioDropdown>
|
</RadioDropdown>
|
||||||
{!isStreaming && (
|
{methodType === 'client_streaming' || methodType === 'streaming' ? (
|
||||||
|
<>
|
||||||
|
{isStreaming && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
className="border border-highlight"
|
||||||
|
size="sm"
|
||||||
|
title="Cancel"
|
||||||
|
onClick={onCancel}
|
||||||
|
icon="x"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
className="border border-highlight"
|
||||||
|
size="sm"
|
||||||
|
title="Commit"
|
||||||
|
onClick={onCommit}
|
||||||
|
icon="check"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
className="border border-highlight"
|
||||||
|
size="sm"
|
||||||
|
title={isStreaming ? 'Connect' : 'Send'}
|
||||||
|
hotkeyAction="grpc_request.send"
|
||||||
|
onClick={isStreaming ? handleSend : handleConnect}
|
||||||
|
icon={isStreaming ? 'sendHorizontal' : 'arrowUpDown'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
className="border border-highlight"
|
className="border border-highlight"
|
||||||
size="sm"
|
size="sm"
|
||||||
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
title={methodType === 'unary' ? 'Send' : 'Connect'}
|
||||||
hotkeyAction="grpc_request.send"
|
hotkeyAction="grpc_request.send"
|
||||||
onClick={handleConnect}
|
onClick={isStreaming ? onCancel : handleConnect}
|
||||||
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
disabled={methodType === 'no-schema' || methodType === 'no-method'}
|
||||||
icon={
|
icon={
|
||||||
isStreaming
|
isStreaming
|
||||||
? 'refresh'
|
? 'x'
|
||||||
: methodType.includes('streaming')
|
: methodType.includes('streaming')
|
||||||
? 'arrowUpDown'
|
? 'arrowUpDown'
|
||||||
: 'sendHorizontal'
|
: 'sendHorizontal'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isStreaming && (
|
|
||||||
<IconButton
|
|
||||||
className="border border-highlight"
|
|
||||||
size="sm"
|
|
||||||
title="Cancel"
|
|
||||||
onClick={onCancel}
|
|
||||||
icon="x"
|
|
||||||
disabled={!isStreaming}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
className="border border-highlight"
|
|
||||||
size="sm"
|
|
||||||
title="to-do"
|
|
||||||
onClick={onCommit}
|
|
||||||
icon="check"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
className="border border-highlight"
|
|
||||||
size="sm"
|
|
||||||
title="to-do"
|
|
||||||
hotkeyAction="grpc_request.send"
|
|
||||||
onClick={() => onSend({ message: activeRequest.message ?? '' })}
|
|
||||||
icon="sendHorizontal"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -90,13 +90,17 @@ export function Sidebar({ className }: Props) {
|
|||||||
namespace: NAMESPACE_NO_SYNC,
|
namespace: NAMESPACE_NO_SYNC,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotKey('http_request.duplicate', async () => {
|
useHotKey(
|
||||||
if (activeRequest?.model === 'http_request') {
|
'http_request.duplicate',
|
||||||
await duplicateHttpRequest.mutateAsync();
|
async () => {
|
||||||
} else {
|
if (activeRequest?.model === 'http_request') {
|
||||||
await duplicateGrpcRequest.mutateAsync();
|
await duplicateHttpRequest.mutateAsync();
|
||||||
}
|
} else {
|
||||||
});
|
await duplicateGrpcRequest.mutateAsync();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ enable: !hidden },
|
||||||
|
);
|
||||||
|
|
||||||
const isCollapsed = useCallback(
|
const isCollapsed = useCallback(
|
||||||
(id: string) => collapsed.value?.[id] ?? false,
|
(id: string) => collapsed.value?.[id] ?? false,
|
||||||
@@ -242,25 +246,29 @@ export function Sidebar({ className }: Props) {
|
|||||||
useKeyPressEvent('Backspace', handleDeleteKey);
|
useKeyPressEvent('Backspace', handleDeleteKey);
|
||||||
useKeyPressEvent('Delete', handleDeleteKey);
|
useKeyPressEvent('Delete', handleDeleteKey);
|
||||||
|
|
||||||
useHotKey('sidebar.focus', async () => {
|
useHotKey(
|
||||||
// Hide the sidebar if it's already focused
|
'sidebar.focus',
|
||||||
if (!hidden && hasFocus) {
|
async () => {
|
||||||
await hide();
|
// Hide the sidebar if it's already focused
|
||||||
return;
|
if (!hidden && hasFocus) {
|
||||||
}
|
await hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show the sidebar if it's hidden
|
// Show the sidebar if it's hidden
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
await show();
|
await show();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select 0 index on focus if none selected
|
// Select 0 index on focus if none selected
|
||||||
focusActiveRequest(
|
focusActiveRequest(
|
||||||
selectedTree != null && selectedId != null
|
selectedTree != null && selectedId != null
|
||||||
? { forced: { id: selectedId, tree: selectedTree } }
|
? { forced: { id: selectedId, tree: selectedTree } }
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
{ enable: !hidden },
|
||||||
|
);
|
||||||
|
|
||||||
useKeyPressEvent('Enter', (e) => {
|
useKeyPressEvent('Enter', (e) => {
|
||||||
if (!hasFocus) return;
|
if (!hasFocus) return;
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { trackEvent } from '../lib/analytics';
|
import { trackEvent } from '../lib/analytics';
|
||||||
import type { Folder } from '../lib/models';
|
import type { Folder } from '../lib/models';
|
||||||
|
import { useActiveRequest } from './useActiveRequest';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { foldersQueryKey } from './useFolders';
|
import { foldersQueryKey } from './useFolders';
|
||||||
import { usePrompt } from './usePrompt';
|
import { usePrompt } from './usePrompt';
|
||||||
|
|
||||||
export function useCreateFolder() {
|
export function useCreateFolder() {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
|
const activeRequest = useActiveRequest();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ export function useCreateFolder() {
|
|||||||
placeholder: 'Name',
|
placeholder: 'Name',
|
||||||
}));
|
}));
|
||||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||||
|
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||||
return invoke('cmd_create_folder', { workspaceId, ...patch });
|
return invoke('cmd_create_folder', { workspaceId, ...patch });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('folder', 'create'),
|
onSettled: () => trackEvent('folder', 'create'),
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { invoke } from '@tauri-apps/api';
|
|||||||
import { trackEvent } from '../lib/analytics';
|
import { trackEvent } from '../lib/analytics';
|
||||||
import type { GrpcRequest } from '../lib/models';
|
import type { GrpcRequest } from '../lib/models';
|
||||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
|
import { useActiveRequest } from './useActiveRequest';
|
||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useAppRoutes } from './useAppRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
|
|
||||||
export function useCreateGrpcRequest() {
|
export function useCreateGrpcRequest() {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
const activeEnvironmentId = useActiveEnvironmentId();
|
const activeEnvironmentId = useActiveEnvironmentId();
|
||||||
const activeRequest = null;
|
const activeRequest = useActiveRequest();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<
|
||||||
@@ -24,13 +25,13 @@ export function useCreateGrpcRequest() {
|
|||||||
if (patch.sortPriority === undefined) {
|
if (patch.sortPriority === undefined) {
|
||||||
if (activeRequest != null) {
|
if (activeRequest != null) {
|
||||||
// Place above currently-active request
|
// Place above currently-active request
|
||||||
// patch.sortPriority = activeRequest.sortPriority + 0.0001;
|
patch.sortPriority = activeRequest.sortPriority + 0.0001;
|
||||||
} else {
|
} else {
|
||||||
// Place at the very top
|
// Place at the very top
|
||||||
patch.sortPriority = -Date.now();
|
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 });
|
return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch });
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('grpc_request', 'create'),
|
onSettled: () => trackEvent('grpc_request', 'create'),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function useGrpc(
|
|||||||
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 1000);
|
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 500);
|
||||||
const reflect = useQuery<ReflectResponseService[], string>({
|
const reflect = useQuery<ReflectResponseService[], string>({
|
||||||
enabled: req != null,
|
enabled: req != null,
|
||||||
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl],
|
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl],
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { capitalize } from '../lib/capitalize';
|
|||||||
import { debounce } from '../lib/debounce';
|
import { debounce } from '../lib/debounce';
|
||||||
import { useOsInfo } from './useOsInfo';
|
import { useOsInfo } from './useOsInfo';
|
||||||
|
|
||||||
|
const HOLD_KEYS = ['Shift', 'CmdCtrl', 'Alt', 'Meta'];
|
||||||
|
|
||||||
export type HotkeyAction =
|
export type HotkeyAction =
|
||||||
| 'popup.close'
|
| 'popup.close'
|
||||||
| 'environmentEditor.toggle'
|
| 'environmentEditor.toggle'
|
||||||
@@ -61,21 +63,6 @@ export function useHotKey(
|
|||||||
action: HotkeyAction | null,
|
action: HotkeyAction | null,
|
||||||
callback: (e: KeyboardEvent) => void,
|
callback: (e: KeyboardEvent) => void,
|
||||||
options: Options = {},
|
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<Set<string>>(new Set());
|
const currentKeys = useRef<Set<string>>(new Set());
|
||||||
const callbackRef = useRef(callback);
|
const callbackRef = useRef(callback);
|
||||||
@@ -87,7 +74,7 @@ function useAnyHotkey(
|
|||||||
}, [callback]);
|
}, [callback]);
|
||||||
|
|
||||||
useEffect(() => {
|
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 clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000);
|
||||||
|
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
@@ -95,26 +82,41 @@ function useAnyHotkey(
|
|||||||
return;
|
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 [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
||||||
for (const hkKey of hkKeys) {
|
for (const hkKey of hkKeys) {
|
||||||
const keys = hkKey.split('+');
|
const keys = hkKey.split('+');
|
||||||
if (
|
if (
|
||||||
keys.length === currentKeys.current.size &&
|
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();
|
clearCurrentKeys();
|
||||||
};
|
};
|
||||||
const up = (e: KeyboardEvent) => {
|
const up = (e: KeyboardEvent) => {
|
||||||
if (options.enable === false) {
|
if (options.enable === false) {
|
||||||
return;
|
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('keydown', down, { capture: true });
|
||||||
document.addEventListener('keyup', up, { capture: true });
|
document.addEventListener('keyup', up, { capture: true });
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ if (osType !== 'Darwin') {
|
|||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
setAppearanceOnDocument(settings.appearance as Appearance);
|
setAppearanceOnDocument(settings.appearance as Appearance);
|
||||||
|
|
||||||
window.addEventListener('keypress', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
// Don't go back in history on backspace
|
// Hack to not go back in history on backspace. Check for document body
|
||||||
if (e.key === 'Backspace') e.preventDefault();
|
// 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(
|
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
|||||||
Reference in New Issue
Block a user