import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { UnreadCommentsBadge } from '@renderer/components/team/UnreadCommentsBadge'; import { Button } from '@renderer/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount'; import { REVIEW_STATE_DISPLAY } from '@renderer/utils/memberHelpers'; import { buildTaskChangeRequestOptions, canDisplayTaskChangesForOptions, } from '@renderer/utils/taskChangeRequest'; import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { ArrowLeftFromLine, ArrowRightFromLine, CheckCircle2, Eye, FileCode, FilePenLine, HelpCircle, Play, RotateCcw, Trash2, XCircle, } from 'lucide-react'; import type { KanbanColumnId, KanbanTaskState, TeamTask, TeamTaskWithKanban } from '@shared/types'; interface KanbanTaskCardProps { task: TeamTaskWithKanban; teamName: string; columnId: KanbanColumnId; kanbanTaskState?: KanbanTaskState; hasReviewers: boolean; compact?: boolean; taskMap: Map; memberColorMap: Map; onRequestReview: (taskId: string) => void; onApprove: (taskId: string) => void; onRequestChanges: (taskId: string) => void; onMoveBackToDone: (taskId: string) => void; onStartTask: (taskId: string) => void; onCompleteTask: (taskId: string) => void; onCancelTask: (taskId: string) => void; onScrollToTask?: (taskId: string) => void; onTaskClick?: (task: TeamTask) => void; onViewChanges?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void; } interface DependencyBadgeProps { taskId: string; taskMap: Map; onScrollToTask?: (taskId: string) => void; } const DependencyBadge = ({ taskId, taskMap, onScrollToTask, }: DependencyBadgeProps): React.JSX.Element => { const depTask = taskMap.get(taskId); const isCompleted = depTask?.status === 'completed'; const label = depTask ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` : `#${deriveTaskDisplayId(taskId)}`; return ( {label} ); }; const TruncatedTitle = ({ text, className, }: { text: string; className?: string; }): React.JSX.Element => { const ref = useRef(null); const [isTruncated, setIsTruncated] = useState(false); const checkTruncation = useCallback(() => { const el = ref.current; if (el) { setIsTruncated(el.scrollHeight > el.clientHeight); } }, []); return (
{text}
{text}
); }; const CancelTaskButton = ({ taskId, onConfirm, }: { taskId: string; onConfirm: (taskId: string) => void; }): React.JSX.Element => { const [open, setOpen] = useState(false); return ( Cancel e.stopPropagation()} >

Move this task back to TODO and notify the team?

); }; interface TaskActionIconButtonProps { label: string; icon: React.ReactNode; onClick: (e: React.MouseEvent) => void; className: string; variant?: 'outline' | 'ghost' | 'destructive'; disabled?: boolean; } const TaskActionIconButton = ({ label, icon, onClick, className, variant = 'outline', disabled = false, }: TaskActionIconButtonProps): React.JSX.Element => ( {label} ); export const KanbanTaskCard = memo( function KanbanTaskCard({ task, teamName, columnId, kanbanTaskState, hasReviewers, compact, taskMap, memberColorMap, onRequestReview, onApprove, onRequestChanges, onMoveBackToDone, onStartTask, onCompleteTask, onCancelTask, onScrollToTask, onTaskClick, onViewChanges, onDeleteTask, }: KanbanTaskCardProps): React.JSX.Element { const unreadCount = useUnreadCommentCount(teamName, task.id, task.comments); const blockedByIds = task.blockedBy?.filter((id) => id.length > 0) ?? []; const blocksIds = task.blocks?.filter((id) => id.length > 0) ?? []; const hasBlockedBy = blockedByIds.length > 0; const hasBlocks = blocksIds.length > 0; const taskChangeRequestOptions = useMemo(() => buildTaskChangeRequestOptions(task), [task]); const canDisplay = useMemo( () => canDisplayTaskChangesForOptions(taskChangeRequestOptions) && !!onViewChanges, [taskChangeRequestOptions, onViewChanges] ); const effectiveReviewer = (kanbanTaskState?.reviewer ?? task.reviewer ?? '').trim(); const isReviewManual = columnId === 'review' && !hasReviewers && effectiveReviewer.length === 0; const metaActions = ( <> {canDisplay && task.changePresence === 'has_changes' ? ( } variant="ghost" className="text-sky-400 hover:bg-sky-500/10 hover:text-sky-300" onClick={(e) => { e.stopPropagation(); onViewChanges!(task.id); }} /> ) : canDisplay && task.changePresence === 'no_changes' ? ( No changes ) : null} {onDeleteTask ? ( } variant="ghost" className="text-red-400 hover:bg-red-500/10 hover:text-red-300" onClick={(e) => { e.stopPropagation(); onDeleteTask(task.id); }} /> ) : null} ); return (
onTaskClick?.(task)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onTaskClick?.(task); } }} > {formatTaskDisplayLabel(task)} {task.owner ? ( ) : null}
{!compact && } {task.needsClarification ? ( {task.needsClarification === 'user' ? 'Awaiting user' : 'Awaiting lead'} ) : null} {task.reviewState === 'needsFix' ? ( {REVIEW_STATE_DISPLAY.needsFix.label} ) : null} {compact && }
{hasBlockedBy ? (
Blocked by {blockedByIds.map((id) => ( ))}
) : null} {hasBlocks ? (
Blocks {blocksIds.map((id) => ( ))}
) : null}
{columnId === 'todo' ? ( <> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onStartTask(task.id); }} /> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onCompleteTask(task.id); }} /> ) : null} {columnId === 'in_progress' ? ( <> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onCompleteTask(task.id); }} /> ) : null} {columnId === 'done' ? ( <> } className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onApprove(task.id); }} /> } className="border-violet-500/40 text-violet-400 hover:bg-violet-500/10 hover:text-violet-300" onClick={(e) => { e.stopPropagation(); onRequestReview(task.id); }} /> ) : null} {columnId === 'review' ? (
{isReviewManual ? (
Manual review
) : null}
} className="border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10 hover:text-emerald-300" onClick={(e) => { e.stopPropagation(); onApprove(task.id); }} /> } variant="destructive" className="bg-red-500/90 text-white hover:bg-red-500" onClick={(e) => { e.stopPropagation(); onRequestChanges(task.id); }} />
) : null} {columnId === 'approved' ? ( } className="border-amber-500/40 text-amber-400 hover:bg-amber-500/10 hover:text-amber-300" onClick={(e) => { e.stopPropagation(); onMoveBackToDone(task.id); }} /> ) : null}
{metaActions}
); }, (prev, next) => prev.task === next.task && prev.teamName === next.teamName && prev.columnId === next.columnId && prev.kanbanTaskState === next.kanbanTaskState && prev.hasReviewers === next.hasReviewers && prev.compact === next.compact && prev.taskMap === next.taskMap && prev.memberColorMap === next.memberColorMap && prev.onRequestReview === next.onRequestReview && prev.onApprove === next.onApprove && prev.onRequestChanges === next.onRequestChanges && prev.onMoveBackToDone === next.onMoveBackToDone && prev.onStartTask === next.onStartTask && prev.onCompleteTask === next.onCompleteTask && prev.onCancelTask === next.onCancelTask && prev.onScrollToTask === next.onScrollToTask && prev.onTaskClick === next.onTaskClick && prev.onViewChanges === next.onViewChanges && prev.onDeleteTask === next.onDeleteTask );