80 lines
4.7 KiB
TypeScript
80 lines
4.7 KiB
TypeScript
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 });
|
||
},
|
||
});
|
||
}
|