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:
@@ -11,6 +11,8 @@ import type {
|
||||
VisualGroupsResponse,
|
||||
VisualMatchesResponse,
|
||||
} from "@/lib/types";
|
||||
import { confirmAction } from "@/store/confirm";
|
||||
import { toast } from "@/store/toasts";
|
||||
|
||||
const ALGORITHMS = [
|
||||
{ value: "ahash", label: "aHash" },
|
||||
@@ -100,7 +102,6 @@ export default function DuplicatesPage() {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [dismissed, setDismissed] = useState<string[]>([]);
|
||||
const [similarTarget, setSimilarTarget] = useState("");
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
|
||||
const exactQuery = useQuery({
|
||||
queryKey: ["duplicates-md5"],
|
||||
@@ -130,7 +131,7 @@ export default function DuplicatesPage() {
|
||||
method: "POST",
|
||||
json: { j_id: jId, threshold, algorithms },
|
||||
}),
|
||||
onError: (error) => setStatus(errorMessage(error)),
|
||||
onError: (error) => toast.error(errorMessage(error)),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
@@ -138,18 +139,52 @@ export default function DuplicatesPage() {
|
||||
api<DeleteResult>("/api/delete/", { method: "POST", json: payload }),
|
||||
onSuccess: (result) => {
|
||||
setSelected([]);
|
||||
setStatus(
|
||||
`Deleted ${result.deleted.length} entr${
|
||||
result.deleted.length === 1 ? "y" : "ies"
|
||||
}${result.errors.length > 0 ? ` · ${result.errors.length} failed` : ""}.`,
|
||||
);
|
||||
const noun = result.deleted.length === 1 ? "entry" : "entries";
|
||||
toast.ok(`Deleted ${result.deleted.length} ${noun}.`);
|
||||
if (result.errors.length > 0) {
|
||||
toast.error(
|
||||
`${result.errors.length} ${noun} could not be deleted: ` +
|
||||
result.errors.map((entry) => entry.error).join(", "),
|
||||
);
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: ["duplicates-md5"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["duplicate-groups"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["files"] });
|
||||
},
|
||||
onError: (error) => setStatus(errorMessage(error)),
|
||||
onError: (error) => toast.error(errorMessage(error)),
|
||||
});
|
||||
|
||||
async function handleDeleteCopy(locationId: number) {
|
||||
const confirmed = await confirmAction({
|
||||
title: "Delete this copy?",
|
||||
description:
|
||||
"The file at this path is removed. The item stays as long as another copy exists.",
|
||||
confirmLabel: "Delete copy",
|
||||
danger: true,
|
||||
});
|
||||
if (confirmed) deleteMutation.mutate({ location_ids: [locationId] });
|
||||
}
|
||||
|
||||
async function handleDeleteItem(jId: string) {
|
||||
const confirmed = await confirmAction({
|
||||
title: `Delete ${jId}?`,
|
||||
description: "The item and all of its copies are removed from the library.",
|
||||
confirmLabel: "Delete",
|
||||
danger: true,
|
||||
});
|
||||
if (confirmed) deleteMutation.mutate({ j_ids: [jId] });
|
||||
}
|
||||
|
||||
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({ j_ids: selected });
|
||||
}
|
||||
|
||||
function toggleSelected(jId: string) {
|
||||
setSelected((current) =>
|
||||
current.includes(jId)
|
||||
@@ -190,12 +225,6 @@ export default function DuplicatesPage() {
|
||||
</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="flex flex-col gap-3">
|
||||
<header className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-sm font-semibold text-ctp-subtext1">
|
||||
@@ -252,9 +281,7 @@ export default function DuplicatesPage() {
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
deleteMutation.mutate({ location_ids: [location.id] })
|
||||
}
|
||||
onClick={() => void handleDeleteCopy(location.id)}
|
||||
className="shrink-0 rounded-md px-1.5 py-0.5 font-mono text-[10px] text-ctp-red transition hover:bg-ctp-red/15"
|
||||
>
|
||||
delete copy
|
||||
@@ -267,7 +294,7 @@ export default function DuplicatesPage() {
|
||||
<Button
|
||||
variant="danger"
|
||||
className="px-2 py-1 text-xs"
|
||||
onClick={() => deleteMutation.mutate({ j_ids: [group.j_id] })}
|
||||
onClick={() => void handleDeleteItem(group.j_id)}
|
||||
>
|
||||
Delete item
|
||||
</Button>
|
||||
@@ -347,11 +374,6 @@ export default function DuplicatesPage() {
|
||||
>
|
||||
{similarMutation.isPending ? "Searching…" : "Search"}
|
||||
</Button>
|
||||
{similarMutation.isError ? (
|
||||
<p className="text-xs text-ctp-red">
|
||||
{errorMessage(similarMutation.error)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{similarMutation.data ? (
|
||||
@@ -460,7 +482,7 @@ export default function DuplicatesPage() {
|
||||
variant="danger"
|
||||
className="px-2 py-1 text-xs"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => deleteMutation.mutate({ j_ids: selected })}
|
||||
onClick={() => void handleDeleteSelected()}
|
||||
>
|
||||
{deleteMutation.isPending ? "Deleting…" : "Delete selected"}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user