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 });