import { useState } from "react"; import { Link } from "react-router-dom"; import type { Issue } from "@paperclip/shared"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { agentsApi } from "../api/agents"; import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; import { StatusIcon } from "./StatusIcon"; import { PriorityIcon } from "./PriorityIcon"; import { Identity } from "./Identity"; import { formatDate, cn } from "../lib/utils"; import { timeAgo } from "../lib/timeAgo"; import { Separator } from "@/components/ui/separator"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react"; import { AgentIcon } from "./AgentIconPicker"; interface IssuePropertiesProps { issue: Issue; onUpdate: (data: Record) => void; } function PropertyRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) { const { selectedCompanyId } = useCompany(); const queryClient = useQueryClient(); const companyId = issue.companyId ?? selectedCompanyId; const [assigneeOpen, setAssigneeOpen] = useState(false); const [assigneeSearch, setAssigneeSearch] = useState(""); const [projectOpen, setProjectOpen] = useState(false); const [projectSearch, setProjectSearch] = useState(""); const [labelsOpen, setLabelsOpen] = useState(false); const [labelSearch, setLabelSearch] = useState(""); const [newLabelName, setNewLabelName] = useState(""); const [newLabelColor, setNewLabelColor] = useState("#6366f1"); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(companyId!), queryFn: () => agentsApi.list(companyId!), enabled: !!companyId, }); const { data: projects } = useQuery({ queryKey: queryKeys.projects.list(companyId!), queryFn: () => projectsApi.list(companyId!), enabled: !!companyId, }); const { data: labels } = useQuery({ queryKey: queryKeys.issues.labels(companyId!), queryFn: () => issuesApi.listLabels(companyId!), enabled: !!companyId, }); const createLabel = useMutation({ mutationFn: (data: { name: string; color: string }) => issuesApi.createLabel(companyId!, data), onSuccess: async (created) => { await queryClient.invalidateQueries({ queryKey: queryKeys.issues.labels(companyId!) }); onUpdate({ labelIds: [...(issue.labelIds ?? []), created.id] }); setNewLabelName(""); }, }); const deleteLabel = useMutation({ mutationFn: (labelId: string) => issuesApi.deleteLabel(labelId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.issues.labels(companyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId!) }); queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issue.id) }); }, }); const toggleLabel = (labelId: string) => { const ids = issue.labelIds ?? []; const next = ids.includes(labelId) ? ids.filter((id) => id !== labelId) : [...ids, labelId]; onUpdate({ labelIds: next }); }; 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 (
onUpdate({ status })} showLabel /> onUpdate({ priority })} showLabel /> { setLabelsOpen(open); if (!open) setLabelSearch(""); }}> setLabelSearch(e.target.value)} autoFocus />
{(labels ?? []) .filter((label) => { if (!labelSearch.trim()) return true; return label.name.toLowerCase().includes(labelSearch.toLowerCase()); }) .map((label) => { const selected = (issue.labelIds ?? []).includes(label.id); return (
); })}
setNewLabelColor(e.target.value)} /> setNewLabelName(e.target.value)} />
{ setAssigneeOpen(open); if (!open) setAssigneeSearch(""); }}> setAssigneeSearch(e.target.value)} autoFocus />
{(agents ?? []) .filter((a) => a.status !== "terminated") .filter((a) => { if (!assigneeSearch.trim()) return true; const q = assigneeSearch.toLowerCase(); return a.name.toLowerCase().includes(q); }) .map((a) => ( ))}
{issue.assigneeAgentId && ( e.stopPropagation()} > )}
{ setProjectOpen(open); if (!open) setProjectSearch(""); }}> setProjectSearch(e.target.value)} autoFocus />
{(projects ?? []) .filter((p) => { if (!projectSearch.trim()) return true; const q = projectSearch.toLowerCase(); return p.name.toLowerCase().includes(q); }) .map((p) => ( ))}
{issue.projectId && ( e.stopPropagation()} > )}
{issue.parentId && ( {issue.ancestors?.[0]?.title ?? issue.parentId.slice(0, 8)} )} {issue.requestDepth > 0 && ( {issue.requestDepth} )}
{issue.startedAt && ( {formatDate(issue.startedAt)} )} {issue.completedAt && ( {formatDate(issue.completedAt)} )} {formatDate(issue.createdAt)} {timeAgo(issue.updatedAt)}
); }