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"; import { Button } from "@/components/ui/button"; const allStatuses = ["backlog", "todo", "in_progress", "in_review", "done", "cancelled", "blocked"]; function statusLabel(status: string): string { return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } interface StatusIconProps { status: string; blockerAttention?: IssueBlockerAttention | null; onChange?: (status: string) => void; className?: string; showLabel?: boolean; } function blockedAttentionLabel(blockerAttention: IssueBlockerAttention | null | undefined) { if (!blockerAttention || blockerAttention.state === "none") return "Blocked"; if (blockerAttention.reason === "active_child") { const count = blockerAttention.coveredBlockerCount; if (count === 1 && blockerAttention.sampleBlockerIdentifier) { return `Blocked · waiting on active sub-issue ${blockerAttention.sampleBlockerIdentifier}`; } if (count === 1) return "Blocked · waiting on 1 active sub-issue"; return `Blocked · waiting on ${count} active sub-issues`; } if (blockerAttention.reason === "active_dependency") { const count = blockerAttention.coveredBlockerCount; if (count === 1 && blockerAttention.sampleBlockerIdentifier) { return `Blocked · covered by active dependency ${blockerAttention.sampleBlockerIdentifier}`; } if (count === 1) return "Blocked · covered by 1 active dependency"; return `Blocked · covered by ${count} active dependencies`; } if (blockerAttention.reason === "stalled_review") { const count = blockerAttention.stalledBlockerCount; const leaf = blockerAttention.sampleStalledBlockerIdentifier ?? blockerAttention.sampleBlockerIdentifier; if (count === 1 && leaf) return `Blocked · review stalled on ${leaf}`; if (count === 1) return "Blocked · review stalled with no clear next step"; return `Blocked · ${count} reviews stalled with no clear next step`; } if (blockerAttention.reason === "attention_required") { const count = blockerAttention.attentionBlockerCount || blockerAttention.unresolvedBlockerCount; const attentionCopy = `${count} ${count === 1 ? "blocker needs" : "blockers need"} attention`; const coveredCount = blockerAttention.coveredBlockerCount; if (coveredCount > 0) { return `Blocked · ${attentionCopy}; ${coveredCount} covered by active work`; } return `Blocked · ${attentionCopy}`; } return "Blocked"; } export function StatusIcon({ status, blockerAttention, onChange, className, showLabel }: StatusIconProps) { const [open, setOpen] = useState(false); const isCoveredBlocked = status === "blocked" && blockerAttention?.state === "covered"; const isStalledBlocked = status === "blocked" && blockerAttention?.state === "stalled"; const isAttentionBlocked = status === "blocked" && blockerAttention?.state === "needs_attention"; const hasCoveredBlockedWork = isAttentionBlocked && (blockerAttention?.coveredBlockerCount ?? 0) > 0; const colorClass = isCoveredBlocked ? "text-cyan-600 border-cyan-600 dark:text-cyan-400 dark:border-cyan-400" : isStalledBlocked ? "text-amber-600 border-amber-600 dark:text-amber-400 dark:border-amber-400" : issueStatusIcon[status] ?? issueStatusIconDefault; const isDone = status === "done"; const ariaLabel = status === "blocked" ? blockedAttentionLabel(blockerAttention) : statusLabel(status); const blockerAttentionState = isCoveredBlocked ? "covered" : isStalledBlocked ? "stalled" : isAttentionBlocked ? "needs_attention" : undefined; const circle = ( {isDone && ( )} {isCoveredBlocked && ( )} {hasCoveredBlockedWork && ( )} {isStalledBlocked && ( )} ); if (!onChange) return showLabel ? {circle}{statusLabel(status)} : circle; const trigger = showLabel ? ( ) : circle; return ( {trigger} {allStatuses.map((s) => ( ))} ); }