mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:13:51 +01:00
Start of command palette
This commit is contained in:
59
src-web/components/CommandPalette.tsx
Normal file
59
src-web/components/CommandPalette.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { Input } from './core/Input';
|
||||
|
||||
export function CommandPalette() {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
||||
const workspaces = useWorkspaces();
|
||||
const requests = useRequests();
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
setSelectedIndex((prev) => prev + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
setSelectedIndex((prev) => prev - 1);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="px-2 py-2 w-full">
|
||||
<Input
|
||||
hideLabel
|
||||
name="command"
|
||||
label="Command"
|
||||
placeholder="Type a command"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full px-1.5 overflow-y-auto">
|
||||
{requests.map((r, i) => (
|
||||
<CommandPaletteItem active={i === selectedIndex} key={r.id}>
|
||||
Switch Request → {fallbackRequestName(r)}
|
||||
</CommandPaletteItem>
|
||||
))}
|
||||
{workspaces.map((w, i) => (
|
||||
<CommandPaletteItem active={i === selectedIndex} key={w.id}>
|
||||
Switch Workspace → {w.name}
|
||||
</CommandPaletteItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandPaletteItem({ children, active }: { children: ReactNode; active: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'h-xs flex items-center rounded px-1.5 text-gray-600',
|
||||
active && 'bg-highlightSecondary text-gray-800',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Dialog } from './core/Dialog';
|
||||
type DialogEntry = {
|
||||
id: string;
|
||||
render: ({ hide }: { hide: () => void }) => React.ReactNode;
|
||||
} & Pick<DialogProps, 'title' | 'description' | 'hideX' | 'className' | 'size' | 'noPadding'>;
|
||||
} & Omit<DialogProps, 'onClose' | 'open' | 'children'>;
|
||||
|
||||
interface State {
|
||||
dialogs: DialogEntry[];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCommandPalette } from '../hooks/useCommandPalette';
|
||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||
import { useGlobalCommands } from '../hooks/useGlobalCommands';
|
||||
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
||||
@@ -35,6 +36,7 @@ export function GlobalHooks() {
|
||||
useSyncAppearance();
|
||||
useSyncWindowTitle();
|
||||
useGlobalCommands();
|
||||
useCommandPalette();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
|
||||
@@ -122,7 +122,7 @@ export function GrpcConnectionSetupPane({
|
||||
const handleSend = useCallback(async () => {
|
||||
if (activeRequest == null) return;
|
||||
onSend({ message: activeRequest.message });
|
||||
}, [activeRequest, onGo]);
|
||||
}, [activeRequest, onSend]);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useGrpcRequests } from '../hooks/useGrpcRequests';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useHttpRequests } from '../hooks/useHttpRequests';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
@@ -21,20 +20,10 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const routes = useAppRoutes();
|
||||
const allRecentRequestIds = useRecentRequests();
|
||||
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
||||
const requests = useMemo(() => [...httpRequests, ...grpcRequests], [httpRequests, grpcRequests]);
|
||||
|
||||
// Toggle the menu on Cmd+k
|
||||
useKey('k', (e) => {
|
||||
if (e.metaKey) {
|
||||
e.preventDefault();
|
||||
dropdownRef.current?.toggle();
|
||||
}
|
||||
});
|
||||
const requests = useRequests();
|
||||
|
||||
// Handle key-up
|
||||
useKeyPressEvent('Control', undefined, () => {
|
||||
@@ -42,16 +31,20 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
dropdownRef.current?.select?.();
|
||||
});
|
||||
|
||||
useHotKey('requestSwitcher.prev', () => {
|
||||
useHotKey('request_switcher.prev', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
|
||||
dropdownRef.current?.next?.();
|
||||
});
|
||||
|
||||
useHotKey('requestSwitcher.next', () => {
|
||||
useHotKey('request_switcher.next', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
|
||||
dropdownRef.current?.prev?.();
|
||||
});
|
||||
|
||||
useHotKey('request_switcher.toggle', () => {
|
||||
dropdownRef.current?.toggle();
|
||||
});
|
||||
|
||||
const items = useMemo<DropdownItem[]>(() => {
|
||||
if (activeWorkspaceId === null) return [];
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ export function RedirectToLatestWorkspace() {
|
||||
const recentWorkspaces = useRecentWorkspaces();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaces.length === 0) {
|
||||
console.log('No workspaces found to redirect to. Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
(async function () {
|
||||
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0];
|
||||
|
||||
@@ -61,6 +61,25 @@ export const RequestPane = memo(function RequestPane({
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
async (contentType: string | null) => {
|
||||
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type');
|
||||
|
||||
if (contentType != null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: contentType,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
await updateRequest.mutateAsync({ headers });
|
||||
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
},
|
||||
[activeRequest.headers, updateRequest],
|
||||
);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -153,7 +172,15 @@ export const RequestPane = memo(function RequestPane({
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeRequest, updateRequest],
|
||||
[
|
||||
activeRequest.authentication,
|
||||
activeRequest.authenticationType,
|
||||
activeRequest.bodyType,
|
||||
activeRequest.headers,
|
||||
activeRequest.urlParameters,
|
||||
handleContentTypeChange,
|
||||
updateRequest,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
@@ -161,24 +188,6 @@ export const RequestPane = memo(function RequestPane({
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
async (contentType: string | null) => {
|
||||
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type');
|
||||
|
||||
if (contentType != null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: contentType,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
await updateRequest.mutateAsync({ headers });
|
||||
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
},
|
||||
[activeRequest.headers, updateRequest],
|
||||
);
|
||||
const handleBinaryFileChange = useCallback(
|
||||
(body: HttpRequest['body']) => {
|
||||
updateRequest.mutate({ body });
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface DialogProps {
|
||||
size?: 'sm' | 'md' | 'lg' | 'full' | 'dynamic';
|
||||
hideX?: boolean;
|
||||
noPadding?: boolean;
|
||||
noScroll?: boolean;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
@@ -29,6 +30,7 @@ export function Dialog({
|
||||
description,
|
||||
hideX,
|
||||
noPadding,
|
||||
noScroll,
|
||||
}: DialogProps) {
|
||||
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
|
||||
const descriptionId = useMemo(
|
||||
@@ -60,7 +62,7 @@ export function Dialog({
|
||||
animate={{ top: 0, scale: 1 }}
|
||||
className={classNames(
|
||||
className,
|
||||
'grid grid-rows-[auto_minmax(0,1fr)]',
|
||||
'h-full grid grid-rows-[auto_auto_minmax(0,1fr)]',
|
||||
'relative bg-gray-50 pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'dark:border border-highlight shadow shadow-black/10',
|
||||
@@ -79,15 +81,20 @@ export function Dialog({
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{description && (
|
||||
|
||||
{description ? (
|
||||
<p className="px-6 text-gray-700" id={descriptionId}>
|
||||
{description}
|
||||
</p>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)] overflow-y-auto',
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)]',
|
||||
!noPadding && 'px-6 py-2',
|
||||
!noScroll && 'overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as app from '@tauri-apps/api/app';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export function useAppInfo() {
|
||||
return useQuery(['appInfo'], async () => {
|
||||
const [version, appDataDir] = await Promise.all([app.getVersion(), path.appDataDir()]);
|
||||
return { version, appDataDir };
|
||||
return (await invoke('cmd_metadata')) as {
|
||||
isDev: boolean;
|
||||
version: string;
|
||||
name: string;
|
||||
appDataDir: string;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
24
src-web/hooks/useCommandPalette.tsx
Normal file
24
src-web/hooks/useCommandPalette.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CommandPalette } from '../components/CommandPalette';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import { useAppInfo } from './useAppInfo';
|
||||
import { useHotKey } from './useHotKey';
|
||||
|
||||
export function useCommandPalette() {
|
||||
const dialog = useDialog();
|
||||
const appInfo = useAppInfo();
|
||||
useHotKey('command_palette.toggle', () => {
|
||||
// Disabled in production for now
|
||||
if (!appInfo.data?.isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.toggle({
|
||||
id: 'command_palette',
|
||||
size: 'md',
|
||||
hideX: true,
|
||||
noPadding: true,
|
||||
noScroll: true,
|
||||
render: () => <CommandPalette />,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -13,12 +13,14 @@ export type HotkeyAction =
|
||||
| 'http_request.create'
|
||||
| 'http_request.duplicate'
|
||||
| 'http_request.send'
|
||||
| 'requestSwitcher.next'
|
||||
| 'requestSwitcher.prev'
|
||||
| 'request_switcher.next'
|
||||
| 'request_switcher.prev'
|
||||
| 'request_switcher.toggle'
|
||||
| 'settings.show'
|
||||
| 'sidebar.focus'
|
||||
| 'sidebar.toggle'
|
||||
| 'urlBar.focus';
|
||||
| 'urlBar.focus'
|
||||
| 'command_palette.toggle';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
|
||||
@@ -27,12 +29,14 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'http_request.create': ['CmdCtrl+n'],
|
||||
'http_request.duplicate': ['CmdCtrl+d'],
|
||||
'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'requestSwitcher.next': ['Control+Shift+Tab'],
|
||||
'requestSwitcher.prev': ['Control+Tab'],
|
||||
'request_switcher.next': ['Control+Shift+Tab'],
|
||||
'request_switcher.prev': ['Control+Tab'],
|
||||
'request_switcher.toggle': ['CmdCtrl+p'],
|
||||
'settings.show': ['CmdCtrl+,'],
|
||||
'sidebar.focus': ['CmdCtrl+1'],
|
||||
'sidebar.toggle': ['CmdCtrl+b'],
|
||||
'urlBar.focus': ['CmdCtrl+l'],
|
||||
'command_palette.toggle': ['CmdCtrl+k'],
|
||||
};
|
||||
|
||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
@@ -42,12 +46,14 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'http_request.create': 'New Request',
|
||||
'http_request.duplicate': 'Duplicate Request',
|
||||
'http_request.send': 'Send Request',
|
||||
'requestSwitcher.next': 'Go To Previous Request',
|
||||
'requestSwitcher.prev': 'Go To Next Request',
|
||||
'request_switcher.next': 'Go To Previous Request',
|
||||
'request_switcher.prev': 'Go To Next Request',
|
||||
'request_switcher.toggle': 'Toggle Request Switcher',
|
||||
'settings.show': 'Open Settings',
|
||||
'sidebar.focus': 'Focus Sidebar',
|
||||
'sidebar.toggle': 'Toggle Sidebar',
|
||||
'urlBar.focus': 'Focus URL',
|
||||
'command_palette.toggle': 'Toggle Command Palette',
|
||||
};
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
@@ -135,7 +141,7 @@ export function useHotKey(
|
||||
document.removeEventListener('keydown', down, { capture: true });
|
||||
document.removeEventListener('keyup', up, { capture: true });
|
||||
};
|
||||
}, [options.enable, os]);
|
||||
}, [action, options.enable, os]);
|
||||
}
|
||||
|
||||
export function useHotKeyLabel(action: HotkeyAction): string {
|
||||
|
||||
Reference in New Issue
Block a user