sdlc-hub/web/src/board/BoardPage.tsx
Andrey Epifancev 80cff5bcfb Доска: закрытые задачи и их связи приглушены (#57)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:46:22 +04:00

90 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useMemo, useRef, useState } from "react";
import { useClosedStatuses } from "../api/closed.ts";
import { useSettings } from "../api/queries.ts";
import { computeVisible, emptyReason } from "./analysis/visible.ts";
import { BoardCanvas } from "./components/BoardCanvas.tsx";
import { ExportBox } from "./components/ExportBox.tsx";
import { FilterBar } from "./components/FilterBar.tsx";
import { Legend } from "./components/Legend.tsx";
import { LinkTable } from "./components/LinkTable.tsx";
import { problemCount, ProblemsDrawer } from "./components/ProblemsDrawer.tsx";
import { Notice, Toolbar } from "./components/Toolbar.tsx";
import { dictsFromSettings } from "./dicts.ts";
import { exportFileName, exportJson } from "./exportJson.ts";
import { todayIso } from "./model/util.ts";
import { useBoardPrefs } from "./useBoardPrefs.ts";
import { useBoardSource } from "./useBoardSource.ts";
import "./board.css";
export function BoardPage() {
const settings = useSettings();
const closedStatuses = useClosedStatuses();
const d = useMemo(() => (settings.data ? dictsFromSettings(settings.data) : null), [settings.data]);
const src = useBoardSource(d && !d.missing.length ? d.dicts : null);
const prefs = useBoardPrefs(src.model);
const today = todayIso();
const vis = useMemo(() => computeVisible(src.model, prefs.filters, prefs.store.laneOrder, today),
[src.model, prefs.filters, prefs.store.laneOrder, today]);
const [exportText, setExportText] = useState<string | null>(null);
const saveRef = useRef<(() => void) | null>(null);
// высота закреплённой шапки — ящик «Проблемы» и полоса дат липнут под ней (строки 1016–1019); при уходе — снимаем
const headRef = useCallback((el: HTMLDivElement | null) => {
if (!el) return;
const root = document.documentElement.style;
const set = () => root.setProperty("--head-h", (getComputedStyle(el).position === "sticky" ? el.offsetHeight : 0) + "px");
set();
const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(set);
ro?.observe(el);
return () => { ro?.disconnect(); root.removeProperty("--head-h"); };
}, []);
if (settings.isPending) return <p className="hint" data-testid="page-board">Загрузка настроек хаба…</p>;
if (settings.error) return <p className="warn" data-testid="page-board">Настройки хаба не загрузились: {settings.error.message}</p>;
if (d && d.missing.length) {
return (
<div className="notice" role="alert" data-testid="page-board">
В настройках хаба нет {d.missing.join(" и ")} — доска не может перевести виды работ и типы связей Jira. Задайте
словари в config.example.json и выполните <code>uv run jira-hub init --config …</code> или запишите их через PUT /settings.
</div>
);
}
const onExport = () => {
saveRef.current?.();
const now = new Date().toISOString();
const text = JSON.stringify(exportJson(src.model, vis, prefs.filters, now), null, 2);
setExportText(text);
try { // в обычном браузере файл скачается; если нет — остаются текст и «Копировать»
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([text], { type: "application/json" }));
a.download = exportFileName(src.model, now);
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
} catch { /* остаётся текст */ }
};
const onFile = (name: string, text: string) => { const r = src.loadFile(name, text); if (r) prefs.applyJson(r); };
return (
<div data-testid="page-board">
<div className="sticky" ref={headRef}>
<Toolbar label={src.label} title={src.title} status={src.status} refreshing={src.refreshing} byDate={prefs.filters.byDate}
snap={prefs.snap} model={src.model} vis={vis} problemCount={problemCount(vis.problems)} problemsOpen={prefs.problems}
tableOpen={prefs.table} onReload={src.reload} onRefresh={() => void src.refreshFromJira()} onFile={onFile}
onProblems={() => prefs.toggle("problems")} onExport={onExport} onTable={() => prefs.toggle("table")}
onSnap={() => prefs.toggle("snap")} onReset={prefs.resetLayout} />
<FilterBar model={src.model} prefs={prefs} />
<Notice model={src.model} />
<Legend prefs={prefs} />
</div>
{exportText !== null && <ExportBox text={exportText} onClose={() => setExportText(null)} />}
<BoardCanvas vis={vis} byDate={prefs.filters.byDate} snap={prefs.snap} today={today} store={prefs.store}
sprintSel={prefs.filters.sprints} emptyText={vis.nodes.length ? "" : emptyReason(src.model, vis, prefs.filters)} closedStatuses={closedStatuses}
onToggleLane={prefs.toggleLane} onMoveLane={(name, to) => prefs.moveLane(vis.lanes.map((l) => l.name), name, to)}
onColWidth={prefs.setColWidth} saveRef={saveRef}>
{prefs.problems && <ProblemsDrawer problems={vis.problems} onClose={() => prefs.toggle("problems")} />}
</BoardCanvas>
{prefs.table && <LinkTable links={vis.links} />}
</div>
);
}