feat(tests): review panels, blind visibility UX, red_review/blue_review badges

This commit is contained in:
kitos
2026-07-06 12:02:14 +02:00
parent 82033b5037
commit d5e3df00f9
11 changed files with 449 additions and 11 deletions
+63
View File
@@ -14,6 +14,8 @@ import {
startBlueWork,
validateAsRedLead,
validateAsBlueLead,
reviewAsRedLead,
reviewAsBlueLead,
reopenTest,
pauseTimer,
resumeTimer,
@@ -29,6 +31,7 @@ import type { TestResult, ContainmentResult, TeamSide, TestTimelineEntry } from
import TestDetailHeader from "../components/test-detail/TestDetailHeader";
import TeamTabs from "../components/test-detail/TeamTabs";
import ValidationModal from "../components/test-detail/ValidationModal";
import ReviewModal from "../components/test-detail/ReviewModal";
import ConfirmDialog from "../components/ConfirmDialog";
import JiraLinkPanel from "../components/JiraLinkPanel";
import TestPhaseTimeline from "../components/TestPhaseTimeline";
@@ -56,6 +59,11 @@ export default function TestDetailPage() {
side: "red" | "blue";
}>({ open: false, side: "red" });
const [reviewModal, setReviewModal] = useState<{
open: boolean;
side: "red" | "blue";
}>({ open: false, side: "red" });
const [confirmReopen, setConfirmReopen] = useState(false);
const [holdModal, setHoldModal] = useState(false);
const [holdReason, setHoldReason] = useState("");
@@ -254,6 +262,28 @@ export default function TestDetailPage() {
onError: (err: unknown) => showToast(extractError(err), "error"),
});
const reviewRedMutation = useMutation({
mutationFn: (payload: { decision: "approve" | "reopen"; notes?: string }) =>
reviewAsRedLead(testId!, payload),
onSuccess: () => {
invalidateAll();
setReviewModal({ open: false, side: "red" });
showToast("Red review submitted", "success");
},
onError: (err: unknown) => showToast(extractError(err), "error"),
});
const reviewBlueMutation = useMutation({
mutationFn: (payload: { decision: "approve" | "reopen" | "gap"; notes?: string; system_gaps?: string }) =>
reviewAsBlueLead(testId!, payload),
onSuccess: () => {
invalidateAll();
setReviewModal({ open: false, side: "blue" });
showToast("Blue review submitted", "success");
},
onError: (err: unknown) => showToast(extractError(err), "error"),
});
const reopenMutation = useMutation({
mutationFn: () => reopenTest(testId!),
onSuccess: () => {
@@ -360,6 +390,22 @@ export default function TestDetailPage() {
}
};
const handleReviewRedSubmit = (decision: "approve" | "reopen", notes: string) => {
reviewRedMutation.mutate({ decision, notes: notes || undefined });
};
const handleReviewBlueSubmit = (
decision: "approve" | "reopen" | "gap",
notes: string,
systemGaps: string,
) => {
reviewBlueMutation.mutate({
decision,
notes: notes || undefined,
system_gaps: decision === "gap" ? systemGaps : undefined,
});
};
const isTransitioning =
startExecMutation.isPending ||
submitRedMutation.isPending ||
@@ -439,6 +485,7 @@ export default function TestDetailPage() {
onSubmitBlue={() => submitBlueMutation.mutate()}
onStartBlueWork={() => startBlueWorkMutation.mutate()}
onOpenValidateModal={(side) => setValidationModal({ open: true, side })}
onOpenReviewModal={(side) => setReviewModal({ open: true, side })}
onReopen={() => setConfirmReopen(true)}
onPauseTimer={() => pauseTimerMutation.mutate()}
onResumeTimer={() => resumeTimerMutation.mutate()}
@@ -717,6 +764,22 @@ export default function TestDetailPage() {
/>
)}
{/* Review Modal (red_review / blue_review lead gate) */}
{reviewModal.open && (
<ReviewModal
side={reviewModal.side}
test={test}
isSubmitting={
reviewModal.side === "red"
? reviewRedMutation.isPending
: reviewBlueMutation.isPending
}
onSubmitRed={reviewModal.side === "red" ? handleReviewRedSubmit : undefined}
onSubmitBlue={reviewModal.side === "blue" ? handleReviewBlueSubmit : undefined}
onClose={() => setReviewModal({ open: false, side: "red" })}
/>
)}
{/* Save as Template Modal */}
{showTemplateModal && (
<SaveAsTemplateModal