Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions src-web/components/ImportCurlDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<string>('');
const [requestName, setRequestName] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(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 (
<VStack space={4} className="h-full">
{/* Info Banner */}
<Banner color="info" className="text-sm">
<VStack space={1.5}>
<div className="flex items-start gap-2">
<Icon icon="info" className="mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium mb-1">Paste your cURL command below</div>
<div className="text-text-subtle">
The command will be converted into a new HTTP request with all headers, body, and
parameters preserved.
</div>
</div>
</div>
</VStack>
</Banner>

{/* Request Name (Optional) */}
<VStack space={2}>
<div className="text-sm font-medium text-text">Name <span className="text-text-subtle font-normal">(optional)</span></div>
<PlainInput
label="Request Name"
hideLabel
placeholder="e.g., Get Users, Create Order"
defaultValue={requestName}
onChange={(value) => setRequestName(value)}
/>
</VStack>

{/* Editor Section */}
<VStack space={2} className="flex-1 min-h-0">
<div className="text-sm font-medium text-text">cURL Command</div>
<div className="flex-1 min-h-[280px] border border-border rounded-md overflow-hidden shadow-sm">
<Editor
heightMode="full"
hideGutter
language="text"
placeholder={EXAMPLE_CURL}
onChange={(value) => {
setCurlCommand(value);
if (error) setError(null);
}}
defaultValue={curlCommand}
stateKey="import-curl-dialog"
/>
</div>
</VStack>

{/* Error Message */}
{error && (
<Banner color="danger" className="text-sm">
<div className="flex items-start gap-2">
<Icon icon="alert_triangle" className="mt-0.5 flex-shrink-0" />
<div>{error}</div>
</div>
</Banner>
)}

{/* Action Buttons */}
<HStack space={2} justifyContent="end" className="pt-2 border-t border-border-subtle">
<Button variant="border" onClick={hide} disabled={isLoading}>
Cancel
</Button>
<Button
color="primary"
onClick={handleImport}
disabled={!curlCommand.trim() || isLoading}
isLoading={isLoading}
leftSlot={!isLoading && <Icon icon="import" />}
>
{isLoading ? 'Importing...' : 'Import Request'}
</Button>
</HStack>
</VStack>
);
}
36 changes: 33 additions & 3 deletions src-web/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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]')}
>
<div className="w-full pl-3 pr-0.5 pt-3 grid grid-cols-[minmax(0,1fr)_auto] items-center">
<div className="w-full px-3 pt-3 flex flex-col gap-2">
<IconButton
size="sm"
icon="import"
className="w-full justify-center"
title="Import cURL command"
onClick={handleImportCurl}
>
Import
</IconButton>
{(tree.children?.length ?? 0) > 0 && (
<>
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center">
<Input
hideLabel
setRef={setFilterRef}
Expand Down Expand Up @@ -544,7 +574,7 @@ function Sidebar({ className }: { className?: string }) {
title="Show sidebar actions menu"
/>
</Dropdown>
</>
</div>
)}
</div>
{allHidden ? (
Expand Down
10 changes: 9 additions & 1 deletion src-web/hooks/useImportCurl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,28 @@ 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', {
command,
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) => ({
Expand Down