import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api } from "./client.ts"; import type { BoardSnapshot, ChangesResponse, CreatedProject, HubSettings, IssueDetail, IssueRow, Note, NoteFields, Project } 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>; export const ISSUE_PAGE = 50; export const qk = { settings: ["settings"] as const, projects: ["projects"] as const, board: (project: string) => ["board", 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("/changes", { project: q.project, kinds: q.kinds, since: since.toISOString(), until: until.toISOString(), limit: CHANGES_LIMIT }); }, }); export const useSettings = () => useQuery({ queryKey: qk.settings, queryFn: () => api.get("/settings"), staleTime: Infinity }); // project === null — проект ещё не выбран: запроса нет export const useBoard = (project: string | null) => useQuery({ queryKey: qk.board(project ?? ""), enabled: project !== null, queryFn: () => api.get("/board", { project: project! }) }); export const useProjects = () => useQuery({ queryKey: qk.projects, queryFn: () => api.get<{ projects?: Project[] }>("/projects").then((r) => r.projects ?? []) }); // «Новый проект»: пакет обновления хаб ставит сам (batch_id), автор — viewer, чтобы доска подхватила его ход export function useCreateProject() { const qc = useQueryClient(); return useMutation({ mutationFn: (f: { label: string; name: string; board_id?: number }) => api.post("/projects", { ...f, author: "viewer" }), onSuccess: () => qc.invalidateQueries({ queryKey: qk.projects }), // ждём список: onCreated уже видит новый проект }); } export const useIssue = (key: string) => useQuery({ queryKey: qk.issue(key), queryFn: () => api.get(`/issues/${encodeURIComponent(key)}`) }); // список порциями по ISSUE_PAGE: следующая порция есть, пока последняя полная export const useIssues = (filters: IssueFilters) => useInfiniteQuery({ 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(`/issues/${encodeURIComponent(key)}/note`, { ...fields, updated_by: "web" }), onSuccess: (note) => { qc.setQueryData(qk.issue(key), (d) => (d ? { ...d, note } : d)); void qc.invalidateQueries({ queryKey: ["issues"] }); void qc.invalidateQueries({ queryKey: qk.boardAll }); }, }); }