diff --git a/src-tauri/plugins/importer-yaak/index.mjs b/src-tauri/plugins/importer-yaak/index.mjs index 67ab377d..00238af6 100644 --- a/src-tauri/plugins/importer-yaak/index.mjs +++ b/src-tauri/plugins/importer-yaak/index.mjs @@ -5,8 +5,8 @@ function u(r) { } catch { return; } - if (t(e) && "yaakSchema" in e && (e.yaakSchema === 1 && (e.resources.httpRequests = e.resources.requests, e.yaakSchema = 2), e.yaakSchema === 2)) - return { resources: e.resources }; + if (!(!t(e) || !("yaakSchema" in e))) + return "requests" in e.resources && (e.resources.httpRequests = e.resources.requests, delete e.resources.requests), { resources: e.resources }; } function t(r) { return Object.prototype.toString.call(r) === "[object Object]"; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f588d019..a0b3268a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -56,18 +56,6 @@ mod updates; mod window_ext; mod window_menu; -#[derive(serde::Serialize)] -pub struct CustomResponse { - status: u16, - body: String, - url: String, - method: String, - elapsed: u128, - elapsed2: u128, - headers: HashMap, - pub status_reason: Option<&'static str>, -} - async fn migrate_db(app_handle: AppHandle, db: &Mutex>) -> Result<(), String> { let pool = &*db.lock().await; let p = app_handle @@ -82,6 +70,26 @@ async fn migrate_db(app_handle: AppHandle, db: &Mutex>) -> Result<( Ok(()) } +#[derive(serde::Serialize)] +#[serde(default, rename_all = "camelCase")] +struct AppMetaData { + is_dev: bool, + version: String, + name: String, + app_data_dir: String, +} + +#[tauri::command] +async fn cmd_metadata(app_handle: AppHandle) -> Result { + let p = app_handle.path_resolver(); + return Ok(AppMetaData{ + is_dev: is_dev(), + version: app_handle.package_info().version.to_string(), + name: app_handle.package_info().name.to_string(), + app_data_dir: p.app_data_dir().unwrap().to_string_lossy().to_string(), + }) +} + #[tauri::command] async fn cmd_grpc_reflect( request_id: &str, @@ -1442,26 +1450,26 @@ fn main() { cmd_create_grpc_request, cmd_create_http_request, cmd_create_workspace, - cmd_delete_all_http_responses, cmd_delete_all_grpc_connections, + cmd_delete_all_http_responses, cmd_delete_cookie_jar, cmd_delete_environment, cmd_delete_folder, - cmd_delete_grpc_request, cmd_delete_grpc_connection, + cmd_delete_grpc_request, cmd_delete_http_request, cmd_delete_http_response, cmd_delete_workspace, - cmd_duplicate_http_request, cmd_duplicate_grpc_request, + cmd_duplicate_http_request, cmd_export_data, cmd_filter_response, cmd_get_cookie_jar, cmd_get_environment, cmd_get_folder, - cmd_get_key_value, - cmd_get_http_request, cmd_get_grpc_request, + cmd_get_http_request, + cmd_get_key_value, cmd_get_settings, cmd_get_workspace, cmd_grpc_go, @@ -1470,12 +1478,13 @@ fn main() { cmd_list_cookie_jars, cmd_list_environments, cmd_list_folders, - cmd_list_http_requests, - cmd_list_grpc_requests, cmd_list_grpc_connections, cmd_list_grpc_events, + cmd_list_grpc_requests, + cmd_list_http_requests, cmd_list_http_responses, cmd_list_workspaces, + cmd_metadata, cmd_new_window, cmd_send_ephemeral_request, cmd_send_http_request, diff --git a/src-web/components/CommandPalette.tsx b/src-web/components/CommandPalette.tsx new file mode 100644 index 00000000..c62229e9 --- /dev/null +++ b/src-web/components/CommandPalette.tsx @@ -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(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 ( +
+
+ +
+
+ {requests.map((r, i) => ( + + Switch Request → {fallbackRequestName(r)} + + ))} + {workspaces.map((w, i) => ( + + Switch Workspace → {w.name} + + ))} +
+
+ ); +} + +function CommandPaletteItem({ children, active }: { children: ReactNode; active: boolean }) { + return ( +
+ {children} +
+ ); +} diff --git a/src-web/components/DialogContext.tsx b/src-web/components/DialogContext.tsx index 1c2b272d..a323049f 100644 --- a/src-web/components/DialogContext.tsx +++ b/src-web/components/DialogContext.tsx @@ -6,7 +6,7 @@ import { Dialog } from './core/Dialog'; type DialogEntry = { id: string; render: ({ hide }: { hide: () => void }) => React.ReactNode; -} & Pick; +} & Omit; interface State { dialogs: DialogEntry[]; diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index f9991c02..09cae818 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -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); diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx index b607827a..ff28dd88 100644 --- a/src-web/components/GrpcConnectionSetupPane.tsx +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -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( () => [ diff --git a/src-web/components/RecentRequestsDropdown.tsx b/src-web/components/RecentRequestsDropdown.tsx index d1903a89..4d7663f9 100644 --- a/src-web/components/RecentRequestsDropdown.tsx +++ b/src-web/components/RecentRequestsDropdown.tsx @@ -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 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 { + 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(() => { if (activeWorkspaceId === null) return []; diff --git a/src-web/components/RedirectToLatestWorkspace.tsx b/src-web/components/RedirectToLatestWorkspace.tsx index 6b561972..7b63826c 100644 --- a/src-web/components/RedirectToLatestWorkspace.tsx +++ b/src-web/components/RedirectToLatestWorkspace.tsx @@ -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]; diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 7a77bac4..1f3a2eb6 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -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 }); diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index 8aa0ffa5..fa9903b4 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -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({ ) : ( )} - {description && ( + + {description ? (

{description}

+ ) : ( + )} +
{children} diff --git a/src-web/hooks/useAppInfo.ts b/src-web/hooks/useAppInfo.ts index c1a15896..590962f0 100644 --- a/src-web/hooks/useAppInfo.ts +++ b/src-web/hooks/useAppInfo.ts @@ -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; + }; }); } diff --git a/src-web/hooks/useCommandPalette.tsx b/src-web/hooks/useCommandPalette.tsx new file mode 100644 index 00000000..50960cb7 --- /dev/null +++ b/src-web/hooks/useCommandPalette.tsx @@ -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: () => , + }); + }); +} diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts index 86357e96..44ce1d63 100644 --- a/src-web/hooks/useHotKey.ts +++ b/src-web/hooks/useHotKey.ts @@ -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 = { 'environmentEditor.toggle': ['CmdCtrl+Shift+e'], @@ -27,12 +29,14 @@ const hotkeys: Record = { '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 = { @@ -42,12 +46,14 @@ const hotkeyLabels: Record = { '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 {