src/features/reviews/use-finish-flow.ts
Linked repository file from this build.
import { useCallback, useEffect, useRef, useState } from "react";
import type { SnapReview } from "@/programs/snap-negative/types";
import { canSubmitPolicyDecision, type ReviewCommand } from "./commands";
import type { Actor, DemoRole } from "./types";
import type { FinishKind, PendingFinish } from "./workspace-finish-actions";
/** Approvals run the edit check first, so the dialog shows current findings. */
export function needsDecisionCheck(kind: FinishKind): boolean {
return (
(kind.type === "transition" && kind.action === "approve") ||
(kind.type === "policy" && kind.decision !== "deny")
);
}
/**
* The submit, approve, PEC and refusal steps of the review workspace.
* Each step saves pending edits first, then sends its command and saves again.
*/
export function useFinishFlow({
review,
actor,
perform,
flush,
setRole,
}: {
review: SnapReview;
actor: Actor;
perform(command: ReviewCommand): boolean;
flush(): Promise<boolean>;
setRole(role: DemoRole): void;
}) {
const [pendingFinish, setPendingFinish] = useState<PendingFinish | null>(null);
const [submitAttempted, setSubmitAttempted] = useState(false);
const finishRequest = useRef(0);
const reviewRef = useRef(review);
const actorRef = useRef(actor);
useEffect(() => {
reviewRef.current = review;
actorRef.current = actor;
}, [review, actor]);
// An edit while the dialog is open makes its check stale; run it again.
useEffect(() => {
if (!pendingFinish || review.validation?.revision === review.revision) return;
if (!needsDecisionCheck(pendingFinish.kind)) return;
let active = true;
void (async () => {
if (!(await flush()) || !active) return;
if (perform({ type: "prepare_decision" })) await flush();
})();
return () => {
active = false;
};
}, [pendingFinish, review.revision, review.validation?.revision, flush, perform]);
function changePendingFinish(pending: PendingFinish | null) {
const request = ++finishRequest.current;
if (pending && needsDecisionCheck(pending.kind)) {
void (async () => {
if (!(await flush()) || request !== finishRequest.current) return;
if (!perform({ type: "prepare_decision" }) || !(await flush())) return;
if (request === finishRequest.current) setPendingFinish(pending);
})();
} else setPendingFinish(pending);
if (pending?.kind.type === "submit") setSubmitAttempted(true);
}
function changeRole(role: DemoRole) {
finishRequest.current++;
setPendingFinish(null);
setRole(role);
}
async function executeFinish({
kind,
note,
skipPec,
policyNote,
}: {
kind: FinishKind;
note: string;
skipPec: boolean;
policyNote: string;
}) {
if (kind.type === "submit") setSubmitAttempted(true);
if (!(await flush())) return false;
if (kind.type === "submit")
return perform({ type: "transition", action: "submit", note }) && (await flush());
if (needsDecisionCheck(kind) && (!perform({ type: "prepare_decision" }) || !(await flush())))
return false;
if (kind.type === "transition") {
const action =
kind.action === "approve" && reviewRef.current.outcome !== "drop" && !skipPec
? "route_pec"
: kind.action;
return perform({ type: "transition", action, note }) && (await flush());
}
if (kind.type === "policy") {
if (!canSubmitPolicyDecision(reviewRef.current, actorRef.current)) return false;
const ok = perform({
type: "policy_decision",
decision: kind.decision,
policyNote: policyNote.trim() || note.trim(),
});
return ok && (await flush());
}
return false;
}
/** Refuse a finding by its content, since the fresh check gives findings new IDs. */
const refuseFinding = useCallback(
(findingId: string, reason: string) => {
void (async () => {
const intended = reviewRef.current.validation?.findings.find(
(finding) => finding.id === findingId,
);
if (!intended || !(await flush())) return;
if (!perform({ type: "prepare_decision" }) || !(await flush())) return;
const current = reviewRef.current.validation?.findings.find(
(finding) =>
finding.field === intended.field &&
finding.code === intended.code &&
finding.message === intended.message,
);
if (current) {
perform({ type: "refuse_finding", findingId: current.id, reason });
await flush();
}
})();
},
[perform, flush],
);
return {
pendingFinish,
submitAttempted,
changePendingFinish,
changeRole,
executeFinish,
refuseFinding,
};
}