import type {HTMLProps, ReactNode} from 'react' import {useMemo, useState} from 'react' import {Label} from '@/components/ui/label' import {Input} from '@/components/ui/input' import {Checkbox} from '@/components/ui/checkbox' import {Controller, useFormContext, useWatch} from 'react-hook-form' import FormError from '@/components/form/formError' interface FormMultiselectProps<T> extends HTMLProps<HTMLInputElement> { options: T[] filterPredicate?: (option: T, filter: string) => boolean valueExtractor: (option: T) => string labelExtractor: (option: T) => string descriptionExtractor?: (option: T) => string name: string label?: string } function FormMultiselect<T>({ options, filterPredicate, valueExtractor, labelExtractor, descriptionExtractor, name, label, }: FormMultiselectProps<T>) { const [filter, setFilter] = useState<string>('') const form = useFormContext() const filteredOptions = useMemo( () => (filterPredicate ? options.filter(t => filterPredicate(t, filter)) : options), [options, filter, filterPredicate], ) const checkboxIds = (useWatch({control: form.control, name: name}) as string[]) || [] return ( <div className="col-span-2 space-y-2"> <Label>{label}</Label> <div className="space-y-3"> {filterPredicate && ( <Input placeholder="Filter options..." value={filter} onChange={e => setFilter(e.target.value)} /> )} <div className="border rounded-lg p-4 space-y-3 max-h-48 overflow-y-auto"> {filteredOptions.length === 0 ? ( <p className="text-sm text-muted-foreground"> {filter ? 'No options match your search' : 'No options available'} </p> ) : ( <Controller name={name} render={({field}) => { const output: ReactNode[] = [] for (const option of filteredOptions) { const value = valueExtractor(option) const isChecked = checkboxIds.includes(value) output.push( <div key={valueExtractor(option)} className="flex flex-col items-start space-x-3"> <div className="flex flex-row gap-4 items-center"> <Checkbox className="mt-1" id={`option-${value}`} checked={isChecked} name={name} value={value} defaultChecked={isChecked} onCheckedChange={() => field.onChange(isChecked ? checkboxIds.filter(x => x != value) : [...checkboxIds, value]) } /> <label htmlFor={`option-${value}`} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"> {labelExtractor(option)} </label> </div> {descriptionExtractor && ( <p className="text-xs text-muted-foreground mt-1 ml-8">{descriptionExtractor(option)}</p> )} </div>, ) } return <>{output}</> }} /> )} </div> <FormError path={name} /> </div> </div> ) } export default FormMultiselect