From a7bea17f3dfbeb316cee16aea7483a9b19a7192b Mon Sep 17 00:00:00 2001 From: Zekbot001 <280472700+Zekbot001@users.noreply.github.com> Date: Sun, 31 May 2026 20:25:57 +0200 Subject: [PATCH 1/2] fix(directory): keep failed owner actions in place --- .../[id]/DirectoryOwnerActions.test.tsx | 62 +++++++++++++++++++ .../directory/[id]/DirectoryOwnerActions.tsx | 60 ++++++++++++++---- 2 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 src/app/directory/[id]/DirectoryOwnerActions.test.tsx diff --git a/src/app/directory/[id]/DirectoryOwnerActions.test.tsx b/src/app/directory/[id]/DirectoryOwnerActions.test.tsx new file mode 100644 index 00000000..bd92a67d --- /dev/null +++ b/src/app/directory/[id]/DirectoryOwnerActions.test.tsx @@ -0,0 +1,62 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { DirectoryOwnerActions } from "./DirectoryOwnerActions"; + +const { push, refresh } = vi.hoisted(() => ({ + push: vi.fn(), + refresh: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push, refresh }), +})); + +const listing = { + id: "listing-1", + title: "Example", + url: "https://example.com", + description: null, + tags: [], + logo_url: null, + banner_url: null, + screenshot_url: null, + status: "active", +}; + +describe("DirectoryOwnerActions", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("fetch", vi.fn()); + vi.stubGlobal("confirm", vi.fn(() => true)); + }); + + afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + }); + + it("keeps the listing in place when a visibility update fails", async () => { + vi.mocked(fetch).mockResolvedValue( + new Response(JSON.stringify({ error: "Visibility denied" }), { status: 403 }) + ); + + render(); + await userEvent.click(screen.getByRole("button", { name: "Hide" })); + + expect(await screen.findByRole("alert")).toHaveTextContent("Visibility denied"); + expect(refresh).not.toHaveBeenCalled(); + }); + + it("keeps the listing in place when deletion fails", async () => { + vi.mocked(fetch).mockResolvedValue( + new Response(JSON.stringify({ error: "Delete denied" }), { status: 403 }) + ); + + render(); + await userEvent.click(screen.getByRole("button", { name: "Delete" })); + + expect(await screen.findByRole("alert")).toHaveTextContent("Delete denied"); + expect(push).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/directory/[id]/DirectoryOwnerActions.tsx b/src/app/directory/[id]/DirectoryOwnerActions.tsx index eaddcc9c..46e9a3b2 100644 --- a/src/app/directory/[id]/DirectoryOwnerActions.tsx +++ b/src/app/directory/[id]/DirectoryOwnerActions.tsx @@ -22,6 +22,15 @@ interface DirectoryOwnerActionsProps { }; } +async function responseError(res: Response, fallback: string) { + try { + const data = await res.json(); + return data.error || fallback; + } catch { + return fallback; + } +} + export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) { const router = useRouter(); const [editing, setEditing] = useState(false); @@ -72,22 +81,46 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) { } async function handleToggleVisibility() { + setError(""); setLoading(true); - const newStatus = listing.status === "active" ? "hidden" : "active"; - await fetch(`/api/directory/${listing.id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: newStatus }), - }); - setLoading(false); - router.refresh(); + try { + const newStatus = listing.status === "active" ? "hidden" : "active"; + const res = await fetch(`/api/directory/${listing.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: newStatus }), + }); + + if (!res.ok) { + setError(await responseError(res, "Visibility update failed")); + return; + } + + router.refresh(); + } catch { + setError("Visibility update failed"); + } finally { + setLoading(false); + } } async function handleDelete() { if (!confirm("Delete this listing? This cannot be undone and there is no refund.")) return; + setError(""); setLoading(true); - await fetch(`/api/directory/${listing.id}`, { method: "DELETE" }); - router.push("/directory"); + try { + const res = await fetch(`/api/directory/${listing.id}`, { method: "DELETE" }); + if (!res.ok) { + setError(await responseError(res, "Delete failed")); + return; + } + + router.push("/directory"); + } catch { + setError("Delete failed"); + } finally { + setLoading(false); + } } if (editing) { @@ -95,7 +128,7 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {

Edit Listing

{error && ( -
+
{error}
)} @@ -140,6 +173,11 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) { return (

Owner Actions

+ {error && ( +
+ {error} +
+ )}
- +
@@ -179,7 +182,7 @@ export function DirectoryOwnerActions({ listing }: DirectoryOwnerActionsProps) {
)}
-