60 lines
5.4 KiB
TypeScript
60 lines
5.4 KiB
TypeScript
import { KIND, NO_EPIC, NOT_IN_FILE, NOT_IN_HUB, RELATED } from "../constants.ts";
|
||
import { whoKey } from "../model/util.ts";
|
||
import type { BoardModel, BoardNode, Filters, Link, VisLink, VisNode, Visible } from "../types.ts";
|
||
import { criticalPath } from "./critical.ts";
|
||
import { findProblems } from "./problems.ts";
|
||
|
||
// фильтры задач — пересечение спринта, исполнителя и вида работы; skip — какой из них не учитывать (для счётчиков в выпадашках).
|
||
// Заглушки «нет в файле» фильтры не проходят: они попадают на доску только как соседи по связям
|
||
export const passes = (x: BoardNode, f: Filters, skip?: "sprint" | "who" | "kind"): boolean => !x.stub
|
||
&& (skip === "sprint" || f.sprints.size === 0 || f.sprints.has(x.sprint))
|
||
&& (skip === "who" || f.who.size === 0 || f.who.has(whoKey(x.who)))
|
||
&& (skip === "kind" || !f.hideKind.has(KIND[x.kind] || "org"));
|
||
export const edgeClass = (l: Link): "rel" | "block" => (l.type === RELATED ? "rel" : "block");
|
||
|
||
// доска: ядро = задачи, прошедшие фильтры; контекст = их соседи по связям (если включён); стрелки = все связи между карточками на доске
|
||
// минус скрытые типы и, при «передачах», минус связи внутри одного человека; «скрыть без связей» убирает карточки без оставшихся стрелок.
|
||
// removed — сколько карточек/стрелок убрал каждый шаг, для строки статуса и объяснения пустой доски
|
||
export function computeVisible(model: BoardModel, f: Filters, laneOrder: string[], today: string): Visible {
|
||
const all = model.nodes, byKey = Object.fromEntries(all.map((x) => [x.key, x]));
|
||
const isHand = (l: Link) => l.type !== RELATED && whoKey(byKey[l.src].who) !== whoKey(byKey[l.dst].who);
|
||
const core = new Set(all.filter((x) => passes(x, f)).map((x) => x.key));
|
||
let keep = new Set(core);
|
||
if (f.neighbors) model.links.forEach((l) => { if (core.has(l.src) || core.has(l.dst)) { keep.add(l.src); keep.add(l.dst); } });
|
||
const removed = { core: core.size, ctx: keep.size - core.size, edge: 0, loose: 0 };
|
||
let rawLinks = model.links.filter((l) => keep.has(l.src) && keep.has(l.dst));
|
||
const allLinks = rawLinks.length;
|
||
if (f.hideEdge.size) rawLinks = rawLinks.filter((l) => !f.hideEdge.has(edgeClass(l)));
|
||
if (f.onlyHand) rawLinks = rawLinks.filter(isHand);
|
||
removed.edge = allLinks - rawLinks.length;
|
||
if (f.onlyLinked) {
|
||
const ends = new Set(rawLinks.flatMap((l) => [l.src, l.dst])), before = keep.size;
|
||
keep = new Set([...keep].filter((k) => ends.has(k)));
|
||
removed.loose = before - keep.size;
|
||
}
|
||
const links: VisLink[] = rawLinks.map((l) => ({ ...l, hand: isHand(l), crit: false }));
|
||
// ядро сплошной рамкой, соседи пунктиром
|
||
const nodes: VisNode[] = all.filter((x) => keep.has(x.key)).map((x) => ({ ...x, dim: !core.has(x.key), focus: false, crit: false }));
|
||
const crit = f.crit ? criticalPath(nodes, links) : { nodes: new Set<string>(), edges: new Set<string>() };
|
||
nodes.forEach((x) => { x.crit = crit.nodes.has(x.key); });
|
||
links.forEach((l) => { l.crit = crit.edges.has(l.src + ">" + l.dst); });
|
||
// дорожки по эпикам: по алфавиту, служебные внизу, затем сохранённый порядок пользователя
|
||
const byEpic: Record<string, string[]> = {};
|
||
nodes.forEach((x) => (byEpic[x.epic] = byEpic[x.epic] || []).push(x.key));
|
||
const tail = (name: string) => name === NO_EPIC || name === NOT_IN_FILE || name === NOT_IN_HUB;
|
||
const lanes = Object.entries(byEpic).sort((a, b) => Number(tail(a[0])) - Number(tail(b[0])) || a[0].localeCompare(b[0], "ru")).map(([name, keys]) => ({ name, keys }));
|
||
if (laneOrder.length) lanes.sort((a, b) => { const ia = laneOrder.indexOf(a.name), ib = laneOrder.indexOf(b.name); return (ia < 0 ? 1e9 : ia) - (ib < 0 ? 1e9 : ib); });
|
||
return { nodes, links, lanes, removed, problems: findProblems(nodes, links, today) };
|
||
}
|
||
|
||
export function emptyReason(model: BoardModel, vis: Visible, f: Filters): string {
|
||
const r = vis.removed;
|
||
if (!model.nodes.some((n) => !n.stub)) return model.emptyText || (model.stubLane === NOT_IN_HUB ? "В зеркале хаба нет задач." : "В файле нет задач.");
|
||
if (!r.core) {
|
||
const active = [f.sprints.size ? "спринт" : "", f.who.size ? "исполнитель" : "", f.hideKind.size ? "виды работ в легенде" : ""].filter(Boolean);
|
||
return `Фильтры не оставили ни одной задачи (${active.join(", ")}). Ослабьте один из них: «Снять выбор» в выпадашке или щелчок по зачёркнутому виду работы.`;
|
||
}
|
||
if (r.loose) return `Все ${r.loose} задач скрыты кнопкой «скрыть без связей»: ${f.onlyHand ? "передач между людьми на доске нет" : "видимых стрелок у них нет"}. Отключите её${f.onlyHand ? " или «передачи»" : f.hideEdge.size ? " или верните тип связи в легенде" : ""}.`;
|
||
return "Нет задач для показа.";
|
||
}
|