import type { ReactNode } from "react"; import type { ExternalObjectSummary, Issue, IssueRecoveryAction } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { Archive, Eye, Flag } from "lucide-react"; import { createIssueDetailPath, rememberIssueDetailLocationState, withIssueDetailHeaderSeed, } from "../lib/issueDetailBreadcrumb"; import { cn } from "../lib/utils"; import { deriveActiveRecoveryDisplayState, RECOVERY_CHIP_DEFAULT_TONE, recoveryChipLabel, } from "../lib/recovery-display"; import { StatusIcon } from "./StatusIcon"; import { productivityReviewTriggerLabel } from "./ProductivityReviewBadge"; import { hasAssignedBacklogBlocker } from "../lib/issue-blockers"; import { ExternalObjectStatusSummary } from "./ExternalObjectStatusSummary"; import { Badge } from "@/components/ui/badge"; type UnreadState = "hidden" | "visible" | "fading"; interface IssueRowProps { issue: Issue; issueLinkState?: unknown; selected?: boolean; mobileLeading?: ReactNode; desktopMetaLeading?: ReactNode; desktopLeadingSpacer?: boolean; mobileMeta?: ReactNode; desktopTrailing?: ReactNode; /** * Optional pre-fetched external-object summary. Renders a compact severity * marker before the rest of `desktopTrailing` on desktop only. */ externalObjectSummary?: ExternalObjectSummary | null; trailingMeta?: ReactNode; titleSuffix?: ReactNode; titleClassName?: string; checklistStepNumber?: number | string | null; checklistCurrentStep?: boolean; checklistDependencyChips?: ReactNode; checklistRowId?: string; unreadState?: UnreadState | null; onMarkRead?: () => void; onArchive?: () => void; archiveDisabled?: boolean; className?: string; /** Pointer entered the row (used by list keyboard nav to track hover). */ onMouseEnter?: () => void; /** Ancestor levels; renders that many vertical tree-guide slots (desktop). */ treeGuides?: number; /** * This row has its own collapse chevron sitting in the innermost guide * column (a nested parent). Breaks the guide line there so the chevron is * not crossed out by it. */ chevronInGuide?: boolean; /** Suppress the row divider (parents with expanded children keep visual attachment to their subtree). */ hideDivider?: boolean; } export function IssueRow({ issue, issueLinkState, selected = false, mobileLeading, desktopMetaLeading, desktopLeadingSpacer = false, mobileMeta, desktopTrailing, externalObjectSummary, trailingMeta, titleSuffix, titleClassName, checklistStepNumber = null, checklistCurrentStep = false, checklistDependencyChips, checklistRowId, unreadState = null, onMarkRead, onArchive, archiveDisabled, className, onMouseEnter, treeGuides = 0, chevronInGuide = false, hideDivider = false, }: IssueRowProps) { const issuePathId = issue.identifier ?? issue.id; const identifier = issue.identifier ?? issue.id.slice(0, 8); const showUnreadDot = unreadState === "visible" || unreadState === "fading"; const selectedStatusClass = selected ? "!text-muted-foreground !border-muted-foreground" : undefined; const detailState = withIssueDetailHeaderSeed(issueLinkState, issue); const productivityReview = issue.productivityReview ?? null; const productivityReviewIndicator = productivityReview ? ( ) : null; const hasChecklistStep = checklistStepNumber !== null; const checklistStep = hasChecklistStep ? ( ) : null; const recoveryAction = issue.activeRecoveryAction ?? null; const recoveryIndicator = recoveryAction ? renderRecoveryChip(recoveryAction, selected) : null; const parkedBlockerIndicator = hasAssignedBacklogBlocker(issue.blockedBy) ? ( Blocked by parked work ) : null; return ( rememberIssueDetailLocationState(issuePathId, detailState)} onMouseEnter={onMouseEnter} className={cn( // No color transition on the row band: hover/selection must snap // instantly. A fade (transition-colors) leaves a trail of fading bands // when scrubbing the mouse fast across the list. "group flex items-start gap-2 rounded-lg py-2.5 pl-2 pr-3 text-sm no-underline text-inherit sm:items-center sm:py-2 sm:pl-1", !hideDivider && "border-b border-border last:border-b-0", selected ? "hover:bg-transparent" : "hover:bg-accent/50", checklistCurrentStep ? "bg-primary/5" : null, className, )} > {mobileLeading ?? } {productivityReviewIndicator} {parkedBlockerIndicator} {recoveryIndicator} {issue.title}{titleSuffix} {checklistDependencyChips ? ( {checklistDependencyChips} ) : null} {treeGuides > 0 ? Array.from({ length: treeGuides }, (_, level) => { // The innermost guide lands on THIS row's own chevron column; if // the row has a chevron, break the line around it so it isn't // crossed out. const gapForChevron = chevronInGuide && level === treeGuides - 1; return ( // Tree guide: occupies the same flex slot as the parent's // chevron column so the line lands under the parent's status // column; stretched past the row padding so consecutive rows // read as one continuous line.