import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; import { Clock, Pencil, Trash2 } from "lucide-react"; import { api, type FileFolder, type FolderPatch } from "../lib/api"; import { notifyActionError } from "../lib/actionErrors"; import { invalidateDownloads } from "../lib/downloads"; import { useConfirm } from "./confirmContext"; import { folderLabel } from "./fileFolders"; import { type ActionMenuItem } from "./ui/ActionMenu"; import { menuGroups } from "./ui/menuGroups"; // A library folder's ONE menu — the rail entry's right click and the Files page's "…" beside an open // folder's name. An own folder is renamed (inline, where it is named), kept for a while and deleted; a // source folder is named by its origin and cannot go, so only how long it keeps its files is its // user's to set (the server holds the same rules: `routes/files.py`). /** The choices the Retention submenu offers (days; null = until the user deletes the file). */ const RETENTION_CHOICES: readonly (number | null)[] = [7, 30, 90, null]; // Which failure title a PATCH speaks under: what it was asked to change. const patchFail = (p: FolderPatch) => "name" in p ? "errors.writeFailed.folder.rename" : "errors.writeFailed.folder.retention"; export function useFolderActions({ onRename }: { onRename: (folder: FileFolder) => void }) { const qc = useQueryClient(); const { t } = useTranslation(); const confirm = useConfirm(); // Every write lands in the listings too: a retention change moves its files' expiry, a delete moves // the files themselves. const create = useMutation({ mutationFn: (name: string) => api.createFolder(name), onSuccess: () => invalidateDownloads(qc), meta: { failTitle: "errors.writeFailed.folder.create" }, }); const update = useMutation({ mutationFn: ({ id, patch }: { id: number; patch: FolderPatch }) => api.updateFolder(id, patch), onSuccess: () => invalidateDownloads(qc), onError: (e, v) => notifyActionError(e, t(patchFail(v.patch))), }); const remove = useMutation({ mutationFn: (id: number) => api.deleteFolder(id), onSuccess: () => invalidateDownloads(qc), meta: { failTitle: "errors.writeFailed.folder.delete" }, }); const icon = (I: typeof Clock) => ; const daysLabel = (days: number | null) => days === null ? t("files.folder.forever") : t("files.retention.days", { count: days }); // The folder's current retention is always one of the rows, ticked — an odd value set elsewhere (the // admin's default for source folders) included, so the menu never claims a value it does not hold. const retention = (folder: FileFolder): ActionMenuItem => { const current = folder.retention_days; const days = RETENTION_CHOICES.filter((d): d is number => d !== null); if (current !== null && !days.includes(current)) days.push(current); const choices = [...days.sort((a, b) => a - b), null]; return { id: "retention", label: t("files.folder.retention"), icon: icon(Clock), items: choices.map((d) => ({ id: `retention-${d ?? "forever"}`, label: daysLabel(d), check: { kind: "one", checked: d === current }, onSelect: () => { if (d !== current) update.mutate({ id: folder.id, patch: { retention_days: d } }); }, })), }; }; const confirmDelete = async (folder: FileFolder) => { const ok = await confirm({ title: t("files.folder.deleteTitle", { name: folderLabel(folder, t) }), message: folder.count === 0 ? t("files.folder.deleteBodyEmpty") : t("files.folder.deleteBody", { count: folder.count }), confirmLabel: t("files.folder.deleteAction"), danger: true, }); if (ok) remove.mutate(folder.id); }; /** The folder's menu rows. */ const items = (folder: FileFolder): ActionMenuItem[] => folder.kind === "own" ? menuGroups( [ { id: "rename", label: t("files.folder.rename"), icon: icon(Pencil), onSelect: () => onRename(folder), }, retention(folder), ], [ { id: "delete", label: t("files.folder.delete"), icon: icon(Trash2), danger: true, onSelect: () => void confirmDelete(folder), }, ], ) : [retention(folder)]; /** Save the inline rename: an unchanged or empty name changes nothing (a folder needs a name). */ const rename = (folder: FileFolder, name: string) => { const next = name.trim(); if (!next || next === folder.name) return; update.mutate({ id: folder.id, patch: { name: next } }); }; /** Make an own folder; resolves to it, or null when the server refused (already told). */ const add = async (name: string): Promise => { const next = name.trim(); if (!next) return null; try { return await create.mutateAsync(next); } catch { return null; } }; return { items, rename, add }; }