Files
siftlode/frontend/src/lib/downloads.ts
T
peter fdca12968b feat(downloads): one-click Share + downloadable-by-default links (E7 S3)
D2-A: the library row's Share button becomes a single action — quickShareLink()
ensures a public link (minting on demand), copies it to the clipboard, and toasts
with an "Open in new tab" action. A copy failure still toasts (warning) with that
action so the link is never a dead end. Advanced controls (password/expiry/revoke/
user-ACL) stay in the existing ShareDialog, reached via a new "Manage sharing" row
button.

D1 (forward-only): CreateLinkIn.allow_download defaults to true, so a newly minted
link is downloadable by default. The default lives in the API, not a migration —
the download_links column default stays false, so no existing link flips. The
ShareDialog create-default is flipped to match.

Tests: TestCreateLinkDefault (default true; explicit false honored); 4 vitest cases
for quickShareLink (mint-vs-reuse, copy ok/fail, create failure). HU/EN parity.
2026-08-29 06:36:07 +02:00

53 lines
2.2 KiB
TypeScript

import type { QueryClient } from "@tanstack/react-query";
import { qk } from "./queryKeys";
import { api } from "./api";
import { copyText } from "./clipboard";
import { notify } from "./notifications";
// The download queue, the per-card download index, and the storage-usage meter all move together
// whenever a job is enqueued, deleted, or its state changes — invalidate them as one unit.
export function invalidateDownloads(qc: QueryClient) {
qc.invalidateQueries({ queryKey: qk.downloads() });
qc.invalidateQueries({ queryKey: qk.downloadIndex() });
qc.invalidateQueries({ queryKey: qk.downloadUsage() });
}
// One-click share (E7 S3 · D2-A): the row's Share button turns "share this" into a single action.
// Ensure the job has a public link (minting one on demand — omitting `allow_download` so the backend
// D1 default makes it downloadable), copy it to the clipboard, and toast with an "open in new tab"
// action. A copy failure still toasts (warning) WITH that action, so the link is never a dead end.
// Advanced controls (password, expiry, revoke, share-with-a-user) live in the Share dialog. Returns
// the shared URL, or null if link creation failed — so the caller/tests can react to the outcome.
export async function quickShareLink(
qc: QueryClient,
job: { id: number },
t: (key: string) => string,
): Promise<string | null> {
let link;
try {
const existing = await qc.fetchQuery({
queryKey: qk.downloadLinks(job.id),
queryFn: () => api.downloadLinks(job.id),
});
link = existing[0];
if (!link) {
link = await api.createDownloadLink(job.id, {}); // omit allow_download → backend D1 default (true)
qc.setQueryData(qk.downloadLinks(job.id), [link]);
}
} catch {
notify({ level: "error", message: t("downloads.share.linkFailed") });
return null;
}
const url = window.location.origin + link.url;
const action = {
label: t("downloads.share.openNewTab"),
onClick: () => window.open(url, "_blank", "noopener,noreferrer"),
};
notify(
(await copyText(url))
? { level: "success", message: t("downloads.share.linkCopied"), action }
: { level: "warning", message: t("downloads.share.copyFailed"), action },
);
return url;
}