import { BoardColumn, BoardFilter, IssueSummary, } from '@task-stream/web/tracker/domain'; /** Pure board transformations (unit tested). */ export function findIssue( columns: BoardColumn[], issueId: string, ): IssueSummary | undefined { for (const column of columns) { const issue = column.issues.find((i) => i.id === issueId); if (issue) return issue; } return undefined; } export function matchesFilter( issue: IssueSummary, filter: BoardFilter | null, ): boolean { if (!filter) return true; if (filter.unassigned) { if (issue.assignee) return false; } else if (filter.assigneeId && issue.assignee?.id !== filter.assigneeId) return false; if (filter.releaseId && issue.release?.id !== filter.releaseId) return false; if (filter.area && issue.area !== filter.area) return false; if (filter.types?.length && !filter.types.includes(issue.type)) return false; if (filter.priorities?.length && !filter.priorities.includes(issue.priority)) return false; if ( filter.labels?.length && !filter.labels.every((l) => issue.labels.includes(l.toLowerCase())) ) return false; // Cards carry no description: a change that matched only by it drops off // until the next load, which is the safe side. const text = filter.search?.trim().toLowerCase(); if ( text && !issue.key.toLowerCase().includes(text) && !issue.title.toLowerCase().includes(text) ) return false; return true; } /** `issues` (in backlog order) with `issue` at its place by rank. */ export function insertByRank( issues: IssueSummary[], issue: IssueSummary, ): IssueSummary[] { const at = issues.findIndex((i) => i.rank > issue.rank); return at === -1 ? [...issues, issue] : [...issues.slice(0, at), issue, ...issues.slice(at)]; } /** Removes the issue wherever it is and puts `next` (if any) at its place by rank in its status column. */ export function placeIssue( columns: BoardColumn[], issueId: string, next: IssueSummary | null, ): BoardColumn[] { return columns.map((column) => { const had = column.issues.some((i) => i.id === issueId); let issues = had ? column.issues.filter((i) => i.id !== issueId) : column.issues; let total = column.total - (had ? 1 : 0); if (next && next.status === column.status) { issues = insertByRank(issues, next); total += 1; } return had || issues !== column.issues ? { ...column, issues, total } : column; }); } /** Optimistic move. */ export function moveIssue( columns: BoardColumn[], issueId: string, to: string, ): BoardColumn[] { const issue = findIssue(columns, issueId); return issue ? placeIssue(columns, issueId, { ...issue, status: to }) : columns; } /** Merges a realtime change, ignoring stale versions and issues outside the filter. */ export function applyIssueChange( columns: BoardColumn[], issue: IssueSummary, filter: BoardFilter | null, ): BoardColumn[] { const current = findIssue(columns, issue.id); if (current && current.version > issue.version) return columns; return placeIssue( columns, issue.id, matchesFilter(issue, filter) ? issue : null, ); }