feat(jobs): confirm-before-download dialog so users see what's happening

Files were grabbed via a plain anchor with the 'download' attribute, so
clicking the icon just spawned a silent browser download — nothing in
the UI moved, and users (especially labs receiving scans) couldn't tell
whether the click registered.

Wrapped the download button in a confirm dialog that mirrors the existing
delete flow: title 'Dosya indirilsin mi?', filename + size in the body,
Vazgeç / İndir buttons. The 'İndir' button programmatically clicks a
hidden anchor pointing at the /api/.../download proxy and surfaces a
'İndirme başladı.' toast with the filename so there's a clear visual ack
even before the OS download tray pops.
This commit is contained in:
kovakmedya
2026-05-22 01:08:10 +03:00
parent 12631cf9c5
commit 6fec52b98d
@@ -242,6 +242,7 @@ function FileRow({ file }: { file: JobFileWithUrl }) {
); );
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [downloadOpen, setDownloadOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (state.ok) { if (state.ok) {
@@ -252,6 +253,20 @@ function FileRow({ file }: { file: JobFileWithUrl }) {
} }
}, [state]); }, [state]);
function triggerDownload() {
// Use a programmatic anchor click — the server route streams the file
// with Content-Disposition: attachment, so the browser hands it straight
// to the download manager. Toast confirms it left our side.
const a = document.createElement("a");
a.href = file.url;
a.download = file.name;
document.body.appendChild(a);
a.click();
a.remove();
setDownloadOpen(false);
toast.success("İndirme başladı.", { description: file.name });
}
return ( return (
<li className="flex items-center gap-3 px-3 py-2"> <li className="flex items-center gap-3 px-3 py-2">
<span className="text-muted-foreground">{kindIcon(file.kind)}</span> <span className="text-muted-foreground">{kindIcon(file.kind)}</span>
@@ -264,11 +279,31 @@ function FileRow({ file }: { file: JobFileWithUrl }) {
<Badge variant="outline" className="hidden sm:inline-flex"> <Badge variant="outline" className="hidden sm:inline-flex">
{JOB_FILE_KIND_LABELS[file.kind] ?? file.kind} {JOB_FILE_KIND_LABELS[file.kind] ?? file.kind}
</Badge> </Badge>
<Button asChild size="sm" variant="outline"> <Dialog open={downloadOpen} onOpenChange={setDownloadOpen}>
<a href={file.url} target="_blank" rel="noopener noreferrer" download={file.name}> <Button size="sm" variant="outline" onClick={() => setDownloadOpen(true)}>
<Download className="size-4" /> <Download className="size-4" />
</a>
</Button> </Button>
<DialogContent>
<DialogHeader>
<DialogTitle>Dosya indirilsin mi?</DialogTitle>
<DialogDescription>
<span className="font-medium">{file.name}</span>
<span className="text-muted-foreground"> · {formatSize(file.size)}</span>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
Vazgeç
</Button>
</DialogClose>
<Button type="button" onClick={triggerDownload}>
<Download className="size-4" />
İndir
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<Button size="sm" variant="outline" onClick={() => setOpen(true)}> <Button size="sm" variant="outline" onClick={() => setOpen(true)}>
<Trash2 className="size-4" /> <Trash2 className="size-4" />