"use client"; import { cn } from "@/utils/cn"; import { type InputProps, InputState, useInput } from "@/utils/input"; import Link from "next/link"; import { useCallback, useEffect, useRef, useState } from "react"; import { HiChevronDown, HiExclamationCircle, HiX } from "react-icons/hi"; import { Spinner } from "../spinner"; import { Badge } from "../ui/badge"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from "../ui/dropdown-menu"; export interface MultiSelectItem { icon?: React.ReactNode; name: string; value: T; error?: string; color?: number; } interface Props { link?: string; badge?: string; items: MultiSelectItem[]; max?: number; } export function InputMultiSelect({ className, label, link, badge, description, disabled, items = [], max = Infinity, endpoint, k, defaultState, transform, onSave }: InputProps & Props) { const { value, state, error, isDirty, update, save } = useInput({ endpoint, k, defaultState, transform, onSave, manual: true, isEqual: (a, b) => { if (a === b) return true; if (a.length !== b.length) return false; const setA = new Set(a); return b.every((v) => setA.has(v)); } }); const [open, setOpen] = useState(false); const debounceRef = useRef(null); const valueRef = useRef(value); // Keep valueRef in sync useEffect(() => { valueRef.current = value; }, [value]); // Debounce save after 5 seconds of inactivity while menu is open const debouncedSave = useCallback(() => { if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { void save(); }, 5_000); }, [save]); // Clear debounce on unmount useEffect(() => { return () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, []); const selectedItems = items.filter((i) => value.includes(i.value)); const handleToggle = (item: MultiSelectItem) => { if (item.error) return; const isSelected = value.includes(item.value); if (isSelected) { update(value.filter((v) => v !== item.value)); } else if (value.length < max) { update([...value, item.value]); } // Trigger debounced save debouncedSave(); }; const handleRemove = (e: React.PointerEvent, itemValue: T) => { // Prevent the dropdown from opening e.preventDefault(); e.stopPropagation(); const newValue = value.filter((v) => v !== itemValue); update(newValue); void save(newValue); }; const handleOpenChange = (isOpen: boolean) => { setOpen(isOpen); if (!isOpen) { // Menu closed - save and clear any pending debounce if (debounceRef.current) { clearTimeout(debounceRef.current); debounceRef.current = null; } if (isDirty) { void save(); } } }; const isDisabled = state === InputState.Loading || disabled; const hasErrors = selectedItems.some((v) => Boolean(v.error)); return (
{label} {badge && ( {badge} )} {state === InputState.Loading && ( )}
{!selectedItems.length && Select..} {selectedItems.map((item) => ( ))}
{hasErrors && (
{selectedItems.find((v) => v.error)?.error}
)} {max !== Infinity && ( {value.length}/{max} )}
{items.map((item) => ( handleToggle(item)} onSelect={(e) => e.preventDefault()} // Prevent menu from closing disabled={Boolean(item.error) || (!value.includes(item.value) && value.length >= max)} > {item.icon && {item.icon}} {item.name} {item.error && (
{item.error}
)}
))}
{description && (
{description} {link && Learn more}
)} {error && (
{error}
)}
); }