Files
kandev/apps/web/hooks/domains/github/use-pr-feedback.ts
T
João Salavisa 5257dfbaab fix(web): close the i18n coverage gaps in the non-JSX scanner and locale catalogs (#2727)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:03:29 +00:00

104 lines
3.5 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback, useReducer } from "react";
import { getPRFeedback } from "@/lib/api/domains/github-api";
import type { PRFeedback } from "@/lib/types/github";
import { t } from "@/lib/i18n";
export type PRFeedbackState = {
/** `<workspaceId>/<owner>/<repo>/<prNumber>` of the request `feedback` belongs to. */
key: string;
feedback: PRFeedback | null;
loading: boolean;
error: string | null;
};
type PRFeedbackView = {
feedback: PRFeedback | null;
loading: boolean;
error: string | null;
};
type Action =
| { type: "fetch"; key: string }
| { type: "success"; key: string; feedback: PRFeedback }
| { type: "error"; key: string; message: string };
const INITIAL_STATE: PRFeedbackState = { key: "", feedback: null, loading: false, error: null };
function reducer(state: PRFeedbackState, action: Action): PRFeedbackState {
// Same-PR refresh keeps the cached feedback visible while it reloads;
// a different PR (e.g. a task switch reusing this hook instance) never
// inherits another PR's feedback.
const carried = state.key === action.key ? state.feedback : null;
switch (action.type) {
case "fetch":
return { key: action.key, feedback: carried, loading: true, error: null };
case "success":
return { key: action.key, feedback: action.feedback, loading: false, error: null };
case "error":
return { key: action.key, feedback: carried, loading: false, error: action.message };
}
}
/**
* Derives the feedback actually safe to render for `requestedKey`. Masks out
* `state.feedback` whenever it belongs to a different PR than the one
* currently requested — this runs on every render (not just after the fetch
* effect dispatches), so a PR/task switch stops showing the previous PR's
* reviews/comments immediately, before the new fetch even resolves.
*/
export function resolvePRFeedbackView(
state: PRFeedbackState,
requestedKey: string,
): PRFeedbackView {
if (state.key === requestedKey) {
return { feedback: state.feedback, loading: state.loading, error: state.error };
}
return { feedback: null, loading: requestedKey !== "", error: null };
}
/**
* Fetch live PR feedback (reviews, comments, checks) from GitHub.
* This is not stored in the global store since it's session-scoped
* and fetched on demand.
*/
export function usePRFeedback(
workspaceId: string | null,
owner: string | null,
repo: string | null,
prNumber: number | null,
) {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
const [fetchCount, setFetchCount] = useState(0);
const key =
workspaceId && owner && repo && prNumber ? `${workspaceId}/${owner}/${repo}/${prNumber}` : "";
const refresh = useCallback(() => {
setFetchCount((c) => c + 1);
}, []);
useEffect(() => {
if (!workspaceId || !owner || !repo || !prNumber || !key) return;
let cancelled = false;
dispatch({ type: "fetch", key });
getPRFeedback(workspaceId, owner, repo, prNumber, { cache: "no-store" })
.then((response) => {
if (!cancelled) dispatch({ type: "success", key, feedback: response });
})
.catch((err) => {
if (!cancelled)
dispatch({
type: "error",
key,
message: err instanceof Error ? err.message : t("github:failedToFetchPrFeedback"),
});
});
return () => {
cancelled = true;
};
}, [workspaceId, owner, repo, prNumber, key, fetchCount]);
return { ...resolvePRFeedbackView(state, key), refresh };
}