70 lines
4.1 KiB
TypeScript
70 lines
4.1 KiB
TypeScript
import { useEffect, useMemo, useRef, useState } from "react";
|
||
import { useSearchParams } from "react-router";
|
||
import { isClosed, useClosedStatuses } from "../api/closed.ts";
|
||
import { useSettings } from "../../../api/queries.ts";
|
||
import { ISSUE_FILTER_NAMES, useBoard, useIssues, type IssueFilters as Filters } from "../api/queries.ts";
|
||
import { Button, Notice, PageHeader } from "../../../ui/index.ts";
|
||
import { IssueFilters, SELECTS } from "./IssueFilters.tsx";
|
||
import { useOpenIssue } from "./IssueLink.tsx";
|
||
import { RefreshButton } from "../RefreshButton.tsx";
|
||
import { IssueTable } from "./IssueTable.tsx";
|
||
import { epicNames, filterOptions } from "./options.ts";
|
||
import "./issues.css";
|
||
|
||
const Q_DELAY = 300; // поиск — после паузы ввода (R-7)
|
||
|
||
export function IssuesPage({ project }: { project: string }) {
|
||
const [params, setParams] = useSearchParams();
|
||
const filters = useMemo<Filters>(() =>
|
||
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 reset = () => setParams((p) => { const n = new URLSearchParams(p); SELECTS.forEach(([name]) => 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(project), settings = useSettings();
|
||
const opts = useMemo(() => filterOptions(board.data, settings.data), [board.data, settings.data]);
|
||
const names = useMemo(() => epicNames(board.data), [board.data]);
|
||
// закрытые скрыты, пока не выбрано «Все» (closed=1); выбранный закрытый статус Jira скрытие отменяет
|
||
const showClosed = params.get("closed") === "1";
|
||
const closed = useClosedStatuses();
|
||
const query = useMemo<Filters>(() => (showClosed || isClosed(filters.status, closed) ? { ...filters, project } : { ...filters, project, hide_closed: "1" }),
|
||
[filters, project, showClosed, closed]);
|
||
const list = useIssues(query);
|
||
const rows = list.data?.pages.flat() ?? [];
|
||
const open = useOpenIssue();
|
||
|
||
return (
|
||
<div data-testid="page-issues" className="issues">
|
||
<PageHeader title="Задачи" subtitle={list.isPending ? undefined : `показано ${rows.length}${list.hasNextPage ? " · есть ещё" : ""}`} actions={<RefreshButton />} />
|
||
<IssueFilters filters={filters} set={set} onReset={reset} q={q} setQ={setQ} showClosed={showClosed} opts={opts} hasBoard={!!board.data} />
|
||
{list.isError && (
|
||
<Notice tone="error" action={<Button size="md" onClick={() => void list.refetch()}>Повторить</Button>}>{(list.error as Error).message}</Notice>
|
||
)}
|
||
{rows.length > 0 && <IssueTable rows={rows} epicNames={names} closed={closed} onOpen={open} />}
|
||
{list.isPending && <p className="issues__hint">Загрузка…</p>}
|
||
{!list.isPending && !rows.length && !list.isError && <Notice tone="info">По этим фильтрам задач нет</Notice>}
|
||
{list.hasNextPage && (
|
||
<div className="issues__more">
|
||
<Button disabled={list.isFetchingNextPage} onClick={() => void list.fetchNextPage()}>Показать ещё</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|