import { useState, useRef, useEffect, useCallback } from "react"; import { Popover } from "@mantine/core"; import { IconX } from "@tabler/icons-react"; import clsx from "clsx"; import { IBaseProperty, PersonTypeOptions, } from "@/ee/base/types/base.types"; import { useReferenceStore, useHydrateUsers, } from "@/ee/base/reference/reference-store"; import { CustomAvatar } from "@/components/ui/custom-avatar"; import { PersonReadList } from "@/ee/base/components/cells/person-read-list"; import cellClasses from "@/ee/base/styles/cells.module.css"; import { useListKeyboardNav } from "@/ee/base/hooks/use-list-keyboard-nav"; import { usePersonSearch } from "@/ee/base/hooks/use-person-search"; type CellPersonProps = { value: unknown; property: IBaseProperty; rowId: string; isEditing: boolean; onCommit: (value: unknown) => void; onValueChange: (value: unknown) => void; onCancel: () => void; }; export function CellPerson({ value, property, isEditing, onCommit, onValueChange, onCancel, }: CellPersonProps) { const allowMultiple = (property.typeOptions as PersonTypeOptions)?.allowMultiple === true; const personIds = Array.isArray(value) ? (value as string[]) : typeof value === "string" ? [value] : []; const selectedSet = new Set(personIds); const [search, setSearch] = useState(""); const searchRef = useRef(null); useEffect(() => { if (isEditing) { setSearch(""); requestAnimationFrame(() => searchRef.current?.focus()); } }, [isEditing]); const store = useReferenceStore(property.pageId); const hydrateUsers = useHydrateUsers(property.pageId); const suggestions = usePersonSearch(search, isEditing); // In multi mode omit already-selected from the list (they appear as tags above). // Single mode keeps the selected row visible so it can be deselected. const filteredMembers = allowMultiple ? suggestions.filter((m) => !selectedSet.has(m.id)) : suggestions; const { activeIndex, setActiveIndex, handleNavKey, setOptionRef } = useListKeyboardNav(filteredMembers.length, [search, isEditing]); const handleSelect = useCallback( (memberId: string) => { const picked = suggestions.find((s) => s.id === memberId); if (picked) hydrateUsers([ { id: picked.id, name: picked.name, avatarUrl: picked.avatarUrl }, ]); if (allowMultiple) { if (personIds.includes(memberId)) { const newIds = personIds.filter((id) => id !== memberId); onValueChange(newIds.length > 0 ? newIds : null); } else { onValueChange([...personIds, memberId]); } } else { if (personIds.includes(memberId)) { onCommit(null); } else { onCommit(memberId); } } }, [suggestions, hydrateUsers, allowMultiple, personIds, onCommit, onValueChange], ); const handleRemove = useCallback( (memberId: string) => { if (allowMultiple) { const newIds = personIds.filter((id) => id !== memberId); onValueChange(newIds.length > 0 ? newIds : null); } else { onCommit(null); } }, [allowMultiple, personIds, onCommit, onValueChange], ); 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], ); if (isEditing) { return ( { if (!o) onCancel(); }} onClose={onCancel} position="bottom-start" width={300} trapFocus closeOnClickOutside closeOnEscape >
{personIds.map((id) => { const member = store.users[id]; const name = member?.name ?? id.substring(0, 8); return ( {name} ); })} setSearch(e.currentTarget.value)} onKeyDown={handleKeyDown} data-autofocus />
{allowMultiple && (
Select as many as you like
)}
{filteredMembers.map((member, idx) => { const isSelected = selectedSet.has(member.id); return (
setActiveIndex(idx)} onClick={() => handleSelect(member.id)} >
{member.name ?? ""} {member.email && ( {member.email} )}
); })} {filteredMembers.length === 0 && (
No members found
)}
); } if (personIds.length === 0) { return ; } return ; }