89 lines
5.9 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 { useEffect, useState } from "react";
import { useJiraRefresh } from "../../shell/refresh.ts";
import { refreshText } from "../../shell/refreshStatus.ts";
import { applyTheme, readTheme, saveTheme, THEME_NAMES, THEMES, type Theme } from "../../shell/theme.ts";
import { statusText, type BoardStatus } from "../status.ts";
import type { BoardModel, Visible } from "../types.ts";
import { NOT_IN_HUB } from "../constants.ts";
import { Dropdown } from "../../ui/index.ts";
interface ToolbarProps {
label: string; title: string; status: BoardStatus | null; byDate: boolean; snap: boolean;
model: BoardModel; vis: Visible; problemCount: number; problemsOpen: boolean; tableOpen: boolean;
onReload(): void; onFile(name: string, text: string): void; onProblems(): void;
onExport(): void; onTable(): void; onSnap(): void; onReset(): void;
}
export function Toolbar(p: ToolbarProps) {
const [theme, setTheme] = useState<Theme>(readTheme);
const [confirm, setConfirm] = useState(false);
useEffect(() => { if (!confirm) return; const t = setTimeout(() => setConfirm(false), 8000); return () => clearTimeout(t); }, [confirm]);
const refresh = useJiraRefresh(); // кнопка и ход пакета временно здесь; в верхней панели — с Task 5
const rt = refresh.event ? refreshText(refresh.event) : null;
const st = p.status ? statusText(p.status) : rt ? { text: rt.text, warn: rt.tone === "error" || rt.tone === "warning" } : null;
const m = p.model, v = p.vis, r = v.removed;
const coreShown = v.nodes.filter((n) => !n.dim).length, ctxShown = v.nodes.length - coreShown;
const nextTheme = () => { const t = THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length]; setTheme(t); saveTheme(t); applyTheme(t); };
return (
<div className="top">
<h1>Доска связей</h1>
<span className="src">
<span className="file-name" title={p.title}>{p.label}</span>
<button type="button" className="link" title="Загрузить снимок зеркала хаба заново, без обращения к Jira" onClick={p.onReload}>Перечитать</button>
<button type="button" className="link" disabled={refresh.busy} onClick={() => void refresh.start()}
title="Поставить в хаб пакет «обновление по базовой JQL» и дождаться его: агент перечитает задачи из Jira">Обновить из Jira</button>
<label className="link" title="CSV из фильтра Jira или JSON, выгруженный этой доской. JSON при работе с хабом переносит только расстановку карточек, дорожки и фильтры; CSV показывает снимок файла до «Перечитать»">
файл…
<input type="file" accept=".csv,.json,text/csv,application/json" hidden onChange={async (e) => {
const f = e.target.files?.[0];
e.target.value = "";
if (f) p.onFile(f.name, await f.text());
}} />
</label>
</span>
{st && <span className={"stats" + (st.warn ? " warn" : "")} role="status">{st.text}</span>}
<span className="stats">
<b>{coreShown}</b> задач из {m.nodes.filter((n) => !n.stub).length}
{ctxShown > 0 && <> · <b>+{ctxShown}</b> соседи по связям</>}
{" · "}<b>{v.links.length}</b> связей из {m.links.length}
{r.loose > 0 && <> · скрыто без связей: {r.loose}</>}
</span>
<span className="spacer" />
<button className="btn" type="button" aria-pressed={p.problemsOpen} onClick={p.onProblems}
title="Сроки блокеров, задачи без исполнителя, передачи между людьми">
Проблемы <span className={"n" + (p.problemCount ? "" : " zero")}>{p.problemCount}</span>
</button>
<Dropdown className="more" button={({ open, toggle }) => (
<button type="button" className="btn more-btn" aria-haspopup="menu" aria-expanded={open} title="Ещё" onClick={toggle}>⋯</button>
)}>
{(close) => (
<>
<button type="button" onClick={() => { close(); p.onExport(); }}>Экспорт JSON</button>
<button type="button" aria-pressed={p.tableOpen} onClick={p.onTable}>Таблица связей</button>
<button type="button" aria-pressed={p.snap} onClick={p.onSnap}>Привязка к сетке</button>
<button type="button" onClick={nextTheme}>Тема: {THEME_NAMES[theme]}</button>
<hr />
{!confirm ? <button type="button" className="danger" onClick={() => setConfirm(true)}>Сбросить раскладку</button> : (
<div className="confirm">
<span>Вернуть автоматическую раскладку {p.byDate ? "режима «по датам»" : "(обычную; раскладка «по датам» останется)"}, порядок и раскрытие дорожек?</span>
<div className="row">
<button type="button" className="btn yes" onClick={() => { p.onReset(); setConfirm(false); close(); }}>Да, сбросить</button>
<button type="button" className="btn no" onClick={() => setConfirm(false)}>Нет</button>
</div>
</div>
)}
</>
)}
</Dropdown>
</div>
);
}
// предупреждения о данных под шапкой (строки 878–880)
export function Notice({ model }: { model: BoardModel }) {
const notes = [...model.problems];
if (model.missing) notes.push(`упомянуты в связях, но в ${model.stubLane === NOT_IN_HUB ? "зеркале хаба" : "файле"} отсутствуют: ${model.missing}`);
if (!notes.length) return null;
return <div className="notice">{notes.map((n) => <span key={n}>{n}</span>)}</div>;
}