mirror of
https://github.com/paperclipai/paperclip
synced 2026-03-25 11:21:48 +00:00
shared: - Add IssueAncestor type and ancestors?: IssueAncestor[] to Issue interface IssueDetail: - Show clickable parent chain breadcrumb above issue title when issue has parents - Ancestors rendered root-first (reversed from API order) with chevron separators IssueProperties: - Project field now shows project name with a link instead of raw UUID - Assignee field now links to agent detail page - Parent field shows immediate parent title (from ancestors[0]) with link - Show request depth when > 0 Approvals: - Pending/All filter tabs with pending count badge - HireAgentPayload renders name, role, title, capabilities, adapter type - CeoStrategyPayload renders plan/description text in a monospace block - Decision note shown when present - Requester agent name shown in header - Approve button uses green styling; empty state with icon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
import { Link } from "react-router-dom";
|
|
import type { Issue } from "@paperclip/shared";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { agentsApi } from "../api/agents";
|
|
import { projectsApi } from "../api/projects";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { StatusIcon } from "./StatusIcon";
|
|
import { PriorityIcon } from "./PriorityIcon";
|
|
import { formatDate } from "../lib/utils";
|
|
import { timeAgo } from "../lib/timeAgo";
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
interface IssuePropertiesProps {
|
|
issue: Issue;
|
|
onUpdate: (data: Record<string, unknown>) => void;
|
|
}
|
|
|
|
function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex items-center justify-between py-1.5">
|
|
<span className="text-xs text-muted-foreground">{label}</span>
|
|
<div className="flex items-center gap-1.5">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function statusLabel(status: string): string {
|
|
return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
}
|
|
|
|
function priorityLabel(priority: string): string {
|
|
return priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
}
|
|
|
|
export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) {
|
|
const { selectedCompanyId } = useCompany();
|
|
|
|
const { data: agents } = useQuery({
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
const { data: projects } = useQuery({
|
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId && !!issue.projectId,
|
|
});
|
|
|
|
const agentName = (id: string | null) => {
|
|
if (!id || !agents) return null;
|
|
const agent = agents.find((a) => a.id === id);
|
|
return agent?.name ?? id.slice(0, 8);
|
|
};
|
|
|
|
const projectName = (id: string | null) => {
|
|
if (!id || !projects) return id?.slice(0, 8) ?? "None";
|
|
const project = projects.find((p) => p.id === id);
|
|
return project?.name ?? id.slice(0, 8);
|
|
};
|
|
|
|
const assignee = issue.assigneeAgentId
|
|
? agents?.find((a) => a.id === issue.assigneeAgentId)
|
|
: null;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="space-y-1">
|
|
<PropertyRow label="Status">
|
|
<StatusIcon
|
|
status={issue.status}
|
|
onChange={(status) => onUpdate({ status })}
|
|
/>
|
|
<span className="text-sm">{statusLabel(issue.status)}</span>
|
|
</PropertyRow>
|
|
|
|
<PropertyRow label="Priority">
|
|
<PriorityIcon
|
|
priority={issue.priority}
|
|
onChange={(priority) => onUpdate({ priority })}
|
|
/>
|
|
<span className="text-sm">{priorityLabel(issue.priority)}</span>
|
|
</PropertyRow>
|
|
|
|
<PropertyRow label="Assignee">
|
|
{assignee ? (
|
|
<Link
|
|
to={`/agents/${assignee.id}`}
|
|
className="text-sm hover:underline"
|
|
>
|
|
{assignee.name}
|
|
</Link>
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">Unassigned</span>
|
|
)}
|
|
</PropertyRow>
|
|
|
|
{issue.projectId && (
|
|
<PropertyRow label="Project">
|
|
<Link
|
|
to={`/projects/${issue.projectId}`}
|
|
className="text-sm hover:underline"
|
|
>
|
|
{projectName(issue.projectId)}
|
|
</Link>
|
|
</PropertyRow>
|
|
)}
|
|
|
|
{issue.parentId && (
|
|
<PropertyRow label="Parent">
|
|
<Link
|
|
to={`/issues/${issue.parentId}`}
|
|
className="text-sm hover:underline"
|
|
>
|
|
{issue.ancestors?.[0]?.title ?? issue.parentId.slice(0, 8)}
|
|
</Link>
|
|
</PropertyRow>
|
|
)}
|
|
|
|
{issue.requestDepth > 0 && (
|
|
<PropertyRow label="Depth">
|
|
<span className="text-sm font-mono">{issue.requestDepth}</span>
|
|
</PropertyRow>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-1">
|
|
{issue.startedAt && (
|
|
<PropertyRow label="Started">
|
|
<span className="text-sm">{formatDate(issue.startedAt)}</span>
|
|
</PropertyRow>
|
|
)}
|
|
{issue.completedAt && (
|
|
<PropertyRow label="Completed">
|
|
<span className="text-sm">{formatDate(issue.completedAt)}</span>
|
|
</PropertyRow>
|
|
)}
|
|
<PropertyRow label="Created">
|
|
<span className="text-sm">{formatDate(issue.createdAt)}</span>
|
|
</PropertyRow>
|
|
<PropertyRow label="Updated">
|
|
<span className="text-sm">{timeAgo(issue.updatedAt)}</span>
|
|
</PropertyRow>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|