52 lines
4.8 KiB
TypeScript
52 lines
4.8 KiB
TypeScript
import { EPICS, KEY_RE, NO_EPIC } from "../constants.ts";
|
||
import type { BoardModel, BoardNode, Dicts, Link } from "../types.ts";
|
||
import { parseCsv } from "./csv.ts";
|
||
import { addLink, addStubs } from "./links.ts";
|
||
import { sortableDate } from "./util.ts";
|
||
|
||
// CSV из фильтра Jira: нужны колонки Ключ проблемы, Тема, Статус, Приоритет, Тип задачи, Метки, Связи Фичи, Плановая дата окончания, Исполнитель, Спринт
|
||
// и «Внешняя/Внутренняя ссылка на проблему (ASUPR_…)». У задачи X «Внешняя ссылка (ASUPR_Блокер_Начала)» содержит Y, если X блокирует Y
|
||
// (outward-описание «Блокирует начало выполнения»); «Внутренняя ссылка» — если Y блокирует X. До 11.10.2026 связи в Jira стояли
|
||
// задом наперёд (ошибка направления в jira_sync.py), и страница читала «Внешняя» как блокеров; 11.10.2026 связи пересозданы, чтение исправлено.
|
||
export function fromCsv(text: string, fileName: string, dicts: Dicts): BoardModel {
|
||
const rows = parseCsv(text);
|
||
if (rows.length < 2) throw new Error("в файле нет строк с задачами");
|
||
const head = rows[0].map((h) => h.trim());
|
||
const find = (...subs: string[]) => head.findIndex((h) => subs.some((s) => h.toLowerCase().includes(s.toLowerCase())));
|
||
const cols = {
|
||
key: find("Ключ проблемы", "Ключ задачи", "Issue key", "Ключ"), sum: find("Тема", "Резюме", "Summary"), status: find("Статус", "Status"),
|
||
prio: find("Приоритет", "Priority"), type: find("Тип задачи", "Тип запроса", "Issue Type"), epic: find("Связи Фичи"),
|
||
due: find("Плановая дата окончания", "Срок исполнения", "Due date"), who: find("Исполнитель", "Assignee"),
|
||
};
|
||
const labelCols = head.map((h, i) => (/^(Метки|Labels)$/i.test(h) ? i : -1)).filter((i) => i >= 0);
|
||
const sprintCols = head.map((h, i) => (/^(Спринт|Sprint)$/i.test(h) ? i : -1)).filter((i) => i >= 0);
|
||
if (cols.key < 0 || cols.sum < 0) throw new Error("не нашёл колонки «Ключ проблемы» и «Тема»; в файле: " + head.slice(0, 10).join(", ") + "…");
|
||
const linkCols: { i: number; type: string; out: boolean }[] = [];
|
||
head.forEach((h, i) => { for (const [t, type] of Object.entries(dicts.linkTypes)) if (h.includes(t)) linkCols.push({ i, type, out: /outward|исход|внешн/.test(h.toLowerCase()) }); });
|
||
const problems: string[] = [];
|
||
if (!linkCols.length) problems.push("в файле нет колонок связей — в фильтре Jira добавьте колонку «Связанные задачи» или экспортируйте «все поля»");
|
||
if (!sprintCols.length) problems.push("в файле нет колонки «Спринт» — выбор спринта недоступен, все задачи считаются выбранными");
|
||
if (cols.epic < 0) problems.push("нет колонки «Связи Фичи» — дорожки по эпикам не построить");
|
||
const cell = (r: string[], i: number) => (i >= 0 ? r[i] || "" : "").trim();
|
||
const isEpic = (r: string[]) => cols.type >= 0 && /^(Фича|Epic|Эпик)$/i.test(cell(r, cols.type));
|
||
const epicNames: Record<string, string> = { ...EPICS };
|
||
rows.slice(1).forEach((r) => { const k = cell(r, cols.key); if (KEY_RE.test(k) && isEpic(r)) epicNames[k] = cell(r, cols.sum) || k; });
|
||
const nodes: BoardNode[] = [], links: Link[] = [], seen = new Set<string>(), sprints: Record<string, number> = {};
|
||
for (const r of rows.slice(1)) {
|
||
const key = cell(r, cols.key); if (!KEY_RE.test(key) || isEpic(r)) continue;
|
||
let kind = cell(r, cols.type);
|
||
for (const c of labelCols) { const v = cell(r, c); if (dicts.kindLabels[v]) kind = dicts.kindLabels[v]; }
|
||
const epicKey = cell(r, cols.epic);
|
||
const sprint = sprintCols.map((c) => cell(r, c)).filter(Boolean).pop() || "";
|
||
if (sprint) sprints[sprint] = (sprints[sprint] || 0) + 1;
|
||
nodes.push({ key, title: cell(r, cols.sum), kind, prio: cell(r, cols.prio), due: sortableDate(cell(r, cols.due)), status: cell(r, cols.status),
|
||
epic: epicKey ? (epicNames[epicKey] || epicKey) : NO_EPIC, sprint, who: cell(r, cols.who), stub: false });
|
||
for (const lc of linkCols) {
|
||
const other = cell(r, lc.i); if (!KEY_RE.test(other)) continue;
|
||
addLink(links, seen, lc.out ? key : other, lc.out ? other : key, lc.type);
|
||
}
|
||
}
|
||
const missing = addStubs(nodes, links);
|
||
return { nodes, links, sprints, fileName, problems, hasSprint: sprintCols.length > 0, missing };
|
||
}
|