import { useState, useRef, useEffect, useCallback, useMemo, } from "react"; import { Popover, TextInput } from "@mantine/core"; import { IconX } from "@tabler/icons-react"; import clsx from "clsx"; import { IBaseProperty, SelectTypeOptions, Choice, } from "@/ee/base/types/base.types"; import { choiceColor } from "@/ee/base/components/cells/choice-color"; import { BadgeOverflowList } from "@/ee/base/components/cells/badge-overflow"; import { useUpdatePropertyMutation } from "@/ee/base/queries/base-property-query"; import { generateBaseChoiceId } from "@/ee/base/utils/generate-base-id"; import cellClasses from "@/ee/base/styles/cells.module.css"; import { useListKeyboardNav } from "@/ee/base/hooks/use-list-keyboard-nav"; const CHOICE_COLORS = [ "gray", "red", "pink", "grape", "violet", "indigo", "blue", "cyan", "teal", "green", "lime", "yellow", "orange", ]; type NavItem = | { kind: "choice"; choice: Choice } | { kind: "add" }; type CellMultiSelectProps = { value: unknown; property: IBaseProperty; rowId: string; isEditing: boolean; onCommit: (value: unknown) => void; onValueChange: (value: unknown) => void; onCancel: () => void; }; export function CellMultiSelect({ value, property, isEditing, onValueChange, onCancel, }: CellMultiSelectProps) { const typeOptions = property.typeOptions as SelectTypeOptions | undefined; const choices = typeOptions?.choices ?? []; const selectedIds = Array.isArray(value) ? (value as string[]) : []; const selectedSet = new Set(selectedIds); const selectedChoices = choices.filter((c) => selectedSet.has(c.id)); const [search, setSearch] = useState(""); const searchRef = useRef(null); useEffect(() => { if (isEditing) { setSearch(""); requestAnimationFrame(() => searchRef.current?.focus()); } }, [isEditing]); const filteredChoices = ( search ? choices.filter((c) => c.name.toLowerCase().includes(search.toLowerCase())) : choices ).filter((c) => !selectedSet.has(c.id)); const handleToggle = useCallback( (choice: Choice) => { const newIds = selectedSet.has(choice.id) ? selectedIds.filter((id) => id !== choice.id) : [...selectedIds, choice.id]; onValueChange(newIds); }, [selectedIds, selectedSet, onValueChange], ); const updatePropertyMutation = useUpdatePropertyMutation(); const trimmedSearch = search.trim(); const hasExactMatch = useMemo( () => trimmedSearch.length > 0 && choices.some((c) => c.name.toLowerCase() === trimmedSearch.toLowerCase()), [choices, trimmedSearch], ); const showAddOption = trimmedSearch.length > 0 && !hasExactMatch; const addOptionColor = useMemo( () => CHOICE_COLORS[choices.length % CHOICE_COLORS.length], [choices.length], ); const navItems = useMemo( () => [ ...filteredChoices.map((c) => ({ kind: "choice" as const, choice: c })), ...(showAddOption ? [{ kind: "add" as const }] : []), ], [filteredChoices, showAddOption], ); const { activeIndex, setActiveIndex, handleNavKey, setOptionRef } = useListKeyboardNav(navItems.length, [search, isEditing, showAddOption]); const handleAddOption = useCallback(() => { if (!trimmedSearch) return; const newChoice: Choice = { id: generateBaseChoiceId(), name: trimmedSearch, color: addOptionColor, }; const newChoices = [...choices, newChoice]; updatePropertyMutation.mutate({ propertyId: property.id, pageId: property.pageId, typeOptions: { ...typeOptions, choices: newChoices, choiceOrder: newChoices.map((c) => c.id), }, }); onValueChange([...selectedIds, newChoice.id]); setSearch(""); }, [trimmedSearch, addOptionColor, choices, typeOptions, property, updatePropertyMutation, selectedIds, 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 < navItems.length) { e.preventDefault(); const item = navItems[activeIndex]; if (item.kind === "choice") handleToggle(item.choice); else handleAddOption(); return; } if (showAddOption) { e.preventDefault(); handleAddOption(); } } }, [onCancel, handleNavKey, activeIndex, navItems, handleToggle, handleAddOption, showAddOption], ); if (isEditing) { const addOptionIdx = filteredChoices.length; return ( { if (!o) onCancel(); }} onClose={onCancel} position="bottom-start" width={220} trapFocus closeOnClickOutside closeOnEscape >
{selectedChoices.length > 0 && (
{selectedChoices.map((choice) => ( {choice.name} ))}
)} setSearch(e.currentTarget.value)} onKeyDown={handleKeyDown} mb={4} data-autofocus />
{filteredChoices.map((choice, idx) => (
setActiveIndex(idx)} onClick={() => handleToggle(choice)} > {choice.name}
))} {showAddOption && (
setActiveIndex(addOptionIdx)} onClick={handleAddOption} > Add option: {trimmedSearch}
)}
); } if (selectedChoices.length === 0) { return ; } return ( c.name).join(", ")} /> ); } function BadgeList({ choices, tooltipLabel, }: { choices: Choice[]; tooltipLabel?: string; }) { const chips = choices.map((choice) => ( {choice.name} )); return ( `${c.id}:${c.name}`).join("|")} tooltipLabel={tooltipLabel} /> ); }