import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { Popover } from "@mantine/core"; import { IconX } from "@tabler/icons-react"; import clsx from "clsx"; import { IBaseProperty, PersonTypeOptions, } from "@/features/base/types/base.types"; import { useWorkspaceMembersQuery } from "@/features/workspace/queries/workspace-query"; import { CustomAvatar } from "@/components/ui/custom-avatar"; import cellClasses from "@/features/base/styles/cells.module.css"; import { useListKeyboardNav } from "@/features/base/hooks/use-list-keyboard-nav"; type CellPersonProps = { value: unknown; property: IBaseProperty; rowId: string; isEditing: boolean; onCommit: (value: unknown) => void; onCancel: () => void; }; export function CellPerson({ value, property, isEditing, onCommit, onCancel, }: CellPersonProps) { const allowMultiple = (property.typeOptions as PersonTypeOptions)?.allowMultiple !== false; const personIds = Array.isArray(value) ? (value as string[]) : typeof value === "string" ? [value] : []; const [search, setSearch] = useState(""); const searchRef = useRef(null); useEffect(() => { if (isEditing) { setSearch(""); requestAnimationFrame(() => searchRef.current?.focus()); } }, [isEditing]); const { data: membersData } = useWorkspaceMembersQuery({ limit: 100 }); const members = membersData?.items ?? []; const memberMap = useMemo(() => { const map = new Map(); for (const m of members) map.set(m.id, m); return map; }, [members]); const filteredMembers = search ? members.filter( (m) => m.name.toLowerCase().includes(search.toLowerCase()) || (m.email && m.email.toLowerCase().includes(search.toLowerCase())), ) : members; const { activeIndex, setActiveIndex, handleNavKey, setOptionRef } = useListKeyboardNav(filteredMembers.length, [search, isEditing]); const handleSelect = useCallback( (memberId: string) => { if (allowMultiple) { // Multi mode: toggle add/remove if (personIds.includes(memberId)) { const newIds = personIds.filter((id) => id !== memberId); onCommit(newIds.length > 0 ? newIds : null); } else { onCommit([...personIds, memberId]); } } else { // Single mode: replace or clear if (personIds.includes(memberId)) { onCommit(null); } else { onCommit(memberId); } } }, [allowMultiple, personIds, onCommit], ); const handleRemove = useCallback( (memberId: string) => { if (allowMultiple) { const newIds = personIds.filter((id) => id !== memberId); onCommit(newIds.length > 0 ? newIds : null); } else { onCommit(null); } }, [allowMultiple, personIds, onCommit], ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Escape") { e.preventDefault(); onCancel(); return; } if (handleNavKey(e)) return; if (e.key === "Enter") { if (activeIndex < 0 || activeIndex >= filteredMembers.length) return; e.preventDefault(); handleSelect(filteredMembers[activeIndex].id); return; } if (e.key === "Backspace" && search === "" && personIds.length > 0) { e.preventDefault(); handleRemove(personIds[personIds.length - 1]); } }, [onCancel, handleNavKey, activeIndex, filteredMembers, handleSelect, search, personIds, handleRemove], ); const selectedSet = new Set(personIds); if (isEditing) { return (
{/* Tag input area */}
{personIds.map((id) => { const member = memberMap.get(id); const name = member?.name ?? id.substring(0, 8); return ( {name} ); })} setSearch(e.currentTarget.value)} onKeyDown={handleKeyDown} />
{/* Dropdown */}
{allowMultiple && (
Select as many as you like
)}
{filteredMembers.map((member, idx) => { const isSelected = selectedSet.has(member.id); return (
setActiveIndex(idx)} onMouseDown={(e) => { // Keep focus on the search input so click doesn't blur + close popover. e.preventDefault(); }} onClick={() => handleSelect(member.id)} > {member.name}
); })} {filteredMembers.length === 0 && (
No members found
)}
); } if (personIds.length === 0) { return ; } return ; } function PersonReadList({ personIds, memberMap, }: { personIds: string[]; memberMap: Map< string, { id: string; name: string; email?: string; avatarUrl?: string } >; }) { return (
{personIds.map((id) => { const member = memberMap.get(id); const name = member?.name ?? id.substring(0, 8); return (
{name}
); })}
); }