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 role = user?.role ?? "";
|
||||||
const currentIdx = STATE_INDEX[test.state];
|
const currentIdx = STATE_INDEX[test.state];
|
||||||
const [showDiscussModal, setShowDiscussModal] = useState(false);
|
const [showDiscussModal, setShowDiscussModal] = useState(false);
|
||||||
|
const [discussionSent, setDiscussionSent] = useState(false);
|
||||||
const [discussResult, setDiscussResult] = useState<{
|
const [discussResult, setDiscussResult] = useState<{
|
||||||
username: string; email: string | null; role: string;
|
username: string; email: string | null; role: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const discussMutation = useMutation({
|
const discussMutation = useMutation({
|
||||||
mutationFn: () => requestDiscussion(test.id),
|
mutationFn: () => requestDiscussion(test.id),
|
||||||
onSuccess: (data) => setDiscussResult({
|
onSuccess: (data) => {
|
||||||
|
setDiscussResult({
|
||||||
username: data.rejector_username,
|
username: data.rejector_username,
|
||||||
email: data.rejector_email,
|
email: data.rejector_email,
|
||||||
role: data.rejector_role,
|
role: data.rejector_role,
|
||||||
}),
|
});
|
||||||
|
setDiscussionSent(true);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatDate = (d: string | null) => {
|
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") {
|
if (test.state === "disputed") {
|
||||||
const canChangeVote =
|
const isApproving =
|
||||||
(role === "red_lead" && test.red_validation_status === "approved") ||
|
(role === "red_lead" && test.red_validation_status === "approved") ||
|
||||||
(role === "blue_lead" && test.blue_validation_status === "approved") ||
|
(role === "blue_lead" && test.blue_validation_status === "approved") ||
|
||||||
role === "admin";
|
(role === "admin" && test.red_validation_status === "approved");
|
||||||
|
|
||||||
if (canChangeVote) {
|
const isRejecting =
|
||||||
const side = role === "red_lead" ? "red" : role === "blue_lead" ? "blue" : null;
|
(role === "red_lead" && test.red_validation_status === "rejected") ||
|
||||||
if (side || role === "admin") {
|
(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(
|
buttons.push(
|
||||||
<div key="disputed-actions" className="flex items-center gap-2">
|
<div key="disputed-approver" className="flex flex-col items-end gap-1.5">
|
||||||
{/* Confirm keeping approval → open discussion modal */}
|
<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
|
<button
|
||||||
onClick={() => { setShowDiscussModal(true); setDiscussResult(null); }}
|
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"
|
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" />
|
<MessageSquare className="h-4 w-4" />
|
||||||
Confirm My Validation
|
Request Discussion
|
||||||
</button>
|
</button>
|
||||||
{/* Change vote to rejected */}
|
)}
|
||||||
|
{/* Change approving vote to rejected */}
|
||||||
<button
|
<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"
|
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" />
|
<XCircle className="h-4 w-4" />
|
||||||
Change to Rejected
|
Change to Rejected
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-gray-500">
|
||||||
|
You approved — change your vote or request a discussion
|
||||||
|
</p>
|
||||||
</div>,
|
</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