feat(disputed): symmetric UX for both leads in disputed state
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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({
|
||||
username: data.rejector_username,
|
||||
email: data.rejector_email,
|
||||
role: data.rejector_role,
|
||||
}),
|
||||
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") {
|
||||
buttons.push(
|
||||
<div key="disputed-actions" className="flex items-center gap-2">
|
||||
{/* Confirm keeping approval → open discussion modal */}
|
||||
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-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"
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Request Discussion
|
||||
</button>
|
||||
)}
|
||||
{/* Change approving vote to rejected */}
|
||||
<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
|
||||
</button>
|
||||
{/* Change 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>,
|
||||
);
|
||||
}
|
||||
</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>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user