Add covered blocked issue attention state

This commit is contained in:
Dotta committed 2026-04-24 13:39:10 -05:00
1 parent 9cac22ecbd
commit f0c22b5ddb
14 files changed
+637 -13

No files matched your search

+1 -1
View File
@@ -150,7 +150,7 @@ export function InboxIssueMetaLeading({
<>
{showStatus ? (
<span className="hidden shrink-0 sm:inline-flex">
{statusSlot ?? <StatusIcon status={issue.status} />}
{statusSlot ?? <StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} />}
</span>
) : null}
{showIdentifier ? (
+1 -1
View File
@@ -44,7 +44,7 @@ export function IssueQuicklookCard({
return (
<div className={cn("space-y-2", compact && "space-y-1.5")}>
<div className="flex items-start gap-2">
<StatusIcon status={issue.status} className="mt-0.5 shrink-0" />
<StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} className="mt-0.5 shrink-0" />
<RouterDom.Link
to={linkTo}
state={linkState ?? withIssueDetailHeaderSeed(null, issue)}
+2 -2
View File
@@ -69,7 +69,7 @@ export function IssueRow({
)}
>
<span className="shrink-0 pt-px sm:hidden">
{mobileLeading ?? <StatusIcon status={issue.status} className={selectedStatusClass} />}
{mobileLeading ?? <StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} className={selectedStatusClass} />}
</span>
<span className="flex min-w-0 flex-1 flex-col gap-1 sm:contents">
<span className="line-clamp-2 text-sm sm:order-2 sm:min-w-0 sm:flex-1 sm:truncate sm:line-clamp-none">
@@ -82,7 +82,7 @@ export function IssueRow({
{desktopMetaLeading ?? (
<>
<span className="hidden shrink-0 sm:inline-flex">
<StatusIcon status={issue.status} className={selectedStatusClass} />
<StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} className={selectedStatusClass} />
</span>
<span className="shrink-0 font-mono text-xs text-muted-foreground">
{identifier}
+2 -2
View File
@@ -1087,7 +1087,7 @@ export function IssuesList({
</button>
) : (
<span onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
<StatusIcon status={issue.status} onChange={(s) => onUpdateIssue(issue.id, { status: s })} />
<StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} onChange={(s) => onUpdateIssue(issue.id, { status: s })} />
</span>
)
}
@@ -1111,7 +1111,7 @@ export function IssuesList({
showIdentifier={visibleIssueColumnSet.has("id") && availableIssueColumnSet.has("id")}
statusSlot={(
<span onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
<StatusIcon status={issue.status} onChange={(s) => onUpdateIssue(issue.id, { status: s })} />
<StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} onChange={(s) => onUpdateIssue(issue.id, { status: s })} />
</span>
)}
/>
+48
View File
@@ -0,0 +1,48 @@
// @vitest-environment node
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { StatusIcon } from "./StatusIcon";
describe("StatusIcon", () => {
it("renders covered blocked issues with a non-red attention state label", () => {
const html = renderToStaticMarkup(
<StatusIcon
status="blocked"
blockerAttention={{
state: "covered",
reason: "active_child",
unresolvedBlockerCount: 1,
coveredBlockerCount: 1,
attentionBlockerCount: 0,
sampleBlockerIdentifier: "PAP-2",
}}
/>,
);
expect(html).toContain('data-blocker-attention-state="covered"');
expect(html).toContain('aria-label="Blocked, waiting on running sub-issue"');
expect(html).toContain("border-cyan-600");
expect(html).not.toContain("border-red-600");
});
it("keeps normal blocked issues on the attention-required visual", () => {
const html = renderToStaticMarkup(
<StatusIcon
status="blocked"
blockerAttention={{
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 1,
coveredBlockerCount: 0,
attentionBlockerCount: 1,
sampleBlockerIdentifier: "PAP-2",
}}
/>,
);
expect(html).not.toContain('data-blocker-attention-state="covered"');
expect(html).toContain('aria-label="Blocked"');
expect(html).toContain("border-red-600");
});
});
+20 -2
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import type { IssueBlockerAttention } from "@paperclipai/shared";
import { cn } from "../lib/utils";
import { issueStatusIcon, issueStatusIconDefault } from "../lib/status-colors";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -12,15 +13,26 @@ function statusLabel(status: string): string {
interface StatusIconProps {
status: string;
blockerAttention?: IssueBlockerAttention | null;
onChange?: (status: string) => void;
className?: string;
showLabel?: boolean;
}
export function StatusIcon({ status, onChange, className, showLabel }: StatusIconProps) {
function coveredBlockedLabel(blockerAttention: IssueBlockerAttention | null | undefined) {
return blockerAttention?.reason === "active_child"
? "Blocked, waiting on running sub-issue"
: "Blocked, covered by active dependency";
}
export function StatusIcon({ status, blockerAttention, onChange, className, showLabel }: StatusIconProps) {
const [open, setOpen] = useState(false);
const colorClass = issueStatusIcon[status] ?? issueStatusIconDefault;
const isCoveredBlocked = status === "blocked" && blockerAttention?.state === "covered";
const colorClass = isCoveredBlocked
? "text-cyan-600 border-cyan-600 dark:text-cyan-400 dark:border-cyan-400"
: issueStatusIcon[status] ?? issueStatusIconDefault;
const isDone = status === "done";
const ariaLabel = isCoveredBlocked ? coveredBlockedLabel(blockerAttention) : statusLabel(status);
const circle = (
<span
@@ -30,10 +42,16 @@ export function StatusIcon({ status, onChange, className, showLabel }: StatusIco
onChange && !showLabel && "cursor-pointer",
className
)}
data-blocker-attention-state={isCoveredBlocked ? "covered" : undefined}
aria-label={ariaLabel}
title={ariaLabel}
>
{isDone && (
<span className="absolute inset-0 m-auto h-2 w-2 rounded-full bg-current" />
)}
{isCoveredBlocked && (
<span className="absolute -bottom-0.5 -right-0.5 h-2 w-2 rounded-full border border-background bg-current" />
)}
</span>
);
+2 -2
View File
@@ -356,7 +356,7 @@ export function Dashboard() {
<div className="flex items-start gap-2 sm:items-center sm:gap-3">
{/* Status icon - left column on mobile */}
<span className="shrink-0 sm:hidden">
<StatusIcon status={issue.status} />
<StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} />
</span>
{/* Right column on mobile: title + metadata stacked */}
@@ -365,7 +365,7 @@ export function Dashboard() {
{issue.title}
</span>
<span className="flex items-center gap-2 sm:order-1 sm:shrink-0">
<span className="hidden sm:inline-flex"><StatusIcon status={issue.status} /></span>
<span className="hidden sm:inline-flex"><StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} /></span>
<span className="text-xs font-mono text-muted-foreground">
{issue.identifier ?? issue.id.slice(0, 8)}
</span>
+1 -1
View File
@@ -56,7 +56,7 @@ export function MyIssues() {
title={issue.title}
to={`/issues/${issue.identifier ?? issue.id}`}
leading={
<StatusIcon status={issue.status} />
<StatusIcon status={issue.status} blockerAttention={issue.blockerAttention} />
}
trailing={
<span className="text-xs text-muted-foreground">