From f15f468b6f6bcbc7a2c36afa2ad6b78251b4855c Mon Sep 17 00:00:00 2001 From: Tom Smith <142233216+tomsmith8@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:32:31 +0100 Subject: [PATCH] Reserve delete fields and surface edge delete failures --- src/components/layout/connections-section.tsx | 100 ++++++++++-------- .../__tests__/connections-section.test.tsx | 26 +++++ src/lib/__tests__/node-remap.test.ts | 3 + src/lib/node-schema-utils.ts | 6 +- 4 files changed, 92 insertions(+), 43 deletions(-) diff --git a/src/components/layout/connections-section.tsx b/src/components/layout/connections-section.tsx index f84d42f..c7c2572 100644 --- a/src/components/layout/connections-section.tsx +++ b/src/components/layout/connections-section.tsx @@ -24,6 +24,7 @@ type GroupBy = "edge_type" | "node_type" export function ConnectionsSection({ nodeRefId, schemas, currentNode, onNavigate }: ConnectionsSectionProps) { const [groupBy, setGroupBy] = useState("edge_type") const [confirmingDelete, setConfirmingDelete] = useState(null) + const [deleteError, setDeleteError] = useState<{ edgeRefId: string; message: string } | null>(null) const nodes = useGraphStore((s) => s.nodes) const edges = useGraphStore((s) => s.edges) const removeEdge = useGraphStore((s) => s.removeEdge) @@ -72,11 +73,13 @@ export function ConnectionsSection({ nodeRefId, schemas, currentNode, onNavigate if (!isAdmin) return try { await deleteEdge(edgeRefId) + removeEdge(edgeRefId) + setConfirmingDelete(null) + setDeleteError(null) } catch { - // Best-effort — remove locally regardless + setConfirmingDelete(null) + setDeleteError({ edgeRefId, message: "Could not remove connection. Please try again." }) } - removeEdge(edgeRefId) - setConfirmingDelete(null) } return ( @@ -134,51 +137,64 @@ export function ConnectionsSection({ nodeRefId, schemas, currentNode, onNavigate

{conns.map((conn, i) => { const isConfirming = confirmingDelete === conn.edge_ref_id + const rowError = + conn.edge_ref_id !== undefined && deleteError?.edgeRefId === conn.edge_ref_id + ? deleteError.message + : null return ( -
- - {isAdmin && conn.edge_ref_id !== undefined && ( - isConfirming ? ( -
- Remove? - + {resolveTitle(conn.peer)} + + {displayNodeType(conn.peer.node_type)} + + + {isAdmin && conn.edge_ref_id !== undefined && ( + isConfirming ? ( +
+ Remove? + + +
+ ) : ( -
- ) : ( - - ) + ) + )} +
+ {rowError && ( +

+ {rowError} +

)} ) diff --git a/src/lib/__tests__/connections-section.test.tsx b/src/lib/__tests__/connections-section.test.tsx index 8f013e8..5651e50 100644 --- a/src/lib/__tests__/connections-section.test.tsx +++ b/src/lib/__tests__/connections-section.test.tsx @@ -250,6 +250,32 @@ describe("ConnectionsSection – admin delete", () => { expect(screen.queryByText("Remove?")).toBeNull() }) + it("keeps the edge and shows an inline error when deleteEdge fails", async () => { + mockIsAdmin = true + mockDeleteEdge.mockRejectedValueOnce(new Error("500")) + render() + fireEvent.click(screen.getByRole("button", { name: "Remove connection" })) + fireEvent.click(screen.getByRole("button", { name: "Confirm remove" })) + + expect(await screen.findByRole("alert")).toHaveTextContent("Could not remove connection") + expect(mockDeleteEdge).toHaveBeenCalledWith("edge-ref-1") + expect(mockRemoveEdge).not.toHaveBeenCalled() + expect(screen.getByText("Blockchain")).toBeInTheDocument() + expect(screen.queryByText("Remove?")).toBeNull() + }) + + it("clears the error when a remove is retried", async () => { + mockIsAdmin = true + mockDeleteEdge.mockRejectedValueOnce(new Error("500")) + render() + fireEvent.click(screen.getByRole("button", { name: "Remove connection" })) + fireEvent.click(screen.getByRole("button", { name: "Confirm remove" })) + await screen.findByRole("alert") + + fireEvent.click(screen.getByRole("button", { name: "Remove connection" })) + expect(screen.queryByRole("alert")).toBeNull() + }) + it("hides trash for edges without ref_id even when isAdmin=true", () => { mockIsAdmin = true mockEdges = [EDGE_MENTIONS_N2] // no ref_id diff --git a/src/lib/__tests__/node-remap.test.ts b/src/lib/__tests__/node-remap.test.ts index 0e411fd..48ad75c 100644 --- a/src/lib/__tests__/node-remap.test.ts +++ b/src/lib/__tests__/node-remap.test.ts @@ -216,6 +216,9 @@ describe("fieldsForSchema (from node-schema-utils)", () => { expect(SYSTEM_ATTRIBUTES.has("is_muted")).toBe(true) expect(SYSTEM_ATTRIBUTES.has("unique_source_id")).toBe(true) expect(SYSTEM_ATTRIBUTES.has("owner_reference_id")).toBe(true) + expect(SYSTEM_ATTRIBUTES.has("deleted_at")).toBe(true) + expect(SYSTEM_ATTRIBUTES.has("deleted_at_backfilled")).toBe(true) + expect(SYSTEM_ATTRIBUTES.has("is_deleted")).toBe(true) expect(SYSTEM_ATTRIBUTES.has("date_added_to_graph")).toBe(true) }) diff --git a/src/lib/node-schema-utils.ts b/src/lib/node-schema-utils.ts index 5e3c697..d86e68d 100644 --- a/src/lib/node-schema-utils.ts +++ b/src/lib/node-schema-utils.ts @@ -5,7 +5,8 @@ import type { SchemaNode, SchemaAttribute } from "@/lib/schema-types" // graph-internal moderation knobs, unique_source_id is for dedup of // ingested content (Stakwork). date_added_to_graph is auto-set. // project_id is the graph association and pub_key/pubkey are provenance — -// both set by the backend, not typed by hand. +// both set by the backend, not typed by hand. deleted_at, deleted_at_backfilled +// and is_deleted are the soft-delete markers, reserved for the backend. export const SYSTEM_ATTRIBUTES = new Set([ "weight", "is_muted", @@ -15,6 +16,9 @@ export const SYSTEM_ATTRIBUTES = new Set([ "project_id", "pub_key", "pubkey", + "deleted_at", + "deleted_at_backfilled", + "is_deleted", ]) // A field is hidden from every node form when it's a system/book-keeping