{copied ? : }
{/* All three on the same rung. Only Copy was migrated in S1, which left one 2rem button
between two 1.75rem ones with a different radius and hover fill — and, on a coarse
pointer, `.touch-target` grew ONLY the migrated one to 2.75rem while the destructive
Revoke stayed small (E10 review round 1). */}
{
setNewPass("");
setEditPass((v) => !v);
}}
// On the ICON, not the button: `clsx` does not merge Tailwind, so a colour className loses
// to the variant's own `text-muted`/`hover:text-fg` by stylesheet order and the state was
// simply invisible (E10 review round 2). The Copy button above already does it this way.
>
{
if (
await confirm({
title: t("downloads.share.revoke"),
message: t("downloads.share.revokeConfirm"),
confirmLabel: t("downloads.share.revoke"),
danger: true,
})
)
revoke.mutate();
}}
>
);
}
function LinkShare({ job }: { job: DownloadJob }) {
const { t } = useTranslation();
const qc = useQueryClient();
const links = useQuery({
queryKey: qk.downloadLinks(job.id),
queryFn: () => api.downloadLinks(job.id),
});
const [creating, setCreating] = useState(false);
// D1 (E7 S3): downloadable by default — matches the one-click Share (which relies on the backend
// default) and the common "let them have the file" intent. The owner can uncheck it here.
const [allowDownload, setAllowDownload] = useState(true);
const [expiryDays, setExpiryDays] = useState(null);
const [password, setPassword] = useState("");
const invalidate = () => qc.invalidateQueries({ queryKey: qk.downloadLinks(job.id) });
const create = useMutation({
mutationFn: () =>
api.createDownloadLink(job.id, {
allow_download: allowDownload,
expires_days: expiryDays,
password: password.trim() || undefined,
}),
onSuccess: () => {
invalidate();
setCreating(false);
setAllowDownload(false);
setExpiryDays(null);
setPassword("");
},
meta: { failTitle: "downloads.share.linkFailed" },
});
// Open with a link already generated: sharing is almost always the goal, so mint a default
// (no password, no expiry, downloadable per D1) link the moment the dialog finds none — as if the
// user had pressed "New link" → "Create". All further controls (password, download toggle, expiry,
// extra links, revoke) stay available on the created link and via the "New link" button.
const autoTried = useRef(false);
// The mutation OBJECT is rebuilt every render; `mutate` is not (react-query memoises it), so the
// two halves the effect actually uses can be named while `links.data` stays the only real trigger.
// The autoTried ref is what makes it fire exactly once.
const { mutate: createLink, isPending: createPending } = create;
useEffect(() => {
if (links.data && links.data.length === 0 && !autoTried.current && !createPending) {
autoTried.current = true;
createLink();
}
}, [links.data, createLink, createPending]);
return (