import * as React from "react"; import { useCallback } from "react"; import { Check, ChevronsUpDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { FormControl, FormError, FormField, FormLabel, } from "@/components/admin/form"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; 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 { SupportCreateSuggestionOptions, useSupportCreateSuggestion, } from "@/hooks/useSupportCreateSuggestion"; /** * Form control that lets users choose a value from a list using a dropdown with autocompletion. * * This input allows editing scalar values with a searchable dropdown interface. It supports creating * new choices on the fly and works seamlessly inside ReferenceInput for editing foreign key relationships. * * @see {@link https://marmelab.com/shadcn-admin-kit/docs/autocompleteinput/ AutocompleteInput documentation} * * @example * import { * Create, * SimpleForm, * AutocompleteInput, * ReferenceInput, * } from '@/components/admin'; * * const PostCreate = () => ( * * * * * * * * * ); */ export const AutocompleteInput = ( props: Omit & 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, create, createValue, createLabel, createHintValue, createItemLabel, onCreate, optionText, } = 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 [filterValue, setFilterValue] = React.useState(""); const [open, setOpen] = React.useState(false); const selectedChoice = allChoices.find( (choice) => getChoiceValue(choice) === field.value, ); const getInputText = useCallback( (selectedChoice: any) => { if (typeof inputText === "function") { return inputText(selectedChoice); } if (inputText !== undefined) { return inputText; } return getChoiceText(selectedChoice); }, [inputText, getChoiceText], ); const handleOpenChange = useEvent((isOpen: boolean) => { setOpen(isOpen); // Reset the filter when the popover is closed if (!isOpen) { setFilters(filterToQuery("")); } }); const handleChange = useCallback( (choice: any) => { if (field.value === getChoiceValue(choice) && !isRequired) { field.onChange(""); setFilterValue(""); if (isFromReference) { setFilters(filterToQuery("")); } setOpen(false); return; } field.onChange(getChoiceValue(choice)); setOpen(false); }, [ field.value, field.onChange, getChoiceValue, isRequired, setFilterValue, isFromReference, setFilters, filterToQuery, setOpen, ], ); const { getCreateItem, handleChange: handleChangeWithCreateSupport, createElement, getOptionDisabled, } = useSupportCreateSuggestion({ create, createLabel, createValue, createHintValue, createItemLabel, onCreate, handleChange, optionText, filter: filterValue, }); const createItem = (create || onCreate) && (filterValue !== "" || createLabel) ? getCreateItem(filterValue) : null; let finalChoices = allChoices; if (createItem) { finalChoices = [...finalChoices, createItem]; } return ( <> {props.label !== false && ( )} {/* We handle the filtering ourselves */} { 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)); } }} /> No matching item found. {finalChoices.map((choice) => { const isCreateItem = !!createItem && choice?.id === createItem.id; const disabled = getOptionDisabled(choice); return ( handleChangeWithCreateSupport(choice)} disabled={disabled} > {getChoiceText(isCreateItem ? createItem : choice)} ); })} {createElement} ); }; const DefaultFilterToQuery = (searchText: string) => ({ q: searchText });