diff --git a/src-web/components/ImportCurlDialog.tsx b/src-web/components/ImportCurlDialog.tsx new file mode 100644 index 000000000..cc75e1ac7 --- /dev/null +++ b/src-web/components/ImportCurlDialog.tsx @@ -0,0 +1,126 @@ +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 { PlainInput } from './core/PlainInput'; +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 [requestName, setRequestName] = 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, name: requestName }); + 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. +
+
+
+
+
+ + {/* Request Name (Optional) */} + +
Name (optional)
+ setRequestName(value)} + /> +
+ + {/* 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/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/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) => ({