"use client"; import { useMemo, useState } from "react"; import { Lock, UserMinus } from "lucide-react"; import type { Agent, IssueAssigneeType, UpdateIssueRequest } from "@multica/core/types"; import { useQuery } from "@tanstack/react-query"; import { useAuthStore } from "@multica/core/auth"; import { useActorName } from "@multica/core/workspace/hooks"; import { useWorkspaceId } from "@multica/core/hooks"; import { memberListOptions, agentListOptions, assigneeFrequencyOptions } from "@multica/core/workspace/queries"; import { ActorAvatar } from "../../../common/actor-avatar"; import { PropertyPicker, PickerItem, PickerSection, PickerEmpty, } from "./property-picker"; export function canAssignAgent(agent: Agent, userId: string | undefined, memberRole: string | undefined): boolean { if (agent.visibility !== "private") return true; if (agent.owner_id === userId) return true; if (memberRole === "owner" || memberRole === "admin") return true; return false; } export function AssigneePicker({ assigneeType, assigneeId, onUpdate, trigger: customTrigger, triggerRender, open: controlledOpen, onOpenChange: controlledOnOpenChange, align, }: { assigneeType: IssueAssigneeType | null; assigneeId: string | null; onUpdate: (updates: Partial) => void; trigger?: React.ReactNode; triggerRender?: React.ReactElement; open?: boolean; onOpenChange?: (v: boolean) => void; align?: "start" | "center" | "end"; }) { const [internalOpen, setInternalOpen] = useState(false); const open = controlledOpen ?? internalOpen; const setOpen = controlledOnOpenChange ?? setInternalOpen; const [filter, setFilter] = useState(""); const user = useAuthStore((s) => s.user); const wsId = useWorkspaceId(); const { data: members = [] } = useQuery(memberListOptions(wsId)); const { data: agents = [] } = useQuery(agentListOptions(wsId)); const { data: frequency = [] } = useQuery(assigneeFrequencyOptions(wsId)); const { getActorName } = useActorName(); const currentMember = members.find((m) => m.user_id === user?.id); const memberRole = currentMember?.role; // Build a lookup map from frequency data for sorting. const freqMap = useMemo(() => { const map = new Map(); for (const entry of frequency) { map.set(`${entry.assignee_type}:${entry.assignee_id}`, entry.frequency); } return map; }, [frequency]); const getFreq = (type: string, id: string) => freqMap.get(`${type}:${id}`) ?? 0; const query = filter.trim().toLowerCase(); const filteredMembers = members .filter((m) => m.name.toLowerCase().includes(query)) .sort((a, b) => getFreq("member", b.user_id) - getFreq("member", a.user_id)); const filteredAgents = agents .filter((a) => !a.archived_at && a.name.toLowerCase().includes(query)) .sort((a, b) => getFreq("agent", b.id) - getFreq("agent", a.id)); const isSelected = (type: string, id: string) => assigneeType === type && assigneeId === id; const triggerLabel = assigneeType && assigneeId ? getActorName(assigneeType, assigneeId) : "Unassigned"; return ( { setOpen(v); if (!v) setFilter(""); }} width="w-52" align={align} searchable searchPlaceholder="Assign to..." onSearchChange={setFilter} triggerRender={triggerRender} trigger={ customTrigger ? customTrigger : assigneeType && assigneeId ? ( <> {triggerLabel} ) : ( Unassigned ) } > {/* Unassigned option — hidden when search is active */} {!query && ( { onUpdate({ assignee_type: null, assignee_id: null }); setOpen(false); }} > Unassigned )} {/* Members */} {filteredMembers.length > 0 && ( {filteredMembers.map((m) => ( { onUpdate({ assignee_type: "member", assignee_id: m.user_id, }); setOpen(false); }} > {m.name} ))} )} {/* Agents */} {filteredAgents.length > 0 && ( {filteredAgents.map((a) => { const allowed = canAssignAgent(a, user?.id, memberRole); return ( { if (!allowed) return; onUpdate({ assignee_type: "agent", assignee_id: a.id, }); setOpen(false); }} > {a.name} {a.visibility === "private" && ( )} ); })} )} {filteredMembers.length === 0 && filteredAgents.length === 0 && filter && } ); }