import { useCallback } from "react"; import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query"; import type { IssueRetryNowOutcome, IssueRetryNowResponse } from "@paperclipai/shared"; import { ApiError } from "../api/client"; import { issuesApi } from "../api/issues"; import { useToastActions } from "../context/ToastContext"; import { queryKeys } from "../lib/queryKeys"; export type RetryNowError = { message: string; outcomeMessage: string | null; status: number | null; }; function readErrorMessage(error: unknown): string { if (error instanceof ApiError) { if (typeof error.message === "string" && error.message.trim().length > 0) return error.message; return `Request failed (${error.status})`; } if (error instanceof Error && error.message) return error.message; return "The request failed. Try again in a moment."; } export const RETRY_NOW_OUTCOME_HEADLINE: Record = { promoted: "Retry promoted", already_promoted: "Retry already running", no_scheduled_retry: "No scheduled retry", gate_suppressed: "Couldn't retry now", }; export function useRetryNowMutation( issueId: string | null | undefined, ): UseMutationResult & { lastError: RetryNowError | null; } { const queryClient = useQueryClient(); const { pushToast } = useToastActions(); const mutation = useMutation({ mutationFn: () => { if (!issueId) throw new Error("Missing issue id"); return issuesApi.retryScheduledRetryNow(issueId); }, onSuccess: (response) => { if (issueId) { queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(issueId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(issueId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId) }); } if (response.outcome === "promoted") { pushToast({ title: RETRY_NOW_OUTCOME_HEADLINE.promoted, body: response.message, tone: "success", }); } else if (response.outcome === "gate_suppressed") { pushToast({ title: RETRY_NOW_OUTCOME_HEADLINE.gate_suppressed, body: response.message, tone: "error", }); } }, onError: (error) => { pushToast({ title: "Couldn't retry now", body: readErrorMessage(error), tone: "error", }); }, }); const reset = mutation.reset; const wrappedReset = useCallback(() => reset(), [reset]); const lastError: RetryNowError | null = (() => { if (mutation.error) { const apiError = mutation.error instanceof ApiError ? mutation.error : null; return { message: readErrorMessage(mutation.error), outcomeMessage: null, status: apiError?.status ?? null, }; } if (mutation.data && mutation.data.outcome === "gate_suppressed") { return { message: mutation.data.message, outcomeMessage: mutation.data.message, status: null, }; } return null; })(); return { ...mutation, reset: wrappedReset, lastError, } as UseMutationResult & { lastError: RetryNowError | null; }; }