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}) +
+ +
+
+ ); +} + +function SummaryRenamed(props: { entries: CurlImportSummary['renamed'] }) { + return ( + 0}> +
+
+ Renamed ({props.entries.length}) +
+ +
+
+ ); +} + +function SummaryFailed(props: { entries: CurlImportSummary['failed'] }) { + return ( + 0}> +
+
+ Failed ({props.entries.length}) +
+ +
+
+ ); +} + +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 ( + + +