From 34159d668daef25c422d31b30467e9f581eec7aa Mon Sep 17 00:00:00 2001
From: luciferlive112116 <291889058+luciferlive112116@users.noreply.github.com>
Date: Thu, 16 Jul 2026 23:50:03 +0800
Subject: [PATCH] refactor(miner-ui): adopt shared StateBoundary in the
portfolio route (#6511)
Replace the hand-rolled loading/error/empty
tags in PortfolioQueueView and
PortfolioQueueActionsSection with the ui-kit StateBoundary, plus content-shaped
Skeleton placeholders so the layout does not jump when the 10s poll lands.
Each user-visible sentence is passed as the whole EmptyState/ErrorState title with
the description suppressed, so the rendered copy is unchanged from the
tags it
replaces. Each fetch keeps its own boundary: the summary and queue-actions reads are
independent, so one failing must not blank the other.
Closes #6511
---
.../src/portfolio-queue-actions.test.tsx | 26 ++-
.../src/portfolio-queue.test.tsx | 26 ++-
.../src/routes/portfolio.tsx | 221 +++++++++++-------
3 files changed, 181 insertions(+), 92 deletions(-)
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 47cd55a9c8..37b23555fe 100644
--- a/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx
+++ b/apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx
@@ -42,7 +42,9 @@ const doneItem: PortfolioQueueActionItem = {
};
describe("PortfolioQueueActionsSection (#4857)", () => {
- it("renders the loading state before the first result arrives", () => {
+ it("renders a content-shaped skeleton before the first result arrives", () => {
+ // #6511: StateBoundary renders the skeleton INSTEAD of a loading title, so the old
+ // "Loading actionable queue items…" text is intentionally gone; assert the placeholder instead.
render(
{
onRequeue={() => undefined}
/>,
);
- expect(screen.getByText(/Loading actionable queue items/i)).toBeTruthy();
+ expect(screen.getByTestId("queue-actions-skeleton")).toBeTruthy();
+ // Shaped like the real content, not one generic bar: the real table is not rendered yet.
+ expect(screen.queryByRole("table")).toBeNull();
+ });
+
+ it("renders the empty-state sentence verbatim, with no extra copy from the shared boundary", () => {
+ // #6511: the whole original sentence is the EmptyState title and the description is suppressed, so the
+ // rendered copy is byte-identical to the
it replaced -- not a reworded title/description split, and
+ // none of StateBoundary's own default "This view has no records to show." boilerplate.
+ render(
+ undefined}
+ onRequeue={() => undefined}
+ />,
+ );
+ expect(screen.getByText("No in-progress or completed items to release or requeue right now.")).toBeTruthy();
+ expect(screen.queryByText(/This view has no records to show/i)).toBeNull();
+ expect(screen.queryByRole("table")).toBeNull();
});
it("renders an error message when the local API is unreachable", () => {
diff --git a/apps/loopover-miner-ui/src/portfolio-queue.test.tsx b/apps/loopover-miner-ui/src/portfolio-queue.test.tsx
index 885240657c..e683090248 100644
--- a/apps/loopover-miner-ui/src/portfolio-queue.test.tsx
+++ b/apps/loopover-miner-ui/src/portfolio-queue.test.tsx
@@ -90,7 +90,13 @@ describe("PortfolioQueueView (#4306, per-repo detail added by #4846)", () => {
it("renders the fresh-install empty state without erroring", () => {
render();
- expect(screen.getByText(/No queued work yet/i)).toBeTruthy();
+ // #6511: asserted as the exact sentence, not a loose regex -- the whole original string is the EmptyState
+ // title with the description suppressed, so the rendered copy is byte-identical to the
it replaced.
+ expect(
+ screen.getByText("No queued work yet — the cards fill in once the miner enqueues its first portfolio item."),
+ ).toBeTruthy();
+ // And none of StateBoundary's own default empty boilerplate leaks in alongside it.
+ expect(screen.queryByText(/This view has no records to show/i)).toBeNull();
expect(screen.queryByRole("table")).toBeNull();
});
@@ -99,9 +105,23 @@ describe("PortfolioQueueView (#4306, per-repo detail added by #4846)", () => {
expect(screen.getByRole("alert").textContent).toContain("connection refused");
});
- it("renders the loading state before the first result arrives", () => {
+ it("renders a content-shaped skeleton before the first result arrives", () => {
+ // #6511: StateBoundary renders the skeleton INSTEAD of a loading title, so the old
+ // "Loading local portfolio queue…" text is intentionally gone; assert the placeholder instead.
render();
- expect(screen.getByText(/Loading local portfolio queue/i)).toBeTruthy();
+ expect(screen.getByTestId("portfolio-queue-skeleton")).toBeTruthy();
+ // Shaped like the real content, not one generic bar: the real table is not rendered yet.
+ expect(screen.queryByRole("table")).toBeNull();
+ });
+
+ it("renders the error sentence verbatim, with no extra copy from the shared boundary", () => {
+ // #6511: same whole-sentence treatment on the error path -- one string, description suppressed, so none of
+ // ErrorState's own "Something went wrong fetching this data." default appears next to it.
+ render();
+ expect(screen.getByRole("alert").textContent).toContain(
+ "Could not read the local portfolio queue: connection refused",
+ );
+ expect(screen.queryByText(/Something went wrong fetching this data/i)).toBeNull();
});
});
diff --git a/apps/loopover-miner-ui/src/routes/portfolio.tsx b/apps/loopover-miner-ui/src/routes/portfolio.tsx
index f017c75b3c..5188fc4f64 100644
--- a/apps/loopover-miner-ui/src/routes/portfolio.tsx
+++ b/apps/loopover-miner-ui/src/routes/portfolio.tsx
@@ -3,6 +3,8 @@ import { useCallback, useEffect, useState } from "react";
import { Button } from "@loopover/ui-kit/components/button";
import { Card, CardContent, CardHeader } from "@loopover/ui-kit/components/card";
+import { Skeleton } from "@loopover/ui-kit/components/skeleton";
+import { StateBoundary } from "@loopover/ui-kit/components/state-views";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@loopover/ui-kit/components/table";
import {
@@ -35,61 +37,100 @@ const STATUS_TONE: Record = {
done: "text-[var(--success)]",
};
-export function PortfolioQueueView({ result }: { result: PortfolioQueueResult | null }) {
- if (result === null) {
- return
Loading local portfolio queue…
;
- }
- if (!result.ok) {
- return (
-
- Could not read the local portfolio queue: {result.error}
-
- No queued work yet — the cards fill in once the miner enqueues its first portfolio item.
-
- );
- }
+/** Placeholder shaped like the real summary -- three status cards over the repo table -- so the layout doesn't
+ * jump when the 10s poll lands. A single generic bar would just move the jump later. */
+function PortfolioQueueSkeleton() {
return (
-
+
{(Object.keys(STATUS_LABELS) as QueueStatus[]).map((status) => (
-
+ );
+}
+
+export function PortfolioQueueView({ result }: { result: PortfolioQueueResult | null }) {
+ const summary = result?.ok ? result.summary : null;
+ return (
+ }
+ // Each message is passed as the WHOLE original sentence with the description suppressed, rather than
+ // split across title/description: the issue requires the user-visible strings not be reworded, and Shell
+ // renders `{description && ...}` so an empty one adds nothing. The rendered text is byte-identical to the
+ //
tags this replaces. ErrorState emits role="alert" itself, so failures still announce the same way.
+ errorTitle={
+ result !== null && !result.ok ? `Could not read the local portfolio queue: ${result.error}` : undefined
+ }
+ errorDescription=""
+ emptyTitle="No queued work yet — the cards fill in once the miner enqueues its first portfolio item."
+ emptyDescription={null}
+ >
+ {summary === null ? null : (
+
+
+ {(Object.keys(STATUS_LABELS) as QueueStatus[]).map((status) => (
+
+
+
+ )}
+
+ );
+}
+
+/** Placeholder shaped like the queue-actions table's rows, for the same reason as the summary's. */
+function QueueActionsSkeleton() {
+ return (
+
- Could not read actionable queue items: {result.error}
-
- ) : result.items.length === 0 ? (
-
- No in-progress or completed items to release or requeue right now.
-
- ) : (
-
-
-
- Repository
- Identifier
- Status
- Action
-
-
-
- {result.items.map((item) => (
-
- {item.repoFullName}
- {item.identifier}
- {STATUS_LABELS[item.status]}
-
- {item.status === "in_progress" ? (
-
- ) : (
-
- )}
-
+ {/* Its own boundary, deliberately: this fetch is independent of the summary above, so a failure here
+ must not blank the summary -- and a summary failure must not hide the actions. Same whole-sentence
+ treatment as above, so the empty/error copy stays byte-identical to the
tags it replaces. */}
+ }
+ errorTitle={
+ result !== null && !result.ok ? `Could not read actionable queue items: ${result.error}` : undefined
+ }
+ errorDescription=""
+ emptyTitle="No in-progress or completed items to release or requeue right now."
+ emptyDescription={null}
+ >
+ {result === null || !result.ok || result.items.length === 0 ? null : (
+