import type { ButtonProps } from '@/components/ui/button'; import { Button } from '@/components/ui/button'; import { Command, CommandEmpty, CommandInput, CommandItem, } from '@/components/ui/command'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { useNumber } from '@/hooks/use-numer-formatter'; import type { RouterOutputs } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { PopoverPortal } from '@radix-ui/react-popover'; import { CheckIcon, ChevronsUpDown, GanttChartIcon } from 'lucide-react'; import VirtualList from 'rc-virtual-list'; import * as React from 'react'; import { EventIcon } from '../events/event-icon'; /** * Type-safe ComboboxEvents component that supports both single and multiple selection. * * @example * // Single selection mode (default) * * items={events} * value={selectedEvent} * onChange={(event: string) => setSelectedEvent(event)} * placeholder="Select an event" * /> * * @example * // Multiple selection mode * * items={events} * value={selectedEvents} * onChange={(events: string[]) => setSelectedEvents(events)} * placeholder="Select events" * multiple={true} * /> */ export interface ComboboxProps { placeholder: string; items: RouterOutputs['chart']['events']; value: TMultiple extends true ? T[] : T | null | undefined; onChange: TMultiple extends true ? (value: T[]) => void : (value: T) => void; className?: string; searchable?: boolean; size?: ButtonProps['size']; label?: string; align?: 'start' | 'end' | 'center'; portal?: boolean; error?: string; disabled?: boolean; multiple?: TMultiple; maxDisplayItems?: number; } export function ComboboxEvents< T extends string, TMultiple extends boolean = false, >({ placeholder, value, onChange, className, searchable, size, align = 'start', portal, error, disabled, items, multiple = false as TMultiple, maxDisplayItems = 2, }: ComboboxProps) { const number = useNumber(); const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(''); const selectedValues = React.useMemo((): T[] => { if (multiple) { return Array.isArray(value) ? (value as T[]) : value ? [value as T] : []; } return value ? [value as T] : []; }, [value, multiple]); function find(value: string) { return items.find( (item) => item.name.toLowerCase() === value.toLowerCase(), ); } const current = selectedValues.length > 0 && selectedValues[0] ? find(selectedValues[0]) : null; const handleSelection = (selectedValue: string) => { if (multiple) { const currentValues = selectedValues; const newValues = currentValues.includes(selectedValue as T) ? currentValues.filter((v) => v !== selectedValue) : [...currentValues, selectedValue as T]; onChange(newValues as any); } else { onChange(selectedValue as any); setOpen(false); } }; const renderTriggerContent = () => { if (selectedValues.length === 0) { return placeholder; } const firstValue = selectedValues[0]; const item = firstValue ? find(firstValue) : null; let label = item?.name || firstValue; if (multiple && selectedValues.length > 1) { label += ` +${selectedValues.length - 1}`; } return label; }; return ( {searchable === true && ( )} Nothing selected { if (search === '') return true; return item.name.toLowerCase().includes(search.toLowerCase()); })} itemHeight={32} itemKey="value" className="w-[33em] max-sm:max-w-[100vw]" > {(item) => { return ( { handleSelection(item.name); }} > {selectedValues.includes(item.name as T) ? ( ) : ( )} {item.name === '*' ? 'Any events' : item.name} {number.short(item.count)} ); }} ); }