Toast action results and confirm destructive actions in one dialog
Inline banners and per-row status text reported action results all over the app; they are replaced by a small toast stack (bottom-right, Level 3 floating well styling) that only speaks for actions: successes fade, errors stay until dismissed, and form-field validation stays inline. Destructive actions no longer use bespoke inline confirm steps (the library detail's Confirm delete button) or fire immediately (duplicate copies/items, delete page selections, temp cleanup, upload discard, job cancellation): they all go through one promise-based confirm dialog (confirmAction) with a danger variant, Escape/backdrop to cancel.
This commit is contained in:
@@ -7,6 +7,8 @@ import { api, errorMessage } from "@/lib/api";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import type { MediaItem, Paginated, StorageInfo } from "@/lib/types";
|
||||
import { confirmAction } from "@/store/confirm";
|
||||
import { toast } from "@/store/toasts";
|
||||
|
||||
function parseJIds(text: string): string[] {
|
||||
return text
|
||||
@@ -20,7 +22,6 @@ export default function DeletePage() {
|
||||
const [jIdsInput, setJIdsInput] = useState("");
|
||||
const [loadedIds, setLoadedIds] = useState<string[]>([]);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
|
||||
const storageQuery = useQuery({
|
||||
queryKey: ["storage"],
|
||||
@@ -43,28 +44,50 @@ export default function DeletePage() {
|
||||
onSuccess: (result) => {
|
||||
setSelected([]);
|
||||
setLoadedIds([]);
|
||||
setStatus(
|
||||
`Deleted ${result.deleted.length} item(s)${
|
||||
result.errors.length > 0 ? ` · ${result.errors.length} failed` : ""
|
||||
}.`,
|
||||
);
|
||||
toast.ok(`Deleted ${result.deleted.length} item(s).`);
|
||||
if (result.errors.length > 0) {
|
||||
toast.error(
|
||||
`${result.errors.length} item(s) could not be deleted: ` +
|
||||
result.errors.map((entry) => entry.error).join(", "),
|
||||
);
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: ["storage"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["files"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["duplicates-md5"] });
|
||||
},
|
||||
onError: (error) => setStatus(errorMessage(error)),
|
||||
onError: (error) => toast.error(errorMessage(error)),
|
||||
});
|
||||
|
||||
const tempMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
api<{ removed: number }>("/api/temp/clear/", { method: "POST" }),
|
||||
onSuccess: (result) => {
|
||||
setStatus(`Removed ${result.removed} temp file(s).`);
|
||||
toast.ok(`Removed ${result.removed} temp file(s).`);
|
||||
void queryClient.invalidateQueries({ queryKey: ["storage"] });
|
||||
},
|
||||
onError: (error) => setStatus(errorMessage(error)),
|
||||
onError: (error) => toast.error(errorMessage(error)),
|
||||
});
|
||||
|
||||
async function handleDeleteSelected() {
|
||||
const confirmed = await confirmAction({
|
||||
title: `Delete ${selected.length} item(s)?`,
|
||||
description: "They are removed from the library along with all of their copies.",
|
||||
confirmLabel: "Delete",
|
||||
danger: true,
|
||||
});
|
||||
if (confirmed) deleteMutation.mutate(selected);
|
||||
}
|
||||
|
||||
async function handleClearTemp() {
|
||||
const confirmed = await confirmAction({
|
||||
title: "Clear the temp folder?",
|
||||
description: "Staged uploads still sitting in it will be removed.",
|
||||
confirmLabel: "Clear",
|
||||
danger: true,
|
||||
});
|
||||
if (confirmed) tempMutation.mutate();
|
||||
}
|
||||
|
||||
function toggleSelected(jId: string) {
|
||||
setSelected((current) =>
|
||||
current.includes(jId)
|
||||
@@ -84,12 +107,6 @@ export default function DeletePage() {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{status ? (
|
||||
<p className="rounded-md border border-ctp-surface1 bg-ctp-base px-3 py-2 text-xs text-ctp-subtext0">
|
||||
{status}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<section className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{storageQuery.isPending ? (
|
||||
<Spinner className="h-5 w-5" />
|
||||
@@ -157,7 +174,7 @@ export default function DeletePage() {
|
||||
variant="secondary"
|
||||
className="mt-2 px-2 py-1 text-xs"
|
||||
disabled={tempMutation.isPending || storage.temp.files === 0}
|
||||
onClick={() => tempMutation.mutate()}
|
||||
onClick={() => void handleClearTemp()}
|
||||
>
|
||||
{tempMutation.isPending ? "Clearing…" : "Clear temp folder"}
|
||||
</Button>
|
||||
@@ -182,7 +199,6 @@ export default function DeletePage() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStatus(null);
|
||||
setSelected([]);
|
||||
setLoadedIds(parseJIds(jIdsInput));
|
||||
}}
|
||||
@@ -193,7 +209,7 @@ export default function DeletePage() {
|
||||
<Button
|
||||
variant="danger"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => deleteMutation.mutate(selected)}
|
||||
onClick={() => void handleDeleteSelected()}
|
||||
>
|
||||
{deleteMutation.isPending
|
||||
? "Deleting…"
|
||||
|
||||
Reference in New Issue
Block a user