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

70 lines
4.1 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 { 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>
);
}