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 { Select } from "./core/Select"; interface Props { currentWorkspace: Workspace | null; workspaces: Workspace[]; 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; } /** Sentinel for the "create a new workspace" option. Workspace IDs are prefixed `wk_`, so this * can never collide with a real one. */ const NEW_WORKSPACE = "new_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, workspaces, selectedFolder, planFile, planUrl, commit, cancel, onError, }: Props) { const [isLoading, setIsLoading] = useState(false); const [plan, setPlan] = useState(null); const [destinationId, setDestinationId] = useState(NEW_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); }; // The selected folder belongs to the workspace being viewed, so it is only offerable when that // is also the destination. const canTargetSelectedFolder = selectedFolder != null && destinationId === currentWorkspace?.id; const destination = (): ImportDestination => { if (destinationId === NEW_WORKSPACE) { return { type: "new_workspace" }; } return { type: "existing_workspace", workspaceId: destinationId, folderId: canTargetSelectedFolder && targetSelectedFolder ? selectedFolder.id : undefined, }; }; 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], plan.resources.workspaces.length], [plan.resources.environments[0], plan.resources.environments.length], [plan.resources.folders[0], plan.resources.folders.length], [plan.resources.httpRequests[0], plan.resources.httpRequests.length], [plan.resources.grpcRequests[0], plan.resources.grpcRequests.length], [plan.resources.websocketRequests[0], plan.resources.websocketRequests.length], ] as const; const destinationLabel = (() => { if (plan.destination.type === "new_workspace") return "New workspace"; const { workspaceId, folderId } = plan.destination; const name = workspaces.find((w) => w.id === workspaceId)?.name ?? "Unknown workspace"; return folderId != null && folderId === selectedFolder?.id ? `${name} / ${selectedFolder.name}` : name; })(); 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 (