mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
Add covered blocked issue attention state
This commit is contained in:
1 parent
9cac22ecbd
commit
f0c22b5ddb
14 files changed
+637
-13
No files matched your search
@@ -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 ? (
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user