import { type Folder, type ImportDestination, type ImportPlan, modelTypeLabel, type Workspace, } from "@yaakapp-internal/models"; import { HStack, Icon, VStack } from "@yaakapp-internal/ui"; import { platform } from "@yaakapp-internal/platform"; import classNames from "classnames"; import { useEffect, useRef, useState } from "react"; import { useLocalStorage } from "react-use"; import { pluralizeCount } from "../lib/pluralize"; import { CommercialUseBanner } from "./CommercialUseBanner"; import { Button } from "./core/Button"; import { Checkbox } from "./core/Checkbox"; import { PlainInput } from "./core/PlainInput"; import { RadioCards } from "./core/RadioCards"; interface Props { currentWorkspace: Workspace | null; selectedFolder: Folder | null; planFile: (filePath: string, destination: ImportDestination) => Promise; planUrl: (url: string, destination: ImportDestination) => Promise; commit: (plan: ImportPlan) => Promise; cancel: () => void; onError: (err: unknown) => void; } type DestinationChoice = "new_workspace" | "current_workspace"; /** * An absolute or relative path is unambiguously a file. Everything else is treated as a URL, so a * bare host like `example.com/openapi.json` still works (the backend defaults it to https). */ function isFilePath(value: string): boolean { return ( value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/") || value.startsWith("\\\\") || /^[a-zA-Z]:[\\/]/.test(value) ); } function fileName(path: string): string { return path.split(/[/\\]/).at(-1) || path; } export function ImportDataDialog({ currentWorkspace, selectedFolder, planFile, planUrl, commit, cancel, onError, }: Props) { const [isLoading, setIsLoading] = useState(false); const [plan, setPlan] = useState(null); const [destinationChoice, setDestinationChoice] = useState( currentWorkspace == null ? "new_workspace" : "current_workspace", ); const [targetSelectedFolder, setTargetSelectedFolder] = useState(selectedFolder != null); // A file path or a URL. Both inputs write here, so there is only ever one thing to import const [source, setSource] = useLocalStorage("importPathOrUrl", null); const [forceUpdateKey, setForceUpdateKey] = useState(0); const [isHovering, setIsHovering] = useState(false); const ref = useRef(null); const trimmedSource = source?.trim() ?? ""; const filePath = isFilePath(trimmedSource) ? trimmedSource : null; const selectSource = (value: string) => { setSource(value); // Remount the input so it shows the path of the newly-picked file setForceUpdateKey((k) => k + 1); }; // Accept a file dropped anywhere on the dialog, the way SelectFile does for its button useEffect(() => { return platform.window.onDragDrop((event) => { if (event.type === "over") { const p = event.position; const r = ref.current?.getBoundingClientRect(); if (r == null) return; setIsHovering(p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom); } else if (event.type === "drop" && isHovering) { const p = event.paths[0]; if (p) selectSource(p); setIsHovering(false); } else { setIsHovering(false); } }); }, [isHovering, setSource]); const handleSelectFile = async () => { const selected = await platform.dialog.open({ title: "Select File", multiple: false }); if (selected == null) return; selectSource(selected); }; const destination = (): ImportDestination => { if (destinationChoice === "current_workspace" && currentWorkspace != null) { return { type: "current_workspace", workspaceId: currentWorkspace.id, folderId: targetSelectedFolder ? selectedFolder?.id : undefined, }; } return { type: "new_workspace" }; }; const handlePreview = async () => { setIsLoading(true); try { const nextPlan = filePath != null ? await planFile(filePath, destination()) : await planUrl(trimmedSource, destination()); setPlan(nextPlan); } catch (err) { onError(err); } finally { setIsLoading(false); } }; const handleCommit = async () => { if (plan == null) return; setIsLoading(true); try { await commit(plan); } catch (err) { onError(err); } finally { setIsLoading(false); } }; if (plan != null) { const counts = [ [plan.resources.workspaces[0]?.resource, plan.resources.workspaces.length], [plan.resources.environments[0]?.resource, plan.resources.environments.length], [plan.resources.folders[0]?.resource, plan.resources.folders.length], [plan.resources.httpRequests[0]?.resource, plan.resources.httpRequests.length], [plan.resources.grpcRequests[0]?.resource, plan.resources.grpcRequests.length], [plan.resources.websocketRequests[0]?.resource, plan.resources.websocketRequests.length], ] as const; const destinationLabel = plan.destination.type === "new_workspace" ? "New workspace" : selectedFolder != null && plan.destination.folderId === selectedFolder.id ? `${currentWorkspace?.name ?? "Current workspace"} / ${selectedFolder.name}` : (currentWorkspace?.name ?? "Current workspace"); return (
Resources
    {counts.map(([model, count]) => model == null ? null : (
  • {pluralizeCount(modelTypeLabel(model), count)}
  • ), )}
{plan.warnings.length > 0 && (
Import details
{plan.warnings.map((warning) => (
{warning.title}
{warning.detail}
))}
)}
); } return (
Import destination
{destinationChoice === "current_workspace" && selectedFolder != null && ( )}
); } function PreviewRow({ label, value }: { label: string; value: string }) { return (
{label} {value}
); }