import clsx from 'clsx' import { filter, orderBy, without } from 'lodash' import { AlertCircle, Check, ChevronDown, Plus, Search } from 'lucide-react' import { ReactNode, useEffect, useRef, useState } from 'react' import { Input, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, ScrollArea, } from 'ui' import { BadgeDisabled, BadgeSelected } from './Badges' export interface MultiSelectOption { id: string | number value: string name: string description?: string disabled: boolean } interface Props { value: string[] options: MultiSelectOption[] label?: string error?: string placeholder?: string | ReactNode searchPlaceholder?: string descriptionText?: string | ReactNode emptyMessage?: string | ReactNode disabled?: boolean allowDuplicateSelection?: boolean onChange?(x: string[]): void } /** * Copy styling from supabase/ui default.theme * input base + standard */ /** * @deprecated Use ./multi-select instead */ export default function MultiSelect({ options, value, label, error, descriptionText, placeholder, searchPlaceholder = 'Search for option', emptyMessage, disabled, allowDuplicateSelection = false, onChange = () => {}, }: Props) { const ref = useRef(null) const [open, setOpen] = useState(false) const [selected, setSelected] = useState(value || []) const [searchString, setSearchString] = useState('') const [inputWidth, setInputWidth] = useState(128) // Selected is `value` if defined, otherwise use local useState const selectedOptions = value || selected // Calculate width of the Popover useEffect(() => { setInputWidth(ref.current ? (ref.current as any).offsetWidth : inputWidth) }, [ref.current]) useEffect(() => { if (!open) setSearchString('') }, [open]) const width = `${inputWidth}px` // Order the options so disabled items are at the beginning const formattedOptions = orderBy(options, ['disabled'], ['desc']) // Options to show in Popover menu const filteredOptions = searchString.length > 0 ? filter(formattedOptions, (option) => !option.disabled && option.name.includes(searchString)) : filter(formattedOptions, { disabled: false }) const checkIfActive = (option: MultiSelectOption) => { const isOptionSelected = (selectedOptions || []).find((x) => x === option.value) return isOptionSelected !== undefined } const handleRemove = (idx: number) => { const updatedSelected = selected.filter((x, index) => index !== idx) setSelected(updatedSelected) onChange(updatedSelected) } const handleChange = (option: MultiSelectOption) => { const _selected = selectedOptions const isActive = checkIfActive(option) const updatedPayload = allowDuplicateSelection ? [..._selected.concat([option.value])] : isActive ? [...without(_selected, option.value)] : [..._selected.concat([option.value])] // Payload must always include disabled options const compulsoryOptions = options .filter((option) => option.disabled) .map((option) => option.name) const formattedPayload = allowDuplicateSelection ? updatedPayload.concat(compulsoryOptions) : [...new Set(updatedPayload.concat(compulsoryOptions))] setSelected(formattedPayload) onChange(formattedPayload) } return (
{label && }
setOpen(true)} > {selectedOptions.length === 0 && placeholder && (
{placeholder}
)} {selectedOptions.map((value, idx) => { const id = `${value}-${idx}` const option = formattedOptions.find((x) => x.value === value) const isDisabled = option?.disabled ?? false if (!option) { return <> } else if (isDisabled) { return } else { return ( allowDuplicateSelection ? handleRemove(idx) : handleChange(option) } /> ) } })}
} placeholder={searchPlaceholder} value={searchString} onChange={(e) => setSearchString(e.target.value)} /> 5 ? 'h-[225px]' : '')}> {filteredOptions.length >= 1 ? ( filteredOptions.map((option) => { const active = !allowDuplicateSelection && selectedOptions && selectedOptions.find((selected) => { return selected === option.value }) ? true : false return (
handleChange(option)} className={[ 'text-typography-body-light [[data-theme*=dark]_&]:text-typography-body-dark', 'group flex cursor-pointer items-center justify-between transition', 'space-x-1 rounded bg-transparent p-2 px-4 text-sm hover:bg-overlay-hover', `${ active ? ' [[data-theme*=dark]_&]:bg-green-600 [[data-theme*=dark]_&]:bg-opacity-25' : '' }`, ].join(' ')} >

{option.name}

{option.description !== undefined && (

{option.description}

)}
{active && ( )} {allowDuplicateSelection && (

Add value

)}
) }) ) : options.length === 0 ? (
{emptyMessage ? ( emptyMessage ) : (

No options available

)}
) : (
{emptyMessage ? ( emptyMessage ) : (

No options found

)}
)}
{descriptionText && ( {descriptionText} )} {error && {error}}
) }