sdlc-hub/web/src/api/queries.ts

80 lines
4.7 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 "./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<Record<(typeof ISSUE_FILTER_NAMES)[number] | "hide_closed" | "project", string>>;
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<ChangesResponse>("/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<HubSettings>("/settings"), staleTime: Infinity });
// 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 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<CreatedProject>("/projects", { ...f, author: "viewer" }),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.projects }), // ждём список: onCreated уже видит новый проект
});
}
export const useIssue = (key: string) =>
useQuery({ queryKey: qk.issue(key), queryFn: () => api.get<IssueDetail>(`/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<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 });
},
});
}