Files
paperclip/ui/src/pages/Approvals.tsx
Forgotten 3b81557f7c UI: URL-based tab routing, ActivityRow extraction, and agent detail redesign
Switch agents, issues, and approvals pages from query-param tabs to
URL-based routes (/agents/active, /issues/backlog, /approvals/pending).
Extract shared ActivityRow component used by both Dashboard and Activity
pages. Redesign agent detail overview with LatestRunCard showing live/
recent run status, move permissions toggle to Configuration tab, add
budget progress bar, and reorder tabs (Runs before Configuration).
Dashboard now counts idle agents as active and shows "Recent Tasks"
instead of "Stale Tasks". Remove unused MyIssues page and sidebar link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:39:48 -06:00

129 lines
4.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { approvalsApi } from "../api/approvals";
import { agentsApi } from "../api/agents";
import { useCompany } from "../context/CompanyContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
import { PageTabBar } from "../components/PageTabBar";
import { Tabs } from "@/components/ui/tabs";
import { ShieldCheck } from "lucide-react";
import { ApprovalCard } from "../components/ApprovalCard";
type StatusFilter = "pending" | "all";
export function Approvals() {
const { selectedCompanyId } = useCompany();
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const navigate = useNavigate();
const location = useLocation();
const pathSegment = location.pathname.split("/").pop() ?? "pending";
const statusFilter: StatusFilter = pathSegment === "all" ? "all" : "pending";
const [actionError, setActionError] = useState<string | null>(null);
useEffect(() => {
setBreadcrumbs([{ label: "Approvals" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
queryKey: queryKeys.approvals.list(selectedCompanyId!),
queryFn: () => approvalsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const { data: agents } = useQuery({
queryKey: queryKeys.agents.list(selectedCompanyId!),
queryFn: () => agentsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const approveMutation = useMutation({
mutationFn: (id: string) => approvalsApi.approve(id),
onSuccess: (_approval, id) => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
navigate(`/approvals/${id}?resolved=approved`);
},
onError: (err) => {
setActionError(err instanceof Error ? err.message : "Failed to approve");
},
});
const rejectMutation = useMutation({
mutationFn: (id: string) => approvalsApi.reject(id),
onSuccess: () => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(selectedCompanyId!) });
},
onError: (err) => {
setActionError(err instanceof Error ? err.message : "Failed to reject");
},
});
const filtered = (data ?? [])
.filter(
(a) => statusFilter === "all" || a.status === "pending" || a.status === "revision_requested",
)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
const pendingCount = (data ?? []).filter(
(a) => a.status === "pending" || a.status === "revision_requested",
).length;
if (!selectedCompanyId) {
return <p className="text-sm text-muted-foreground">Select a company first.</p>;
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Tabs value={statusFilter} onValueChange={(v) => navigate(`/approvals/${v}`)}>
<PageTabBar items={[
{ value: "pending", label: <>Pending{pendingCount > 0 && (
<span className={cn(
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
"bg-yellow-500/20 text-yellow-500"
)}>
{pendingCount}
</span>
)}</> },
{ value: "all", label: "All" },
]} />
</Tabs>
</div>
{isLoading && <p className="text-sm text-muted-foreground">Loading...</p>}
{error && <p className="text-sm text-destructive">{error.message}</p>}
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
{!isLoading && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-center">
<ShieldCheck className="h-8 w-8 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground">
{statusFilter === "pending" ? "No pending approvals." : "No approvals yet."}
</p>
</div>
)}
{filtered.length > 0 && (
<div className="grid gap-3">
{filtered.map((approval) => (
<ApprovalCard
key={approval.id}
approval={approval}
requesterAgent={approval.requestedByAgentId ? (agents ?? []).find((a) => a.id === approval.requestedByAgentId) ?? null : null}
onApprove={() => approveMutation.mutate(approval.id)}
onReject={() => rejectMutation.mutate(approval.id)}
onOpen={() => navigate(`/approvals/${approval.id}`)}
isPending={approveMutation.isPending || rejectMutation.isPending}
/>
))}
</div>
)}
</div>
);
}