From 59f1a40d276f49c12d38997c692df7d915d41057 Mon Sep 17 00:00:00 2001 From: Rassl Date: Tue, 1 Sep 2026 15:46:04 +0400 Subject: [PATCH] feat: update payload --- src/app/admin/reviews/page.tsx | 6 +++++- src/components/admin/review-row.tsx | 8 ++++---- src/lib/__tests__/reviews.test.tsx | 12 +++++------ src/lib/graph-api.ts | 31 +++++++++++++++++++---------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/admin/reviews/page.tsx b/src/app/admin/reviews/page.tsx index 5f9707a..1820de8 100644 --- a/src/app/admin/reviews/page.tsx +++ b/src/app/admin/reviews/page.tsx @@ -307,7 +307,11 @@ export default function ReviewsPage() { const results = await Promise.allSettled( selectedReviews.map((r) => fn(r.ref_id)) ) - const failures = results.filter((r) => r.status === "rejected").length + // A failed approve action answers HTTP 200 with an Error envelope, so a + // fulfilled promise is not enough — check the envelope status too. + const failures = results.filter( + (r) => r.status === "rejected" || r.value.status !== "Success" + ).length setBulkRunning(null) if (failures > 0) { setBulkError( diff --git a/src/components/admin/review-row.tsx b/src/components/admin/review-row.tsx index 3fe262f..0da9f62 100644 --- a/src/components/admin/review-row.tsx +++ b/src/components/admin/review-row.tsx @@ -707,8 +707,8 @@ export function ReviewRow({ setInlineError(null) try { const res = await approveReview(review.ref_id, override) - if (res.error_message || res.status === "failed") { - setInlineError(res.error_message ?? "Approval failed") + if (res.status !== "Success") { + setInlineError(res.error_message ?? res.message ?? "Approval failed") onCountRefresh?.() return } @@ -744,12 +744,12 @@ export function ReviewRow({ ? { from: effectiveFrom, to: canonicalId } : undefined const res = await approveReview(review.ref_id, override) - if (res.error_message || res.status === "failed") { + if (res.status !== "Success") { // Keep the row and its error visible instead of silently refetching it // out of the pending list (it is now 'failed', not pending) — otherwise // a failed merge just vanishes and looks like it succeeded. Still refresh // the pending badge so the count reflects that it left the queue. - setInlineError(res.error_message ?? "Approval failed") + setInlineError(res.error_message ?? res.message ?? "Approval failed") onCountRefresh?.() return } diff --git a/src/lib/__tests__/reviews.test.tsx b/src/lib/__tests__/reviews.test.tsx index 06c62f2..730534d 100644 --- a/src/lib/__tests__/reviews.test.tsx +++ b/src/lib/__tests__/reviews.test.tsx @@ -205,7 +205,7 @@ describe("ReviewRow", () => { it("calls approveReview with correct ref_id after confirmation", async () => { const user = userEvent.setup() - mockApproveReview.mockResolvedValue({ status: "approved" }) + mockApproveReview.mockResolvedValue({ status: "Success" }) const onRefresh = vi.fn() const { getByText } = render( @@ -223,10 +223,10 @@ describe("ReviewRow", () => { await waitFor(() => expect(onRefresh).toHaveBeenCalled()) }) - it("shows inline error when approve returns failed status", async () => { + it("shows inline error when approve returns an Error envelope", async () => { const user = userEvent.setup() mockApproveReview.mockResolvedValue({ - status: "failed", + status: "Error", error_message: "no handler registered for action: supersede", }) @@ -246,7 +246,7 @@ describe("ReviewRow", () => { it("calls dismissReview with reason after entering text", async () => { const user = userEvent.setup() - mockDismissReview.mockResolvedValue({ status: "dismissed" }) + mockDismissReview.mockResolvedValue({ status: "Success" }) const onRefresh = vi.fn() const { getByText, getByPlaceholderText } = render( @@ -266,7 +266,7 @@ describe("ReviewRow", () => { it("calls dismissReview without reason when textarea left empty", async () => { const user = userEvent.setup() - mockDismissReview.mockResolvedValue({ status: "dismissed" }) + mockDismissReview.mockResolvedValue({ status: "Success" }) const onRefresh = vi.fn() const { getByText } = render( @@ -1061,7 +1061,7 @@ describe("ReviewRow merge_nodes interactive controls", () => { beforeEach(() => { mockApproveReview.mockReset() - mockApproveReview.mockResolvedValue({ status: "approved" }) + mockApproveReview.mockResolvedValue({ status: "Success" }) }) it("shows all sources checked by default on expand", async () => { diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts index 9d2ecb4..bead31a 100644 --- a/src/lib/graph-api.ts +++ b/src/lib/graph-api.ts @@ -1145,15 +1145,26 @@ export async function getSchemaProposal( ) } +/** + * Edges-style envelope shared by the review decision endpoints: `status` is + * the operation outcome ("Success"/"Error"), while the updated review under + * `review` carries the lifecycle state (approved/failed/dismissed). A failed + * approve action still answers HTTP 200 with `status: "Error"`. + */ +export interface ReviewDecisionResponse { + status: string + message?: string + status_messages?: string[] + error_message?: string + promotion_summary?: PromotionSummary | null + review?: Review +} + export async function approveReview( refId: string, overridePayload?: ReviewOverridePayload, signal?: AbortSignal -): Promise<{ - status: string - error_message?: string - promotion_summary?: PromotionSummary | null -}> { +): Promise { if (isMocksEnabled()) { const store = getMockReviewsStore() const review = store.find((r) => r.ref_id === refId) @@ -1162,10 +1173,10 @@ export async function approveReview( review.decided_at = new Date().toISOString() review.decided_by = "mock-admin" } - return { status: "approved" } + return { status: "Success", review } } const body = overridePayload ? { override_payload: overridePayload } : {} - return api.post<{ status: string; error_message?: string }>( + return api.post( `/v2/reviews/${refId}/approve`, body, undefined, @@ -1177,7 +1188,7 @@ export async function dismissReview( refId: string, reason?: string, signal?: AbortSignal -): Promise<{ status: string }> { +): Promise { if (isMocksEnabled()) { const store = getMockReviewsStore() const review = store.find((r) => r.ref_id === refId) @@ -1187,9 +1198,9 @@ export async function dismissReview( review.decided_by = "mock-admin" if (reason) review.dismissal_reason = reason } - return { status: "dismissed" } + return { status: "Success", review } } - return api.post<{ status: string }>( + return api.post( `/v2/reviews/${refId}/dismiss`, { reason }, undefined,