import { useEffect, useMemo } from "react"; import Avatar from "@/components/avatar"; import { useSelection } from "@/components/selection-provider"; import StatusTag from "@/components/status-tag"; import Icon, { type IconName } from "@/components/ui/icon"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { useIssues, useSelectedOrganisation, useSelectedProject } from "@/lib/query/hooks"; import { cn } from "@/lib/utils"; export type IssuesTableFilters = { query: string; statuses: string[]; types: string[]; assignees: string[]; sprintId: "all" | "none" | number; sort: "newest" | "oldest" | "title-asc" | "title-desc" | "status"; }; export const defaultIssuesTableFilters: IssuesTableFilters = { query: "", statuses: [], types: [], assignees: [], sprintId: "all", sort: "newest", }; export function IssuesTable({ columns = {}, className, filters, }: { columns?: { id?: boolean; title?: boolean; description?: boolean; status?: boolean; assignee?: boolean }; className: string; filters?: IssuesTableFilters; }) { const { selectedProjectId, selectedIssueId, selectIssue } = useSelection(); const { data: issuesData = [] } = useIssues(selectedProjectId); const selectedOrganisation = useSelectedOrganisation(); const selectedProject = useSelectedProject(); const statuses = selectedOrganisation?.Organisation.statuses ?? {}; const issueTypes = (selectedOrganisation?.Organisation.issueTypes ?? {}) as Record< string, { icon: string; color: string } >; const issues = useMemo(() => { const query = filters?.query?.trim().toLowerCase() ?? ""; const queryIsNumber = query !== "" && /^[0-9]+$/.test(query); const statusSet = new Set(filters?.statuses ?? []); const typeSet = new Set(filters?.types ?? []); const assigneeFilters = filters?.assignees ?? []; const includeUnassigned = assigneeFilters.includes("unassigned"); const assigneeIds = new Set( assigneeFilters .filter((assignee) => assignee !== "unassigned") .map((assignee) => Number.parseInt(assignee, 10)) .filter((assigneeId) => !Number.isNaN(assigneeId)), ); const sprintFilter = filters?.sprintId ?? "all"; const sort = filters?.sort ?? "newest"; let next = [...issuesData]; if (query) { next = next.filter((issueData) => { const title = issueData.Issue.title.toLowerCase(); const description = issueData.Issue.description.toLowerCase(); const matchesText = title.includes(query) || description.includes(query); if (matchesText) return true; if (queryIsNumber) { return issueData.Issue.number.toString().includes(query); } return false; }); } if (statusSet.size > 0) { next = next.filter((issueData) => statusSet.has(issueData.Issue.status)); } if (typeSet.size > 0) { next = next.filter((issueData) => typeSet.has(issueData.Issue.type)); } if (assigneeFilters.length > 0) { next = next.filter((issueData) => { const hasAssignees = issueData.Assignees && issueData.Assignees.length > 0; const matchesAssigned = hasAssignees && issueData.Assignees.some((assignee) => assigneeIds.has(assignee.id)); const matchesUnassigned = includeUnassigned && !hasAssignees; return matchesAssigned || matchesUnassigned; }); } if (sprintFilter !== "all") { if (sprintFilter === "none") { next = next.filter((issueData) => issueData.Issue.sprintId == null); } else { next = next.filter((issueData) => issueData.Issue.sprintId === sprintFilter); } } switch (sort) { case "oldest": next.sort((a, b) => a.Issue.number - b.Issue.number); break; case "title-asc": next.sort((a, b) => a.Issue.title.localeCompare(b.Issue.title)); break; case "title-desc": next.sort((a, b) => b.Issue.title.localeCompare(a.Issue.title)); break; case "status": next.sort((a, b) => a.Issue.status.localeCompare(b.Issue.status)); break; default: next.sort((a, b) => b.Issue.number - a.Issue.number); break; } return next; }, [issuesData, filters]); useEffect(() => { if (selectedIssueId == null) return; const isVisible = issues.some((issueData) => issueData.Issue.id === selectedIssueId); if (!isVisible) { selectIssue(null); } }, [issues, selectedIssueId, selectIssue]); const getIssueUrl = (issueNumber: number) => { if (!selectedOrganisation || !selectedProject) return "#"; const params = new URLSearchParams(); params.set("o", selectedOrganisation.Organisation.slug.toLowerCase()); params.set("p", selectedProject.Project.key.toLowerCase()); params.set("i", issueNumber.toString()); return `/issues?${params.toString()}`; }; const handleLinkClick = (e: React.MouseEvent) => { if (e.metaKey || e.ctrlKey || e.shiftKey) { e.stopPropagation(); return; } e.preventDefault(); }; const showId = columns.id == null || columns.id === true; const showTitle = columns.title == null || columns.title === true; const showDescription = columns.description == null || columns.description === true; const showAssignee = columns.assignee == null || columns.assignee === true; return ( {showId && ( ID )} {showTitle && Title} {showDescription && ( Description )} {/* below is kept blank to fill the space, used as the "Assignee" column */} {showAssignee && } {issues.map((issueData) => ( { if (issueData.Issue.id === selectedIssueId) { selectIssue(null); return; } selectIssue(issueData); }} > {showId && ( {issueData.Issue.number.toString().padStart(3, "0")} )} {showTitle && ( {selectedOrganisation?.Organisation.features.issueTypes && issueTypes[issueData.Issue.type] && ( )} {selectedOrganisation?.Organisation.features.issueStatus && (columns.status == null || columns.status === true) && ( )} {issueData.Issue.title} )} {showDescription && ( {issueData.Issue.description} )} {showAssignee && ( {selectedOrganisation?.Organisation.features.issueAssigneesShownInTable && issueData.Assignees && issueData.Assignees.length > 0 && (
{issueData.Assignees.slice(0, 3).map((assignee) => ( ))} {issueData.Assignees.length > 3 && ( +{issueData.Assignees.length - 3} )}
)}
)}
))}
); }