2026-10-26 01:06:48 +04:00

71 lines
4.4 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 { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../../../api/client.ts";
import { qk as coreQk } from "../../../api/queries.ts";
import type { AgileResponse, BoardSnapshot, ChangesResponse, IssueDetail, IssueRow, Note, NoteFields } from "./types.ts";
// фильтры списка в адресе; hide_closed в адрес не пишется — его выводит список из флажка «скрыть закрытые» (closed=1 — снят)
export const ISSUE_FILTER_NAMES = ["sprint", "assignee", "work_kind", "status", "label", "epic", "neighbor", "q"] as const;
export type IssueFilters = Partial<Record<(typeof ISSUE_FILTER_NAMES)[number] | "hide_closed" | "project", string>>;
export const ISSUE_PAGE = 50;
// ключи ядра (настройки, проекты) входят целиком: модуль инвалидирует и их
export const qk = {
...coreQk,
board: (project: string) => ["board", project] as const,
agile: (project: string) => ["agile", project] as const,
boardAll: ["board"] as const, // префикс: все снимки досок сразу (инвалидация)
issues: (f: IssueFilters) => ["issues", f] as const,
issue: (key: string) => ["issue", key] as const,
changes: (q: ChangesQuery) => ["changes", q] as const,
};
// лента изменений: в ключе — проект, период и виды; границы считаются в момент запроса (bounds), поэтому перечитывание
// (после «Обновить из Jira», при возврате на вкладку) берёт окно «до сейчас», а не до открытия страницы (C-1, C-2);
// limit — максимум хаба (design web-feed #3)
export interface ChangesQuery { project: string; period: string; kinds: string }
export const CHANGES_LIMIT = 2000;
export const useChanges = (q: ChangesQuery, bounds: () => { since: Date; until: Date }, enabled = true) =>
useQuery({
queryKey: qk.changes(q), enabled, refetchOnWindowFocus: true,
queryFn: () => {
const { since, until } = bounds();
return api.get<ChangesResponse>("/changes", { project: q.project, kinds: q.kinds, since: since.toISOString(), until: until.toISOString(), limit: CHANGES_LIMIT });
},
});
// project === null — проект ещё не выбран: запроса нет
export const useBoard = (project: string | null) =>
useQuery({ queryKey: qk.board(project ?? ""), enabled: project !== null, queryFn: () => api.get<BoardSnapshot>("/board", { project: project! }) });
export const useIssue = (key: string) =>
useQuery({ queryKey: qk.issue(key), queryFn: () => api.get<IssueDetail>(`/issues/${encodeURIComponent(key)}`) });
// список порциями по ISSUE_PAGE: следующая порция есть, пока последняя полная
export const useIssues = (filters: IssueFilters, opts?: { enabled?: boolean }) =>
useInfiniteQuery({
enabled: opts?.enabled ?? true,
queryKey: qk.issues(filters),
initialPageParam: 0,
queryFn: ({ pageParam }) =>
api.get<{ issues: IssueRow[] }>("/issues", { ...filters, limit: ISSUE_PAGE, offset: pageParam }).then((r) => r.issues),
getNextPageParam: (last, all) => (last.length < ISSUE_PAGE ? undefined : all.length * ISSUE_PAGE),
});
// сохранение заметки: key фиксируется при создании мутации — ответ пишется в карточку той задачи, для которой отправлен
export function useSaveNote(key: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (fields: NoteFields) =>
api.put<Note>(`/issues/${encodeURIComponent(key)}/note`, { ...fields, updated_by: "web" }),
onSuccess: (note) => {
qc.setQueryData<IssueDetail>(qk.issue(key), (d) => (d ? { ...d, note } : d));
void qc.invalidateQueries({ queryKey: ["issues"] });
void qc.invalidateQueries({ queryKey: qk.boardAll });
},
});
}
// данные экранов доски (бэклог, спринт, Kanban): запрос только для проекта с доской нужного типа (enabled)
export const useAgile = (project: string, enabled = true) =>
useQuery({ queryKey: qk.agile(project), enabled, queryFn: () => api.get<AgileResponse>("/agile", { project }) });