import * as React from "react"; import { Archive, ChevronDown, Component, Search } from "lucide-react"; import { cn } from "@/src/utils/tailwind"; import { Badge } from "@/src/components/ui/badge"; import { Button } from "@/src/components/ui/button"; import { Input } from "@/src/components/ui/input"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/src/components/ui/dropdown-menu"; import { Separator } from "@/src/components/ui/separator"; type MultiSelectOptions = { value: string; key?: string; count?: number; disabled?: boolean; isArchived?: boolean; }; type MultiSelectGroup = { label: string; options: MultiSelectOptions[]; }; type MultiSelectKeyValuesProps< T extends { key: string; value: string } | string, > = { values: T[]; onValueChange: ( values: T[], changedValue?: string, selectedKeys?: Set, ) => void; options: MultiSelectOptions[] | readonly MultiSelectOptions[]; title?: string; placeholder?: string; groupedOptions?: MultiSelectGroup[]; className?: string; disabled?: boolean; items?: string; align?: "center" | "end" | "start"; controlButtons?: React.ReactNode; hideClearButton?: boolean; iconLeft?: React.ReactNode; iconRight?: React.ReactNode; variant?: "outline" | "secondary" | "ghost"; showSelectedValueStrings?: boolean; }; export function MultiSelectKeyValues< T extends { key: string; value: string } | string, >({ title = "Select", placeholder, values, onValueChange, options, groupedOptions, className, disabled, items = "items", align = "center", controlButtons, hideClearButton = false, iconLeft, iconRight, variant = "secondary", showSelectedValueStrings = true, }: MultiSelectKeyValuesProps) { const [isOpen, setIsOpen] = React.useState(false); const [searchQuery, setSearchQuery] = React.useState(""); const selectedValueKeys = new Set( values.map((value) => (typeof value === "string" ? value : value.key)), ); const showClearItems = selectedValueKeys.size > 0 && !hideClearButton; function formatFilterValues(): T[] { if (values.length > 0 && typeof values[0] === "string") { return Array.from(selectedValueKeys) as T[]; } const allOptions = groupedOptions ? groupedOptions.flatMap((group) => group.options) : options || []; return allOptions .filter((option) => !!option.key && selectedValueKeys.has(option.key)) .map((option) => ({ key: option.key as string, value: option.value, })) as T[]; } const filterOptions = (options: MultiSelectOptions[]) => { if (!searchQuery.trim()) return options; const searchLower = searchQuery.toLowerCase().trim(); return options.filter((option) => { const valueLower = option.value.toLowerCase(); const keyLower = option.key?.toLowerCase() || ""; return valueLower.includes(searchLower) || keyLower.includes(searchLower); }); }; const renderOption = (option: MultiSelectOptions) => { const isSelected = selectedValueKeys.has(option.key ?? option.value); return ( e.preventDefault()} onCheckedChange={() => { const value = option.key ?? option.value; if (isSelected) { selectedValueKeys.delete(value); } else { selectedValueKeys.add(value); } const filterValues = formatFilterValues(); onValueChange( filterValues.length ? filterValues : [], value, selectedValueKeys, ); }} disabled={option.disabled} className="group" > {option.value} {option.isArchived && ( )} {option.count !== undefined && ( {option.count} )} ); }; const inputRef = React.useRef(null); const handleInputClick = (e: React.MouseEvent) => { e.stopPropagation(); inputRef.current?.focus(); }; return ( { setIsOpen(open); if (!open) { setSearchQuery(""); } }} > setIsOpen(false)} >
setSearchQuery(e.target.value)} className="h-6 border-0 bg-transparent p-0 text-sm focus-visible:ring-0" autoComplete="off" autoCorrect="off" spellCheck={false} onKeyDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()} />
{options && options.length > 0 && filterOptions(Array.from(options)).map(renderOption)} {groupedOptions?.map((group) => { const filteredGroupOptions = filterOptions(group.options); if (filteredGroupOptions.length === 0) return null; return ( {group.label} {filteredGroupOptions.map(renderOption)} ); })} {searchQuery && (!options || filterOptions(Array.from(options)).length === 0) && (!groupedOptions || !groupedOptions.some( (group) => filterOptions(group.options).length > 0, )) && (
No results found.
)} {showClearItems && !searchQuery && ( <> { e.preventDefault(); onValueChange([]); }} > Clear {items} )} {controlButtons && ( <> {controlButtons} )}
); }