From bf1fdf0e024db6cff086fb5f6f488365d277cc65 Mon Sep 17 00:00:00 2001 From: Dhruvil Date: Wed, 5 Nov 2025 23:10:51 +0530 Subject: [PATCH 1/2] feat: Add cURL Import Option to Create Dropdown Menu --- src-web/components/ImportCurlDialog.tsx | 112 +++++++++++++++++++++++ src-web/hooks/useCreateDropdownItems.tsx | 13 +++ 2 files changed, 125 insertions(+) create mode 100644 src-web/components/ImportCurlDialog.tsx diff --git a/src-web/components/ImportCurlDialog.tsx b/src-web/components/ImportCurlDialog.tsx new file mode 100644 index 000000000..b64deb3ed --- /dev/null +++ b/src-web/components/ImportCurlDialog.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import { useImportCurl } from '../hooks/useImportCurl'; +import { Banner } from './core/Banner'; +import { Button } from './core/Button'; +import { Editor } from './core/Editor/LazyEditor'; +import { Icon } from './core/Icon'; +import { HStack, VStack } from './core/Stacks'; + +interface Props { + hide: () => void; +} + +const EXAMPLE_CURL = `curl https://api.example.com/users \\ + -H 'Authorization: Bearer token123' \\ + -H 'Content-Type: application/json' \\ + -d '{"name": "John Doe"}'`; + +export function ImportCurlDialog({ hide }: Props) { + const [curlCommand, setCurlCommand] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { mutate: importCurl } = useImportCurl(); + + const handleImport = async () => { + if (!curlCommand.trim()) { + return; + } + + // Basic validation + if (!curlCommand.trim().toLowerCase().startsWith('curl')) { + setError('Please paste a valid cURL command starting with "curl"'); + return; + } + + setError(null); + setIsLoading(true); + try { + await importCurl({ command: curlCommand }); + hide(); + } catch (error) { + console.error('Failed to import cURL:', error); + setError('Failed to import cURL command. Please check the format and try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( + + {/* Info Banner */} + + +
+ +
+
Paste your cURL command below
+
+ The command will be converted into a new HTTP request with all headers, body, and + parameters preserved. +
+
+
+
+
+ + {/* Editor Section */} + +
cURL Command
+
+ { + setCurlCommand(value); + if (error) setError(null); + }} + defaultValue={curlCommand} + stateKey="import-curl-dialog" + /> +
+
+ + {/* Error Message */} + {error && ( + +
+ +
{error}
+
+
+ )} + + {/* Action Buttons */} + + + + +
+ ); +} diff --git a/src-web/hooks/useCreateDropdownItems.tsx b/src-web/hooks/useCreateDropdownItems.tsx index 04776a550..db51106ab 100644 --- a/src-web/hooks/useCreateDropdownItems.tsx +++ b/src-web/hooks/useCreateDropdownItems.tsx @@ -5,7 +5,9 @@ import { useMemo } from 'react'; import { createFolder } from '../commands/commands'; import type { DropdownItem } from '../components/core/Dropdown'; import { Icon } from '../components/core/Icon'; +import { ImportCurlDialog } from '../components/ImportCurlDialog'; import { createRequestAndNavigate } from '../lib/createRequestAndNavigate'; +import { showDialog } from '../lib/dialog'; import { generateId } from '../lib/generateId'; import { BODY_TYPE_GRAPHQL } from '../lib/model_util'; import { activeRequestAtom } from './useActiveRequest'; @@ -64,6 +66,17 @@ export function getCreateDropdownItems({ onCreate?.('http_request', id); }, }, + { + label: 'cURL', + leftSlot: hideIcons ? undefined : , + onSelect: () => + showDialog({ + id: 'import-curl', + title: 'Import cURL Command', + size: 'md', + render: ImportCurlDialog, + }), + }, { label: 'GraphQL', leftSlot: hideIcons ? undefined : , From f0e469947085031a38a94bc04fe9f5c84e86e76c Mon Sep 17 00:00:00 2001 From: Dhruvil Date: Fri, 28 Nov 2025 22:37:24 +0530 Subject: [PATCH 2/2] added cURL option somewhere else --- src-web/components/ImportCurlDialog.tsx | 16 ++++++++++- src-web/components/Sidebar.tsx | 36 ++++++++++++++++++++++-- src-web/hooks/useCreateDropdownItems.tsx | 13 --------- src-web/hooks/useImportCurl.ts | 10 ++++++- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src-web/components/ImportCurlDialog.tsx b/src-web/components/ImportCurlDialog.tsx index b64deb3ed..cc75e1ac7 100644 --- a/src-web/components/ImportCurlDialog.tsx +++ b/src-web/components/ImportCurlDialog.tsx @@ -4,6 +4,7 @@ import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { Editor } from './core/Editor/LazyEditor'; import { Icon } from './core/Icon'; +import { PlainInput } from './core/PlainInput'; import { HStack, VStack } from './core/Stacks'; interface Props { @@ -17,6 +18,7 @@ const EXAMPLE_CURL = `curl https://api.example.com/users \\ export function ImportCurlDialog({ hide }: Props) { const [curlCommand, setCurlCommand] = useState(''); + const [requestName, setRequestName] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const { mutate: importCurl } = useImportCurl(); @@ -35,7 +37,7 @@ export function ImportCurlDialog({ hide }: Props) { setError(null); setIsLoading(true); try { - await importCurl({ command: curlCommand }); + await importCurl({ command: curlCommand, name: requestName }); hide(); } catch (error) { console.error('Failed to import cURL:', error); @@ -63,6 +65,18 @@ export function ImportCurlDialog({ hide }: Props) { + {/* Request Name (Optional) */} + +
Name (optional)
+ setRequestName(value)} + /> +
+ {/* Editor Section */}
cURL Command
diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 2c0d5591e..3645c33d2 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -43,6 +43,7 @@ import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { deepEqualAtom } from '../lib/atoms'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; +import { showDialog } from '../lib/dialog'; import { jotaiStore } from '../lib/jotai'; import { resolvedModelName } from '../lib/resolvedModelName'; import { isSidebarFocused } from '../lib/scopes'; @@ -67,6 +68,7 @@ import type { TreeHandle, TreeProps } from './core/tree/Tree'; import { Tree } from './core/tree/Tree'; import type { TreeItemProps } from './core/tree/TreeItem'; import { GitDropdown } from './git/GitDropdown'; +import { ImportCurlDialog } from './ImportCurlDialog'; type SidebarModel = Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest; function isSidebarLeafModel(m: AnyModel): boolean { @@ -457,6 +459,25 @@ function Sidebar({ className }: { className?: string }) { }); }, [allFields]); + // Subscribe to activeIdAtom changes to auto-select new requests in the sidebar + useEffect(() => { + return jotaiStore.sub(activeIdAtom, () => { + const activeId = jotaiStore.get(activeIdAtom); + if (activeId != null && treeRef.current != null) { + treeRef.current.selectItem(activeId); + } + }); + }, []); + + const handleImportCurl = useCallback(() => { + showDialog({ + id: 'import-curl', + title: 'Import cURL Command', + size: 'md', + render: ImportCurlDialog, + }); + }, []); + if (tree == null || hidden) { return null; } @@ -467,9 +488,18 @@ function Sidebar({ className }: { className?: string }) { aria-hidden={hidden ?? undefined} className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)_auto]')} > -
+
+ + Import + {(tree.children?.length ?? 0) > 0 && ( - <> +
- +
)}
{allHidden ? ( diff --git a/src-web/hooks/useCreateDropdownItems.tsx b/src-web/hooks/useCreateDropdownItems.tsx index db51106ab..04776a550 100644 --- a/src-web/hooks/useCreateDropdownItems.tsx +++ b/src-web/hooks/useCreateDropdownItems.tsx @@ -5,9 +5,7 @@ import { useMemo } from 'react'; import { createFolder } from '../commands/commands'; import type { DropdownItem } from '../components/core/Dropdown'; import { Icon } from '../components/core/Icon'; -import { ImportCurlDialog } from '../components/ImportCurlDialog'; import { createRequestAndNavigate } from '../lib/createRequestAndNavigate'; -import { showDialog } from '../lib/dialog'; import { generateId } from '../lib/generateId'; import { BODY_TYPE_GRAPHQL } from '../lib/model_util'; import { activeRequestAtom } from './useActiveRequest'; @@ -66,17 +64,6 @@ export function getCreateDropdownItems({ onCreate?.('http_request', id); }, }, - { - label: 'cURL', - leftSlot: hideIcons ? undefined : , - onSelect: () => - showDialog({ - id: 'import-curl', - title: 'Import cURL Command', - size: 'md', - render: ImportCurlDialog, - }), - }, { label: 'GraphQL', leftSlot: hideIcons ? undefined : , diff --git a/src-web/hooks/useImportCurl.ts b/src-web/hooks/useImportCurl.ts index f79e2a77e..51fc94eb8 100644 --- a/src-web/hooks/useImportCurl.ts +++ b/src-web/hooks/useImportCurl.ts @@ -14,9 +14,11 @@ export function useImportCurl() { mutationFn: async ({ overwriteRequestId, command, + name, }: { overwriteRequestId?: string; command: string; + name?: string; }) => { const workspaceId = jotaiStore.get(activeWorkspaceIdAtom); const importedRequest: HttpRequest = await invokeCmd('cmd_curl_to_request', { @@ -24,10 +26,16 @@ export function useImportCurl() { workspaceId, }); + // Apply custom name if provided + const requestToCreate = { + ...importedRequest, + name: name?.trim() || importedRequest.name, + }; + let verb: string; if (overwriteRequestId == null) { verb = 'Created'; - await createRequestAndNavigate(importedRequest); + await createRequestAndNavigate(requestToCreate); } else { verb = 'Updated'; await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({