feat: appove and go
This commit is contained in:
@@ -99,19 +99,39 @@ export function Approvals() {
|
||||
});
|
||||
|
||||
const decide = useMutation({
|
||||
mutationFn: ({ id, decision }: { id: string; decision: 'approve' | 'reject' }) => {
|
||||
mutationFn: async ({ proposal, decision }: { proposal: ApprovalProposal; decision: 'approve' | 'reject' }) => {
|
||||
if (!actor) throw new Error('Authenticated session is unavailable.');
|
||||
return api.decideApproval(actor, { id, decision, reason: decisionReason.trim() });
|
||||
const response = await api.decideApproval(actor, { id: proposal.id, decision, reason: decisionReason.trim() });
|
||||
if (decision !== 'approve' || proposal.action !== 'goal.workspace.execute') {
|
||||
return { response, continuedGoalId: null };
|
||||
}
|
||||
|
||||
const goalId = proposal.payload.goal_id;
|
||||
if (typeof goalId !== 'string' || goalId.length === 0) {
|
||||
throw new Error(`Request ${proposal.id} was approved, but its goal identifier is missing. Continue from Goal Orchestrator.`);
|
||||
}
|
||||
try {
|
||||
await api.applyGoal({ ...actor, project: proposal.project }, goalId);
|
||||
return { response, continuedGoalId: goalId };
|
||||
} catch (error) {
|
||||
throw new Error(`Request ${proposal.id} was approved, but apply/verification did not complete: ${errorMessage(error, 'unknown apply error')}`);
|
||||
}
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
onSuccess: ({ response, continuedGoalId }) => {
|
||||
const verb = response.proposal.status === 'approved' ? 'approved' : 'rejected';
|
||||
setNotice({ tone: 'success', text: `Request ${response.proposal.id} was ${verb}.` });
|
||||
const continuation = continuedGoalId ? ' The governed change was applied and verified.' : '';
|
||||
setNotice({ tone: 'success', text: `Request ${response.proposal.id} was ${verb}.${continuation}` });
|
||||
void queryClient.invalidateQueries({ queryKey: ['approvals'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['settings'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['goals'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['goal'] });
|
||||
},
|
||||
onError: (error) => setNotice({ tone: 'error', text: errorMessage(error, 'The approval decision could not be recorded.') }),
|
||||
onError: (error) => {
|
||||
setNotice({ tone: 'error', text: errorMessage(error, 'The approval decision could not be recorded.') });
|
||||
void queryClient.invalidateQueries({ queryKey: ['approvals'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['goals'] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['goal'] });
|
||||
},
|
||||
});
|
||||
|
||||
if (session.isLoading || (actor && inbox.isLoading)) {
|
||||
@@ -184,7 +204,7 @@ export function Approvals() {
|
||||
<div className="mt-5 space-y-3">
|
||||
{inbox.data.proposals.map((proposal) => {
|
||||
const eligibility = reviewEligibility(actor, proposal);
|
||||
const isCurrentDecision = decide.isPending && decide.variables?.id === proposal.id;
|
||||
const isCurrentDecision = decide.isPending && decide.variables?.proposal.id === proposal.id;
|
||||
return (
|
||||
<article key={proposal.id} className="rounded-2xl border border-slate-200 bg-white p-4 transition hover:border-slate-300 hover:shadow-[0_12px_28px_rgba(15,23,42,0.055)] sm:p-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
@@ -211,8 +231,8 @@ export function Approvals() {
|
||||
<span className={`text-xs font-medium ${eligibility.allowed ? 'text-emerald-700' : 'text-amber-700'}`}>{eligibility.reason}</span>
|
||||
{eligibility.allowed && (
|
||||
<div className="flex gap-2">
|
||||
<button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ id: proposal.id, decision: 'reject' })} className="rounded-lg border border-rose-200 bg-white px-3.5 py-2 text-xs font-semibold text-rose-700 transition hover:bg-rose-50 focus:outline-none focus:ring-4 focus:ring-rose-100 disabled:cursor-not-allowed disabled:opacity-40">{isCurrentDecision && decide.variables?.decision === 'reject' ? 'Rejecting…' : 'Reject'}</button>
|
||||
<button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ id: proposal.id, decision: 'approve' })} className="rounded-lg bg-emerald-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-emerald-800 focus:outline-none focus:ring-4 focus:ring-emerald-100 disabled:cursor-not-allowed disabled:bg-slate-300">{isCurrentDecision && decide.variables?.decision === 'approve' ? 'Approving…' : 'Approve'}</button>
|
||||
<button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ proposal, decision: 'reject' })} className="rounded-lg border border-rose-200 bg-white px-3.5 py-2 text-xs font-semibold text-rose-700 transition hover:bg-rose-50 focus:outline-none focus:ring-4 focus:ring-rose-100 disabled:cursor-not-allowed disabled:opacity-40">{isCurrentDecision && decide.variables?.decision === 'reject' ? 'Rejecting…' : 'Reject'}</button>
|
||||
<button type="button" disabled={decide.isPending || decisionReason.trim().length < 5} onClick={() => decide.mutate({ proposal, decision: 'approve' })} className="rounded-lg bg-emerald-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-emerald-800 focus:outline-none focus:ring-4 focus:ring-emerald-100 disabled:cursor-not-allowed disabled:bg-slate-300">{isCurrentDecision && decide.variables?.decision === 'approve' ? 'Approving and continuing…' : proposal.action === 'goal.workspace.execute' ? 'Approve & continue' : 'Approve'}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user