Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 5x 10x 10x 5x 5x 5x 4x 5x 5x 5x | "use client";
import { useRouter } from "next/navigation";
import { AsyncActionButton } from "./AsyncActionButton";
// The "Set aside" list's per-row reversal: a single quiet tap brings an item back
// from set-aside (deletes its `set_aside` disposition) and refreshes so the row
// leaves the list. The DELETE is idempotent (route returns 200 either way), so a
// double-tap is harmless — the in-flight guard still collapses it to one request.
// A gentle, non-alarming error notice on failure; no confirm step — bringing back
// is reversible and low-stakes (TONE.md, commitment 4).
//
// collectionId MUST be the ULID collection.id, not the slug: the dispositions API
// authorizes the item then asserts item.collectionId === params.id, which is
// always a ULID — a slug there 404s.
export function BringBackControl({
collectionId,
itemId,
itemName,
focusAfterLastId,
}: {
collectionId: string;
itemId: string;
itemName: string;
/** Where focus goes when this was the last row (its list leaves with it). */
focusAfterLastId?: string;
}) {
const router = useRouter();
return (
<AsyncActionButton
label="Bring back"
ariaLabel={`Bring back ${itemName}`}
action={async () => {
// Read before the await: the row (and its button) leaves with the refresh.
const row = document.activeElement?.closest("li");
const res = await fetch(`/api/collections/${collectionId}/dispositions/${itemId}`, {
method: "DELETE",
});
if (!res.ok) throw new Error("bring back failed");
const neighbour = row?.nextElementSibling ?? row?.previousElementSibling;
const target = neighbour?.querySelector("button") ?? (focusAfterLastId ? document.getElementById(focusAfterLastId) : null);
target?.focus();
router.refresh(); // the row leaves the list; the item returns to the queue
}}
/>
);
}
|