Список задач: запрос после настроек, без мелькания «задач нет» (#124, #60 M-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-26 01:06:48 +04:00
parent 231ca2f199
commit 9966722820
3 changed files with 38 additions and 5 deletions

View File

@ -41,8 +41,9 @@ export const useIssue = (key: string) =>
useQuery({ queryKey: qk.issue(key), queryFn: () => api.get<IssueDetail>(`/issues/${encodeURIComponent(key)}`) });
// список порциями по ISSUE_PAGE: следующая порция есть, пока последняя полная
export const useIssues = (filters: IssueFilters) =>
export const useIssues = (filters: IssueFilters, opts?: { enabled?: boolean }) =>
useInfiniteQuery({
enabled: opts?.enabled ?? true,
queryKey: qk.issues(filters),
initialPageParam: 0,
queryFn: ({ pageParam }) =>

View File

@ -246,6 +246,37 @@ describe("список задач", () => {
await waitFor(() => expect(calls().at(-1)!.searchParams.has("hide_closed")).toBe(false));
});
it("закрытый статус в адресе до настроек: запроса нет, «Загрузка…», после настроек — один запрос без hide_closed (#60 M-3)", async () => {
const st = deferred<Response>();
const fetch = mockFetch((_m, url) => {
if (url.pathname === "/settings") return st.promise;
if (url.pathname === "/board") return json(200, SNAP);
return json(200, { issues: [row(1)] });
});
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues?status=Готово", path: "/issues" });
await screen.findByText("Загрузка…");
expect(issuesCalls(fetch)).toHaveLength(0);
expect(screen.queryByText("По этим фильтрам задач нет")).toBeNull();
st.resolve(json(200, { ...settings, closed_statuses: ["Готово"] }));
await screen.findByRole("link", { name: "AURORA-9001" });
expect(issuesCalls(fetch)).toHaveLength(1);
const q = issuesCalls(fetch)[0].searchParams;
expect(q.get("status")).toBe("Готово");
expect(q.has("hide_closed")).toBe(false);
});
it("R-11: /settings отвечает 500 — список всё равно запрашивается с hide_closed=1", async () => {
const fetch = mockFetch((_m, url) => {
if (url.pathname === "/settings") return json(500, { error: "boom" });
if (url.pathname === "/board") return json(200, SNAP);
return json(200, { issues: [row(1)] });
});
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues", path: "/issues" });
await screen.findByRole("link", { name: "AURORA-9001" });
expect(issuesCalls(fetch)).toHaveLength(1);
expect(issuesCalls(fetch)[0].searchParams.get("hide_closed")).toBe("1");
});
it("статуса по факту нет: ни фильтра, ни колонки; fact_status из адреса не уходит в запрос", async () => {
const fetch = hub(() => [row(1)]);
renderWithProviders(<IssuesPage project="AWPCA" />, { route: "/issues?fact_status=В работе", path: "/issues" });

View File

@ -64,19 +64,20 @@ export function IssuesPage({ project }: { project: string }) {
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 list = useIssues(query, { enabled: !settings.isPending }); // закрытые статусы — из настроек: без них запрос ушёл бы с неверным hide_closed (#60 M-3); при ошибке настроек isPending=false (R-11)
const rows = list.data?.pages.flat() ?? [];
const loading = list.isPending || settings.isPending;
return (
<div data-testid="page-issues" className="issues">
<PageHeader title="Задачи" subtitle={list.isPending ? undefined : `показано ${rows.length}${list.hasNextPage ? " · есть ещё" : ""}`} actions={<RefreshButton />} />
<PageHeader title="Задачи" subtitle={loading ? 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>}
{loading && <p className="issues__hint">Загрузка…</p>}
{!loading && !rows.length && !list.isError && <Notice tone="info">По этим фильтрам задач нет</Notice>}
{list.hasNextPage && (
<div className="issues__more">
<Button disabled={list.isFetchingNextPage} onClick={() => void list.fetchNextPage()}>Показать ещё</Button>