import { useMemo, useState } from "react"; import type { Agent, Issue } from "@paperclipai/shared"; import { useQuery } from "@tanstack/react-query"; import { accessApi } from "../api/access"; import { formatAssigneeUserLabel } from "../lib/assignees"; import { buildCompanyUserInlineOptions, buildCompanyUserLabelMap } from "../lib/company-members"; import { queryKeys } from "../lib/queryKeys"; import { sortAgentsByRecency, getRecentAssigneeIds } from "../lib/recent-assignees"; import { buildExecutionPolicy, stageParticipantValues, } from "../lib/issue-execution-policy"; import { cn } from "../lib/utils"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { User, Eye, ShieldCheck } from "lucide-react"; import { AgentIcon } from "./AgentIconPicker"; type StageType = "review" | "approval"; interface ExecutionParticipantPickerProps { issue: Issue; stageType: StageType; agents: Agent[]; currentUserId: string | null; onUpdate: (data: Record) => void; } export function ExecutionParticipantPicker({ issue, stageType, agents, currentUserId, onUpdate, }: ExecutionParticipantPickerProps) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const reviewerValues = stageParticipantValues(issue.executionPolicy, "review"); const approverValues = stageParticipantValues(issue.executionPolicy, "approval"); const values = stageType === "review" ? reviewerValues : approverValues; const { data: companyMembers } = useQuery({ queryKey: queryKeys.access.companyUserDirectory(issue.companyId), queryFn: () => accessApi.listUserDirectory(issue.companyId), enabled: !!issue.companyId, }); const sortedAgents = sortAgentsByRecency( agents.filter((a) => a.status !== "terminated"), getRecentAssigneeIds(), ); const userLabelMap = useMemo( () => buildCompanyUserLabelMap(companyMembers?.users), [companyMembers?.users], ); const otherUserOptions = useMemo( () => buildCompanyUserInlineOptions(companyMembers?.users, { excludeUserIds: [currentUserId, issue.createdByUserId] }), [companyMembers?.users, currentUserId, issue.createdByUserId], ); const userLabel = (userId: string | null | undefined) => formatAssigneeUserLabel(userId, currentUserId, userLabelMap); const creatorUserLabel = userLabel(issue.createdByUserId); const agentName = (id: string) => { const agent = agents.find((a) => a.id === id); return agent?.name ?? id.slice(0, 8); }; const participantLabel = (value: string) => { if (value.startsWith("agent:")) return agentName(value.slice("agent:".length)); if (value.startsWith("user:")) return userLabel(value.slice("user:".length)) ?? "User"; return value; }; const updatePolicy = (nextValues: string[]) => { onUpdate({ executionPolicy: buildExecutionPolicy({ existingPolicy: issue.executionPolicy ?? null, reviewerValues: stageType === "review" ? nextValues : reviewerValues, approverValues: stageType === "approval" ? nextValues : approverValues, }), }); }; const toggle = (value: string) => { const next = values.includes(value) ? values.filter((v) => v !== value) : [...values, value]; updatePolicy(next); }; const label = stageType === "review" ? "Reviewers" : "Approvers"; const Icon = stageType === "review" ? Eye : ShieldCheck; return ( { setOpen(o); if (!o) setSearch(""); }}> setSearch(e.target.value)} autoFocus />
{currentUserId && ( )} {issue.createdByUserId && issue.createdByUserId !== currentUserId && ( )} {otherUserOptions .filter((option) => { if (!search.trim()) return true; return `${option.label} ${option.searchText ?? ""}`.toLowerCase().includes(search.toLowerCase()); }) .map((option) => ( ))} {sortedAgents .filter((agent) => { if (!search.trim()) return true; return agent.name.toLowerCase().includes(search.toLowerCase()); }) .map((agent) => { const encoded = `agent:${agent.id}`; return ( ); })}
); }