diff --git a/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx b/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx index bc2740d1b5..47cd55a9c8 100644 --- a/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx +++ b/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx @@ -9,6 +9,7 @@ import { releasePortfolioQueueItem, requeuePortfolioQueueItem, type PortfolioQueueActionItem, + type PortfolioQueueActionResult, } from "./lib/portfolio-queue-actions"; import { PortfolioPage, PortfolioQueueActionsSection } from "./routes/portfolio"; import type { PortfolioQueueResult } from "./lib/portfolio-queue"; @@ -45,6 +46,7 @@ describe("PortfolioQueueActionsSection (#4857)", () => { render( undefined} onRequeue={() => undefined} @@ -57,6 +59,7 @@ describe("PortfolioQueueActionsSection (#4857)", () => { render( undefined} onRequeue={() => undefined} @@ -71,6 +74,7 @@ describe("PortfolioQueueActionsSection (#4857)", () => { render( { render( undefined} onRequeue={() => undefined} @@ -93,6 +98,21 @@ describe("PortfolioQueueActionsSection (#4857)", () => { ); expect((screen.getByRole("button", { name: "Release" }) as HTMLButtonElement).disabled).toBe(true); }); + + it("renders a visible error when a queue action fails (#6090)", () => { + const failed: PortfolioQueueActionResult = { ok: false, error: "queue_entry_not_in_progress" }; + render( + undefined} + onRequeue={() => undefined} + />, + ); + expect(screen.getByRole("alert").textContent).toContain("queue_entry_not_in_progress"); + expect(screen.getByRole("button", { name: "Release" })).toBeTruthy(); + }); }); describe("PortfolioPage queue actions (#4857)", () => { @@ -116,6 +136,28 @@ describe("PortfolioPage queue actions (#4857)", () => { fireEvent.click(screen.getByRole("button", { name: "Release" })); await waitFor(() => expect(releaseItem).toHaveBeenCalledWith(inProgressItem)); }); + + it("REGRESSION (#6090): a failing release action renders the error and does not re-fetch items as if it succeeded", async () => { + const loadPortfolioQueueItems = vi.fn(async () => ({ ok: true as const, items: [inProgressItem] })); + const releaseItem = vi.fn(async (): Promise => ({ + ok: false, + error: "queue_entry_not_in_progress", + })); + render( + , + ); + await waitFor(() => expect(screen.getByRole("button", { name: "Release" })).toBeTruthy()); + expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1); + fireEvent.click(screen.getByRole("button", { name: "Release" })); + await waitFor(() => expect(screen.getByRole("alert").textContent).toContain("queue_entry_not_in_progress")); + expect(releaseItem).toHaveBeenCalledWith(inProgressItem); + expect(loadPortfolioQueueItems).toHaveBeenCalledTimes(1); + }); }); describe("fetchPortfolioQueueItems / release / requeue (#4857)", () => { diff --git a/apps/loopover-miner-ui/src/routes/portfolio.tsx b/apps/loopover-miner-ui/src/routes/portfolio.tsx index 685535c711..f017c75b3c 100644 --- a/apps/loopover-miner-ui/src/routes/portfolio.tsx +++ b/apps/loopover-miner-ui/src/routes/portfolio.tsx @@ -9,8 +9,10 @@ import { fetchPortfolioQueueItems, requeuePortfolioQueueItem, releasePortfolioQueueItem, + type PortfolioQueueActionItem, + type PortfolioQueueActionResult, + type PortfolioQueueItemsResult, } from "../lib/portfolio-queue-actions"; -import type { PortfolioQueueActionItem, PortfolioQueueItemsResult } from "../lib/portfolio-queue-actions"; import { DEFAULT_POLL_INTERVAL_MS, usePolledFetch } from "../lib/use-polled-fetch"; import { fetchPortfolioQueue, type PortfolioQueueResult, type QueueStatus } from "../lib/portfolio-queue"; @@ -94,11 +96,13 @@ export function PortfolioQueueView({ result }: { result: PortfolioQueueResult | export function PortfolioQueueActionsSection({ result, + actionResult, pending, onRelease, onRequeue, }: { result: PortfolioQueueItemsResult | null; + actionResult: PortfolioQueueActionResult | null; pending: boolean; onRelease: (item: PortfolioQueueActionItem) => void; onRequeue: (item: PortfolioQueueActionItem) => void; @@ -106,6 +110,11 @@ export function PortfolioQueueActionsSection({ return (

Queue actions

+ {actionResult !== null && !actionResult.ok ? ( +

+ Queue action failed: {actionResult.error} +

+ ) : null} {result === null ? (

Loading actionable queue items…

) : !result.ok ? ( @@ -168,6 +177,7 @@ export function PortfolioPage({ const [refreshKey, setRefreshKey] = useState(0); const [actionPending, setActionPending] = useState(false); const [itemsResult, setItemsResult] = useState(null); + const [actionResult, setActionResult] = useState(null); const loadSummary = useCallback(() => loadPortfolioQueue(), [loadPortfolioQueue, refreshKey]); const summaryResult = usePolledFetch(loadSummary, pollIntervalMs); @@ -180,11 +190,14 @@ export function PortfolioPage({ refreshItems(); }, [refreshItems]); - const runQueueAction = (action: () => Promise) => { + const runQueueAction = (action: () => Promise) => { setActionPending(true); - void action().then(() => { - setRefreshKey((key) => key + 1); - refreshItems(); + void action().then((next) => { + setActionResult(next); + if (next.ok) { + setRefreshKey((key) => key + 1); + refreshItems(); + } setActionPending(false); }); }; @@ -202,6 +215,7 @@ export function PortfolioPage({ runQueueAction(() => releaseItem(item))} onRequeue={(item) => runQueueAction(() => requeueItem(item))}