feat(disputed): symmetric UX for both leads in disputed state
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Lead who approved: Request Discussion button becomes Discussion Requested after sending.
Lead who rejected: new Change to Approved button to resolve conflict after offline discussion.
Both leads retain vote-change buttons. discussionSent state flag tracks send status.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-03 14:09:52 +02:00
parent 02ff89401c
commit 460faf9935

View File

@@ -87,17 +87,21 @@ export default function TestDetailHeader({
const role = user?.role ?? "";
const currentIdx = STATE_INDEX[test.state];
const [showDiscussModal, setShowDiscussModal] = useState(false);
const [discussionSent, setDiscussionSent] = useState(false);
const [discussResult, setDiscussResult] = useState<{
username: string; email: string | null; role: string;
} | null>(null);
const discussMutation = useMutation({
mutationFn: () => requestDiscussion(test.id),
onSuccess: (data) => setDiscussResult({
onSuccess: (data) => {
setDiscussResult({
username: data.rejector_username,
email: data.rejector_email,
role: data.rejector_role,
}),
});
setDiscussionSent(true);
},
});
const formatDate = (d: string | null) => {
@@ -234,37 +238,74 @@ export default function TestDetailHeader({
);
}
// Disputed: the lead who approved can change their vote to rejected
// Disputed state: symmetric actions for both leads
if (test.state === "disputed") {
const canChangeVote =
const isApproving =
(role === "red_lead" && test.red_validation_status === "approved") ||
(role === "blue_lead" && test.blue_validation_status === "approved") ||
role === "admin";
(role === "admin" && test.red_validation_status === "approved");
if (canChangeVote) {
const side = role === "red_lead" ? "red" : role === "blue_lead" ? "blue" : null;
if (side || role === "admin") {
const isRejecting =
(role === "red_lead" && test.red_validation_status === "rejected") ||
(role === "blue_lead" && test.blue_validation_status === "rejected") ||
(role === "admin" && test.blue_validation_status === "rejected");
const approvingSide: "red" | "blue" =
test.red_validation_status === "approved" ? "red" : "blue";
const rejectingSide: "red" | "blue" =
test.red_validation_status === "rejected" ? "red" : "blue";
if (isApproving || role === "admin") {
buttons.push(
<div key="disputed-actions" className="flex items-center gap-2">
{/* Confirm keeping approval → open discussion modal */}
<div key="disputed-approver" className="flex flex-col items-end gap-1.5">
<div className="flex items-center gap-2">
{/* Discussion request button — shows ✓ after sent */}
{discussionSent ? (
<span className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm text-gray-500 cursor-not-allowed">
<CheckCircle className="h-4 w-4 text-green-500" />
Discussion Requested
</span>
) : (
<button
onClick={() => { setShowDiscussModal(true); setDiscussResult(null); }}
className="flex items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 py-2 text-sm font-medium text-amber-400 hover:bg-amber-500/20 transition-colors"
>
<UserCheck className="h-4 w-4" />
Confirm My Validation
<MessageSquare className="h-4 w-4" />
Request Discussion
</button>
{/* Change vote to rejected */}
)}
{/* Change approving vote to rejected */}
<button
onClick={() => onOpenValidateModal(side ?? (test.red_validation_status === "approved" ? "red" : "blue"))}
onClick={() => onOpenValidateModal(approvingSide)}
className="flex items-center gap-1.5 rounded-lg bg-red-700/80 px-4 py-2 text-sm font-medium text-white hover:bg-red-600 transition-colors"
>
<XCircle className="h-4 w-4" />
Change to Rejected
</button>
</div>
<p className="text-[10px] text-gray-500">
You approved change your vote or request a discussion
</p>
</div>,
);
}
if (isRejecting && !isApproving) {
buttons.push(
<div key="disputed-rejector" className="flex flex-col items-end gap-1.5">
{/* Change rejecting vote to approved */}
<button
onClick={() => onOpenValidateModal(rejectingSide)}
className="flex items-center gap-1.5 rounded-lg bg-green-700 px-4 py-2 text-sm font-medium text-white hover:bg-green-600 transition-colors"
>
<CheckCircle className="h-4 w-4" />
Change to Approved
</button>
<p className="text-[10px] text-gray-500">
You rejected change your vote if you agree after discussion
</p>
</div>,
);
}
}