diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 55470734b0e..d98f170b4ac 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -83,7 +83,9 @@ vi.mock("../lib/assignees", () => ({ })); vi.mock("./StatusIcon", () => ({ - StatusIcon: ({ status }: { status: string }) => {status}, + StatusIcon: ({ status, blockerAttention }: { status: string; blockerAttention?: Issue["blockerAttention"] }) => ( + {status} + ), })); vi.mock("./PriorityIcon", () => ({ @@ -392,6 +394,29 @@ describe("IssueProperties", () => { act(() => root.unmount()); }); + it("passes blocker attention to the sidebar status icon", async () => { + const root = renderProperties(container, { + issue: createIssue({ + status: "blocked", + blockerAttention: { + state: "covered", + reason: "active_child", + unresolvedBlockerCount: 1, + coveredBlockerCount: 1, + attentionBlockerCount: 0, + sampleBlockerIdentifier: "PAP-2", + }, + }), + childIssues: [], + onUpdate: vi.fn(), + }); + await flush(); + + expect(container.querySelector('[data-status-icon-state="covered"]')?.textContent).toBe("blocked"); + + act(() => root.unmount()); + }); + it("renders blocked-by issues as direct chips and edits them from an add action", async () => { const onUpdate = vi.fn(); mockIssuesApi.list.mockResolvedValue([ diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index 27a49bbfba8..c11631a84cb 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -1044,6 +1044,7 @@ export function IssueProperties({ onUpdate({ status })} showLabel /> diff --git a/ui/src/lib/issueDetailBreadcrumb.ts b/ui/src/lib/issueDetailBreadcrumb.ts index 3440fd6aadb..91eae1bc473 100644 --- a/ui/src/lib/issueDetailBreadcrumb.ts +++ b/ui/src/lib/issueDetailBreadcrumb.ts @@ -12,6 +12,7 @@ export type IssueDetailHeaderSeed = { identifier: string | null; title: string; status: Issue["status"]; + blockerAttention?: Issue["blockerAttention"]; priority: Issue["priority"]; projectId: string | null; projectName: string | null; @@ -47,11 +48,15 @@ function isIssueDetailHeaderSeed(value: unknown): value is IssueDetailHeaderSeed candidate.originKind === undefined || typeof candidate.originKind === "string"; const hasOriginId = candidate.originId === undefined || candidate.originId === null || typeof candidate.originId === "string"; + const hasBlockerAttention = + candidate.blockerAttention === undefined + || (typeof candidate.blockerAttention === "object" && candidate.blockerAttention !== null); return ( typeof candidate.id === "string" && (candidate.identifier === null || typeof candidate.identifier === "string") && typeof candidate.title === "string" && typeof candidate.status === "string" + && hasBlockerAttention && typeof candidate.priority === "string" && (candidate.projectId === null || typeof candidate.projectId === "string") && (candidate.projectName === null || typeof candidate.projectName === "string") @@ -66,6 +71,7 @@ function createIssueDetailHeaderSeed(issue: Issue): IssueDetailHeaderSeed { identifier: issue.identifier ?? null, title: issue.title, status: issue.status, + blockerAttention: issue.blockerAttention, priority: issue.priority, projectId: issue.projectId ?? null, projectName: issue.project?.name ?? null, diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index bd47590b302..6ecfcac486e 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -229,7 +229,9 @@ vi.mock("../components/ScrollToBottom", () => ({ })); vi.mock("../components/StatusIcon", () => ({ - StatusIcon: ({ status }: { status: string }) => {status}, + StatusIcon: ({ status, blockerAttention }: { status: string; blockerAttention?: Issue["blockerAttention"] }) => ( + {status} + ), })); vi.mock("../components/PriorityIcon", () => ({ @@ -814,6 +816,31 @@ describe("IssueDetail", () => { expect(consoleErrorSpy).not.toHaveBeenCalled(); }); + it("passes blocker attention to the issue detail header status icon", async () => { + mockIssuesApi.get.mockResolvedValue(createIssue({ + status: "blocked", + blockerAttention: { + state: "covered", + reason: "active_child", + unresolvedBlockerCount: 1, + coveredBlockerCount: 1, + attentionBlockerCount: 0, + sampleBlockerIdentifier: "PAP-2", + }, + })); + + await act(async () => { + root.render( + + + , + ); + }); + await flushReact(); + + expect(container.querySelector('[data-status-icon-state="covered"]')?.textContent).toBe("blocked"); + }); + it("refreshes subtree pause state after resuming a hold", async () => { const childIssue = createIssue({ id: "child-1", diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 2bffbfd18a6..053181c91b1 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -382,7 +382,7 @@ function IssueDetailLoadingState({
{headerSeed ? ( <> - + {identifier ? ( {identifier} @@ -2853,6 +2853,7 @@ export function IssueDetail() {
updateIssue.mutate({ status })} />