Patchpass V1
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { requests } from "../api/client";
|
||||
import type { ChangeRequest } from "../api/types";
|
||||
import { Modal } from "./Modal";
|
||||
import { Button } from "./ui";
|
||||
|
||||
type Decision = "APPROVE" | "REJECT" | "REQUEST_CHANGES";
|
||||
|
||||
const MAX = 500;
|
||||
|
||||
const meta: Record<Decision, { title: string; verb: string; variant: "success" | "danger" | "primary"; blurb: string; commentRequired: boolean }> = {
|
||||
APPROVE: {
|
||||
title: "Approve request",
|
||||
verb: "Approve",
|
||||
variant: "success",
|
||||
blurb: "The agent will be allowed to proceed. A platform-signed receipt will be issued.",
|
||||
commentRequired: false,
|
||||
},
|
||||
REJECT: {
|
||||
title: "Reject request",
|
||||
verb: "Reject",
|
||||
variant: "danger",
|
||||
blurb: "This is a hard blocker. The agent must submit a new request to try again.",
|
||||
commentRequired: false,
|
||||
},
|
||||
REQUEST_CHANGES: {
|
||||
title: "Request changes",
|
||||
verb: "Request changes",
|
||||
variant: "primary",
|
||||
blurb: "The agent will see your notes and can update the request for re-review.",
|
||||
commentRequired: true,
|
||||
},
|
||||
};
|
||||
|
||||
export function DecisionModal({
|
||||
request,
|
||||
decision,
|
||||
onClose,
|
||||
onDone,
|
||||
}: {
|
||||
request: ChangeRequest;
|
||||
decision: Decision | null;
|
||||
onClose: () => void;
|
||||
onDone: (updated: ChangeRequest) => void;
|
||||
}) {
|
||||
const [comment, setComment] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setComment("");
|
||||
}, [decision]);
|
||||
|
||||
if (!decision) return null;
|
||||
const m = meta[decision];
|
||||
const tooLong = comment.length > MAX;
|
||||
const missingRequired = m.commentRequired && comment.trim().length === 0;
|
||||
|
||||
const submit = async () => {
|
||||
if (tooLong || missingRequired) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const updated = await requests.decide(request.request_id, decision, comment.trim() || undefined);
|
||||
toast.success(`${m.verb}d`);
|
||||
onDone(updated);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={!!decision}
|
||||
onClose={onClose}
|
||||
title={m.title}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={m.variant} onClick={submit} loading={loading} disabled={tooLong || missingRequired}>
|
||||
{m.verb}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-border bg-surface-raised/50 p-3">
|
||||
<p className="text-sm font-medium text-text">{request.title}</p>
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
{request.changes.length} change{request.changes.length === 1 ? "" : "s"} ·{" "}
|
||||
{request.agent?.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted">{m.blurb}</p>
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-sm text-muted">
|
||||
Comment {m.commentRequired ? <span className="text-changes">(required)</span> : "(optional)"}
|
||||
</span>
|
||||
<span className={`text-xs ${tooLong ? "text-rejected" : comment.length > MAX * 0.8 ? "text-pending" : "text-faint"}`}>
|
||||
{comment.length}/{MAX}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
rows={4}
|
||||
autoFocus
|
||||
placeholder={m.commentRequired ? "Explain what needs to change…" : "Add an optional note…"}
|
||||
className={`w-full rounded-lg border bg-bg px-3 py-2 text-sm text-text outline-none focus:border-primary ${
|
||||
tooLong ? "border-rejected" : "border-border-strong"
|
||||
}`}
|
||||
/>
|
||||
{tooLong && <p className="mt-1 text-xs text-rejected">Comment is too long (max {MAX}).</p>}
|
||||
{missingRequired && (
|
||||
<p className="mt-1 text-xs text-changes">A comment is required when requesting changes.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user