45 lines
3.2 KiB
TypeScript
45 lines
3.2 KiB
TypeScript
import { isClosed } from "../api/closed.ts";
|
||
import type { IssueDetail } from "../api/types.ts";
|
||
|
||
export type LinkGroupId = "depends" | "blocks" | "related" | "other";
|
||
export interface CardLink { key: string; summary: string | null; status: string | null; tag: string | null }
|
||
|
||
const GROUPS: [LinkGroupId, string][] = [["depends", "Зависит от"], ["blocks", "Блокирует"], ["related", "Связана с"], ["other", "Прочие"]];
|
||
const BLOCK_TAGS: Record<string, string> = { "блокирует начало": "начало", "блокирует завершение": "завершение" };
|
||
const byKey = (a: CardLink, b: CardLink) =>
|
||
a.key.localeCompare(b.key, "ru", { numeric: true }) || (a.tag ?? "").localeCompare(b.tag ?? "", "ru");
|
||
|
||
// связи карточки по смыслу (design 17): логическое имя типа — из link_types настроек (имя типа Jira → логическое)
|
||
export function groupLinks(d: Pick<IssueDetail, "links_out" | "links_in">, linkTypes: Record<string, string>): [LinkGroupId, string, CardLink[]][] {
|
||
const logical = new Map(Object.entries(linkTypes).map(([name, jira]) => [jira, name]));
|
||
const groups: Record<LinkGroupId, CardLink[]> = { depends: [], blocks: [], related: [], other: [] };
|
||
const add = (dir: "out" | "in", key: string, jiraType: string, summary: string | null, status: string | null) => {
|
||
const name = logical.get(jiraType);
|
||
if (name && BLOCK_TAGS[name]) groups[dir === "in" ? "depends" : "blocks"].push({ key, summary, status, tag: BLOCK_TAGS[name] });
|
||
else if (name === "связанные") { if (!groups.related.some((l) => l.key === key)) groups.related.push({ key, summary, status, tag: null }); }
|
||
else groups.other.push({ key, summary, status, tag: `${dir === "out" ? "→" : "←"} ${name ?? jiraType}` });
|
||
};
|
||
for (const l of d.links_out) add("out", l.target_key, l.link_type, l.summary, l.status);
|
||
for (const l of d.links_in) add("in", l.source_key, l.link_type, l.summary, l.status);
|
||
return GROUPS.filter(([id]) => groups[id].length).map(([id, title]) => [id, title, groups[id].sort(byKey)]);
|
||
}
|
||
|
||
// просрочено — срок раньше сегодня, а статус Jira не из closed_statuses (hide-closed; было — по статусу по факту, R-2 #37)
|
||
export function isOverdue(due: string, status: string | null | undefined, closed: Set<string>, today: string): boolean {
|
||
return !!due && due < today && !isClosed(status, closed);
|
||
}
|
||
|
||
export const todayISO = (d = new Date()): string =>
|
||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||
|
||
// метки задачи: виды работ (метка из kind_labels → название вида) отдельно от прочих меток
|
||
export function splitKindLabels(labels: string[], kindLabels: Record<string, string> | undefined): { kinds: string[]; rest: string[] } {
|
||
const byLabel = new Map(Object.entries(kindLabels ?? {}).map(([kind, label]) => [label, kind]));
|
||
const kinds: string[] = [], rest: string[] = [];
|
||
for (const l of labels) {
|
||
const k = byLabel.get(l);
|
||
if (k !== undefined) kinds.push(k); else rest.push(l);
|
||
}
|
||
return { kinds, rest };
|
||
}
|