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(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

Загрузка настроек хаба…

; if (settings.error) return

Настройки хаба не загрузились: {settings.error.message}

; if (d && d.missing.length) { return (
В настройках хаба нет {d.missing.join(" и ")} — доска не может перевести виды работ и типы связей Jira. Задайте словари в config.example.json и выполните uv run jira-hub init --config … или запишите их через PUT /settings.
); } 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 (
void src.refreshFromJira()} onFile={onFile} onProblems={() => prefs.toggle("problems")} onExport={onExport} onTable={() => prefs.toggle("table")} onSnap={() => prefs.toggle("snap")} onReset={prefs.resetLayout} />
{exportText !== null && setExportText(null)} />} prefs.moveLane(vis.lanes.map((l) => l.name), name, to)} onColWidth={prefs.setColWidth} saveRef={saveRef}> {prefs.problems && prefs.toggle("problems")} />} {prefs.table && }
); }