'use client'; /** * IssueTable Component * * Sortable table displaying issues with selection support. * * @module features/issues/components/IssueTable */ import { ChevronUp, ChevronDown, MoreVertical, Bot, User, CircleDot } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Card } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import type { IssueSummary, IssueSort, IssueSortField, IssueSortDirection } from '../types'; /** * Convert our sort direction to ARIA sort value */ function toAriaSortValue( field: IssueSortField, currentField: IssueSortField, direction: IssueSortDirection ): 'ascending' | 'descending' | 'none' | undefined { if (field !== currentField) return undefined; return direction === 'asc' ? 'ascending' : 'descending'; } import { StatusBadge } from './StatusBadge'; import { PriorityBadge } from './PriorityBadge'; import { SyncStatusIndicator } from './SyncStatusIndicator'; interface IssueTableProps { issues: IssueSummary[]; selectedIssues: string[]; onSelectionChange: (ids: string[]) => void; onIssueClick: (id: string) => void; sort: IssueSort; onSortChange: (sort: IssueSort) => void; className?: string; } export function IssueTable({ issues, selectedIssues, onSelectionChange, onIssueClick, sort, onSortChange, className, }: IssueTableProps) { const handleSelectAll = () => { if (selectedIssues.length === issues.length) { onSelectionChange([]); } else { onSelectionChange(issues.map((i) => i.id)); } }; const handleSelectIssue = (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (selectedIssues.includes(id)) { onSelectionChange(selectedIssues.filter((i) => i !== id)); } else { onSelectionChange([...selectedIssues, id]); } }; const handleSort = (field: IssueSortField) => { if (sort.field === field) { onSortChange({ field, direction: sort.direction === 'asc' ? 'desc' : 'asc', }); } else { onSortChange({ field, direction: 'desc' }); } }; const SortIcon = ({ field }: { field: IssueSortField }) => { if (sort.field !== field) return null; return sort.direction === 'asc' ? ( ) : ( ); }; const allSelected = selectedIssues.length === issues.length && issues.length > 0; const someSelected = selectedIssues.length > 0 && !allSelected; return ( { if (el) { (el as unknown as HTMLInputElement).indeterminate = someSelected; } }} onCheckedChange={handleSelectAll} aria-label={allSelected ? 'Deselect all issues' : 'Select all issues'} /> handleSort('number')} role="button" tabIndex={0} aria-sort={toAriaSortValue('number', sort.field, sort.direction)} onKeyDown={(e) => e.key === 'Enter' && handleSort('number')} > # Title Status handleSort('priority')} role="button" tabIndex={0} aria-sort={toAriaSortValue('priority', sort.field, sort.direction)} onKeyDown={(e) => e.key === 'Enter' && handleSort('priority')} > Priority Assignee Sprint Sync Actions {issues.map((issue) => ( onIssueClick(issue.id)} data-testid={`issue-row-${issue.id}`} > handleSelectIssue(issue.id, e)}> {}} aria-label={`Select issue ${issue.number}`} /> {issue.number} {issue.title} {issue.labels.slice(0, 3).map((label) => ( {label} ))} {issue.labels.length > 3 && ( +{issue.labels.length - 3} )} {issue.assignee ? ( {issue.assignee.type === 'agent' ? ( ) : ( )} {issue.assignee.name} ) : ( Unassigned )} {issue.sprint ? ( {issue.sprint} ) : ( Backlog )} e.stopPropagation()}> onIssueClick(issue.id)}> View Details Edit Assign Sync with Tracker Delete ))} {issues.length === 0 && ( No issues found Try adjusting your search or filters )} ); }
{issue.title}
Try adjusting your search or filters