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"
/>