Replace direct file picker trigger with a modal offering two import methods: file upload and paste JSON content. Uses a textarea instead of navigator.clipboard.readText() to avoid browser permission prompts. Also centers both import dialogs and updates spec for clipboard import. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
154 lines
4.1 KiB
TypeScript
154 lines
4.1 KiB
TypeScript
import { ClipboardPaste, FileUp, X } from "lucide-react";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Button } from "./ui/button.js";
|
|
|
|
interface ImportMethodDialogProps {
|
|
open: boolean;
|
|
onSelectFile: () => void;
|
|
onSubmitClipboard: (text: string) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function ImportMethodDialog({
|
|
open,
|
|
onSelectFile,
|
|
onSubmitClipboard,
|
|
onClose,
|
|
}: Readonly<ImportMethodDialogProps>) {
|
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
const [mode, setMode] = useState<"pick" | "paste">("pick");
|
|
const [pasteText, setPasteText] = useState("");
|
|
|
|
const reset = useCallback(() => {
|
|
setMode("pick");
|
|
setPasteText("");
|
|
}, []);
|
|
|
|
const handleClose = useCallback(() => {
|
|
reset();
|
|
onClose();
|
|
}, [reset, onClose]);
|
|
|
|
useEffect(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
if (open && !dialog.open) dialog.showModal();
|
|
else if (!open && dialog.open) {
|
|
dialog.close();
|
|
reset();
|
|
}
|
|
}, [open, reset]);
|
|
|
|
useEffect(() => {
|
|
if (mode === "paste") {
|
|
textareaRef.current?.focus();
|
|
}
|
|
}, [mode]);
|
|
|
|
useEffect(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
function handleCancel(e: Event) {
|
|
e.preventDefault();
|
|
handleClose();
|
|
}
|
|
function handleBackdropClick(e: MouseEvent) {
|
|
if (e.target === dialog) handleClose();
|
|
}
|
|
dialog.addEventListener("cancel", handleCancel);
|
|
dialog.addEventListener("mousedown", handleBackdropClick);
|
|
return () => {
|
|
dialog.removeEventListener("cancel", handleCancel);
|
|
dialog.removeEventListener("mousedown", handleBackdropClick);
|
|
};
|
|
}, [handleClose]);
|
|
|
|
return (
|
|
<dialog
|
|
ref={dialogRef}
|
|
className="m-auto w-80 rounded-lg border border-border bg-card p-6 text-foreground shadow-xl backdrop:bg-black/50"
|
|
>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="font-semibold text-lg">Import Encounter</h2>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
onClick={handleClose}
|
|
className="text-muted-foreground"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
{mode === "pick" && (
|
|
<div className="flex flex-col gap-2">
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-foreground text-sm hover:bg-hover-neutral-bg"
|
|
onClick={() => {
|
|
handleClose();
|
|
onSelectFile();
|
|
}}
|
|
>
|
|
<FileUp className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<div className="font-medium">From file</div>
|
|
<div className="text-muted-foreground text-xs">
|
|
Upload a JSON file
|
|
</div>
|
|
</div>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-foreground text-sm hover:bg-hover-neutral-bg"
|
|
onClick={() => setMode("paste")}
|
|
>
|
|
<ClipboardPaste className="h-5 w-5 text-muted-foreground" />
|
|
<div>
|
|
<div className="font-medium">Paste content</div>
|
|
<div className="text-muted-foreground text-xs">
|
|
Paste JSON content directly
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
)}
|
|
{mode === "paste" && (
|
|
<div className="flex flex-col gap-3">
|
|
<textarea
|
|
ref={textareaRef}
|
|
value={pasteText}
|
|
onChange={(e) => setPasteText(e.target.value)}
|
|
placeholder="Paste exported JSON here..."
|
|
className="h-32 w-full resize-none rounded-md border border-border bg-background px-3 py-2 font-mono text-foreground text-xs placeholder:text-muted-foreground focus:border-accent focus:outline-none"
|
|
/>
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setMode("pick");
|
|
setPasteText("");
|
|
}}
|
|
>
|
|
Back
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={pasteText.trim().length === 0}
|
|
onClick={() => {
|
|
const text = pasteText;
|
|
handleClose();
|
|
onSubmitClipboard(text);
|
|
}}
|
|
>
|
|
Import
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</dialog>
|
|
);
|
|
}
|