From 77bb723b291992c2ee6c41c5af354c9308f882ec Mon Sep 17 00:00:00 2001 From: Andrew Melchor Date: Thu, 26 Feb 2026 19:51:31 -0800 Subject: [PATCH 1/2] feat(desktop): add curl import dialog to explorer Adds a modal dialog to the desktop explorer allowing users to paste curl commands and import them as HTTP request files. Features a preview/apply workflow, configurable conflict policies (fail, skip, overwrite, rename), variable merging, and comprehensive diagnostics for validation errors. --- .../explorer/components/CurlImportDialog.tsx | 487 ++++++++++++++++++ .../explorer/components/ExplorerScreen.tsx | 420 ++++++++++++++- .../components/ExplorerSidebarPanel.tsx | 2 + .../explorer/components/ExplorerToolbar.tsx | 13 +- .../features/explorer/components/icons.tsx | 10 + .../explorer/utils/curl-import.test.ts | 260 ++++++++++ .../features/explorer/utils/curl-import.ts | 292 +++++++++++ 7 files changed, 1481 insertions(+), 3 deletions(-) create mode 100644 packages/desktop/src/features/explorer/components/CurlImportDialog.tsx create mode 100644 packages/desktop/src/features/explorer/utils/curl-import.test.ts create mode 100644 packages/desktop/src/features/explorer/utils/curl-import.ts diff --git a/packages/desktop/src/features/explorer/components/CurlImportDialog.tsx b/packages/desktop/src/features/explorer/components/CurlImportDialog.tsx new file mode 100644 index 0000000..b9185dd --- /dev/null +++ b/packages/desktop/src/features/explorer/components/CurlImportDialog.tsx @@ -0,0 +1,487 @@ +import { setupDialogFocusTrap } from '@t-req/ui'; +import { createEffect, createMemo, For, onCleanup, Show } from 'solid-js'; +import { Portal } from 'solid-js/web'; +import type { + CurlImportConflictPolicy, + CurlImportDiagnostics, + CurlImportStats, + CurlImportSummary +} from '../utils/curl-import'; + +const CONFLICT_POLICIES: Array<{ + value: CurlImportConflictPolicy; + label: string; + description: string; +}> = [ + { value: 'fail', label: 'Fail', description: 'Stop if any destination file already exists.' }, + { value: 'skip', label: 'Skip', description: 'Skip conflicting files and continue.' }, + { value: 'overwrite', label: 'Overwrite', description: 'Replace conflicting files.' }, + { value: 'rename', label: 'Rename', description: 'Write to a suffixed filename instead.' } +]; + +type CurlImportDialogProps = { + open: boolean; + command: string; + outputDir: string; + onConflict: CurlImportConflictPolicy; + fileName: string; + requestName: string; + mergeVariables: boolean; + force: boolean; + advancedOpen: boolean; + isPreviewing: boolean; + isApplying: boolean; + canApply: boolean; + previewResult: CurlImportSummary | undefined; + previewDiagnostics: CurlImportDiagnostics; + previewStats: CurlImportStats | undefined; + previewDiagnosticsBlocked: boolean; + previewError: string | undefined; + applyResult: + | { + kind: 'success' | 'partial'; + summary: CurlImportSummary; + } + | undefined; + applyError: string | undefined; + onClose: () => void; + onCommandChange: (value: string) => void; + onOutputDirChange: (value: string) => void; + onConflictChange: (value: CurlImportConflictPolicy) => void; + onFileNameChange: (value: string) => void; + onRequestNameChange: (value: string) => void; + onMergeVariablesChange: (checked: boolean) => void; + onForceChange: (checked: boolean) => void; + onToggleAdvanced: () => void; + onPreview: () => void; + onApply: () => void; +}; + +function severityClass(severity: CurlImportDiagnostics[number]['severity']): string { + switch (severity) { + case 'error': + return 'badge badge-error badge-xs font-mono'; + case 'warning': + return 'badge badge-warning badge-xs font-mono'; + default: + return 'badge badge-info badge-xs font-mono'; + } +} + +function SummaryPaths(props: { title: string; paths: string[] }) { + return ( + 0}> +
+
+ {props.title} ({props.paths.length}) +
+
    + + {(path) =>
  • {path}
  • } +
    +
+
+
+ ); +} + +function SummaryRenamed(props: { entries: CurlImportSummary['renamed'] }) { + return ( + 0}> +
+
+ Renamed ({props.entries.length}) +
+
    + + {(entry) => ( +
  • + {entry.original} {'->'} {entry.actual} +
  • + )} +
    +
+
+
+ ); +} + +function SummaryFailed(props: { entries: CurlImportSummary['failed'] }) { + return ( + 0}> +
+
+ Failed ({props.entries.length}) +
+
    + + {(entry) => ( +
  • + {entry.path}: {entry.error} +
  • + )} +
    +
+
+
+ ); +} + +export function CurlImportDialog(props: CurlImportDialogProps) { + let dialogRef: HTMLDivElement | undefined; + + const isBusy = createMemo(() => props.isPreviewing || props.isApplying); + const previewDisabled = createMemo(() => isBusy() || props.command.trim().length === 0); + const applyDisabled = createMemo(() => isBusy() || !props.canApply); + const hasPreview = createMemo(() => Boolean(props.previewStats || props.previewResult)); + + createEffect(() => { + if (!props.open || !dialogRef) { + return; + } + + const cleanupFocusTrap = setupDialogFocusTrap(dialogRef, { + onRequestClose: props.onClose + }); + + onCleanup(() => { + cleanupFocusTrap(); + }); + }); + + return ( + + +