import * as React from "react"; import { X } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Command, CommandGroup, CommandItem, CommandList, } from "@/components/ui/command"; import { FormControl, FormError, FormField, FormLabel, } from "@/components/admin/form"; import { Command as CommandPrimitive } from "cmdk"; import type { ChoicesProps, InputProps } from "ra-core"; import { useChoices, useChoicesContext, useGetRecordRepresentation, useInput, useTranslate, FieldTitle, useEvent, } from "ra-core"; import { InputHelperText } from "./input-helper-text"; import { useCallback } from "react"; /** * Form control that lets users choose multiple values from a list using a dropdown with autocompletion. * * This input allows editing array values with a searchable dropdown interface and displays selected items as removable badges. * Works seamlessly inside ReferenceArrayInput for editing many-to-many relationships. * * @see {@link https://marmelab.com/shadcn-admin-kit/docs/autocompletearrayinput/ AutocompleteArrayInput documentation} * * @example * import { * Create, * SimpleForm, * AutocompleteArrayInput, * ReferenceArrayInput, * } from '@/components/admin'; * * const PostCreate = () => ( * * * * * * * * * ); */ export const AutocompleteArrayInput = ( props: Omit & Partial> & ChoicesProps & { className?: string; disableValue?: string; filterToQuery?: (searchText: string) => any; translateChoice?: boolean; placeholder?: string; inputText?: | React.ReactNode | ((option: any | undefined) => React.ReactNode); }, ) => { const { filterToQuery = DefaultFilterToQuery, inputText } = props; const { allChoices = [], source, resource, isFromReference, setFilters, } = useChoicesContext(props); const { id, field, isRequired } = useInput({ ...props, source }); const translate = useTranslate(); const { placeholder = translate("ra.action.search", { _: "Search..." }) } = props; const getRecordRepresentation = useGetRecordRepresentation(resource); const { getChoiceText, getChoiceValue } = useChoices({ optionText: props.optionText ?? (isFromReference ? getRecordRepresentation : "name"), optionValue: props.optionValue ?? "id", disableValue: props.disableValue, translateChoice: props.translateChoice ?? !isFromReference, }); const inputRef = React.useRef(null); const [open, setOpen] = React.useState(false); const handleUnselect = useEvent((choice: any) => { field.onChange( field.value.filter((v: any) => v !== getChoiceValue(choice)), ); }); const handleKeyDown = useEvent((e: React.KeyboardEvent) => { const input = inputRef.current; if (input) { if (e.key === "Delete" || e.key === "Backspace") { if (input.value === "") { field.onChange(field.value.slice(0, -1)); } } // This is not a default behavior of the field if (e.key === "Escape") { input.blur(); } } }); const availableChoices = allChoices.filter( (choice) => !field.value.includes(getChoiceValue(choice)), ); const selectedChoices = allChoices.filter((choice) => field.value.includes(getChoiceValue(choice)), ); const [filterValue, setFilterValue] = React.useState(""); const getInputText = useCallback( (selectedChoice: any) => { if (typeof inputText === "function") { return inputText(selectedChoice); } if (inputText !== undefined) { return inputText; } return getChoiceText(selectedChoice); }, [inputText, getChoiceText], ); return ( {props.label !== false && ( )}
{selectedChoices.map((choice) => ( {getInputText(choice)} ))} {/* Avoid having the "Search" Icon by not using CommandInput */} { setFilterValue(filter); // We don't want the ChoicesContext to filter the choices if the input // is not from a reference as it would also filter out the selected values if (isFromReference) { setFilters(filterToQuery(filter), undefined, true); } }} onBlur={() => setOpen(false)} onFocus={() => setOpen(true)} placeholder={placeholder} className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" />
{open && availableChoices.length > 0 ? (
{availableChoices.map((choice) => { return ( { e.preventDefault(); e.stopPropagation(); }} onSelect={() => { setFilterValue(""); if (isFromReference) { setFilters(filterToQuery("")); } field.onChange([ ...field.value, getChoiceValue(choice), ]); }} className="cursor-pointer" > {getChoiceText(choice)} ); })}
) : null}
); }; const DefaultFilterToQuery = (searchText: string) => ({ q: searchText });