sdlc-hub/web/src/issues/IssuesPage.tsx

79 lines
4.5 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, useMemo, useRef, useState } from "react";
import { Link, useSearchParams } from "react-router";
import { ISSUE_FILTER_NAMES, useBoard, useIssues, useSettings, type IssueFilters } from "../api/queries.ts";
import { dueOf, epicNames, filterOptions } from "./options.ts";
import "./issues.css";
const SELECTS = [
["sprint", "Спринт"], ["assignee", "Исполнитель"], ["work_kind", "Вид работы"], ["fact_status", "Статус по факту"],
["status", "Статус Jira"], ["label", "Метка"], ["epic", "Эпик"],
] as const;
const Q_DELAY = 300; // поиск — после паузы ввода (R-7)
export function IssuesPage() {
const [params, setParams] = useSearchParams();
const filters = useMemo<IssueFilters>(() =>
Object.fromEntries(ISSUE_FILTER_NAMES.map((n) => [n, params.get(n) ?? ""]).filter(([, v]) => v)), [params]);
const set = (name: string, value: string) => setParams((p) => { const n = new URLSearchParams(p); if (value) n.set(name, value); else n.delete(name); return n; });
const [q, setQ] = useState(filters.q ?? "");
const written = useRef(filters.q ?? ""); // последнее значение q, согласованное с адресом (записанное нами или принятое из него)
useEffect(() => {
// адрес изменён извне (назад, щелчок по «Задачи»): поле поиска следует за ним
const fromUrl = filters.q ?? "";
if (fromUrl === written.current) return;
written.current = fromUrl;
setQ(fromUrl);
}, [filters.q]);
useEffect(() => {
if (q === (filters.q ?? "")) return;
const t = setTimeout(() => {
written.current = q;
setParams((p) => { const n = new URLSearchParams(p); if (q) n.set("q", q); else n.delete("q"); return n; }, { replace: true });
}, Q_DELAY);
return () => clearTimeout(t);
}, [q, filters.q, setParams]);
const board = useBoard(), settings = useSettings();
const opts = useMemo(() => filterOptions(board.data, settings.data), [board.data, settings.data]);
const names = useMemo(() => epicNames(board.data), [board.data]);
const list = useIssues(filters);
const rows = list.data?.pages.flat() ?? [];
return (
<div data-testid="page-issues" className="issues">
<h1>Задачи</h1>
<div className="filters">
{SELECTS.map(([name, label]) => (
<label key={name}>{label}
<select aria-label={label} value={filters[name] ?? ""} onChange={(e) => set(name, e.target.value)}>
<option value="">все</option>
{opts[name].map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</label>
))}
<label className="check"><input type="checkbox" checked={filters.neighbor === "1"} onChange={(e) => set("neighbor", e.target.checked ? "1" : "")} />только соседи</label>
<input type="search" aria-label="Поиск по ключу или теме" placeholder="ключ или тема" value={q} onChange={(e) => setQ(e.target.value)} />
</div>
{list.isError && (
<p className="warn" role="alert">{(list.error as Error).message}{" "}
<button className="btn sm" type="button" onClick={() => void list.refetch()}>Повторить</button></p>
)}
<table className="grid">
<thead><tr><th>Ключ</th><th>Тема</th><th>Тип</th><th>Статус Jira</th><th>Исполнитель</th><th>Спринт</th><th>Срок</th><th>Эпик</th><th>Вид работы</th><th>Статус по факту</th><th>Сосед</th></tr></thead>
<tbody>
{rows.map((i) => (
<tr key={i.key}>
<td><Link to={`/issues/${i.key}`}>{i.key}</Link></td><td>{i.summary}</td><td>{i.issue_type}</td><td>{i.status}</td>
<td>{i.assignee}</td><td>{i.sprint}</td><td>{dueOf(i)}</td><td>{i.epic_key ? names[i.epic_key] ?? i.epic_key : ""}</td>
<td>{i.work_kind}</td><td>{i.fact_status}</td><td>{i.neighbor ? "да" : ""}</td>
</tr>
))}
</tbody>
</table>
{list.isPending && <p className="hint">загрузка…</p>}
{!list.isPending && !rows.length && !list.isError && <p className="hint">По этим фильтрам задач нет.</p>}
{list.hasNextPage && <button className="btn" type="button" disabled={list.isFetchingNextPage} onClick={() => void list.fetchNextPage()}>Показать ещё</button>}
</div>
);
}