import { X } from "lucide-react"; import type { ChoicesProps, InputProps } from "ra-core"; import { FieldTitle, useChoices, useChoicesContext, useGetRecordRepresentation, useInput, useTranslate, } from "ra-core"; import type { ComponentProps, ReactElement } from "react"; import { useCallback, useEffect } from "react"; import { FormError, FormField, FormLabel } from "@/components/admin/form"; import { InputHelperText } from "@/components/admin/input-helper-text"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import type { SupportCreateSuggestionOptions } from "@/hooks/useSupportCreateSuggestion"; import { useSupportCreateSuggestion } from "@/hooks/useSupportCreateSuggestion"; import { cn } from "@/lib/utils"; /** * Dropdown select input for choosing a single value from a list of options. * * Use `` for fields with many possible values (5+) like categories, statuses, or * countries. Supports creating new options on the fly with the `create` or `onCreate` props. * Wrap in `` to select from related resources. * * @see {@link https://marmelab.com/shadcn-admin-kit/docs/selectinput/ SelectInput documentation} * @see {@link https://ui.shadcn.com/docs/components/select Select documentation} * * @example * import { Edit, SimpleForm, TextInput, SelectInput } from '@/components/admin'; * * const PostEdit = () => ( * * * * * * * ); */ export const SelectInput = (props: SelectInputProps) => { const { choices: choicesProp, isLoading: isLoadingProp, isFetching: isFetchingProp, isPending: isPendingProp, resource: resourceProp, source: sourceProp, optionText, optionValue, disableValue = "disabled", translateChoice, createValue, createHintValue, alwaysOn, defaultValue, format, label, helperText, name, onBlur, onChange, parse, validate, readOnly, disabled, className, emptyText = "", emptyValue = "", filter: _filter, create, createLabel, onCreate, ...rest } = props; const translate = useTranslate(); useEffect(() => { if (emptyValue == null) { throw new Error( `emptyValue being set to null or undefined is not supported. Use parse to turn the empty string into null.`, ); } }, [emptyValue]); const { allChoices, isPending, error: fetchError, source, resource, isFromReference, } = useChoicesContext({ choices: choicesProp, isLoading: isLoadingProp, isFetching: isFetchingProp, isPending: isPendingProp, resource: resourceProp, source: sourceProp, }); if (source === undefined) { throw new Error( `If you're not wrapping the SelectInput inside a ReferenceInput, you must provide the source prop`, ); } if (!isPending && !fetchError && allChoices === undefined) { throw new Error( `If you're not wrapping the SelectInput inside a ReferenceInput, you must provide the choices prop`, ); } const getRecordRepresentation = useGetRecordRepresentation(resource); const { getChoiceText, getChoiceValue, getDisableValue } = useChoices({ optionText: optionText ?? (isFromReference ? getRecordRepresentation : undefined), optionValue, disableValue, translateChoice: translateChoice ?? !isFromReference, createValue, createHintValue, }); const { id, field, isRequired } = useInput({ alwaysOn, defaultValue, format, label, helperText, name, onBlur, onChange, parse, resource, source, validate, readOnly, disabled, }); const renderEmptyItemOption = useCallback(() => { return typeof emptyText === "string" ? emptyText === "" ? " " // em space, forces the display of an empty line of normal height : translate(emptyText, { _: emptyText }) : emptyText; }, [emptyText, translate]); const renderMenuItemOption = useCallback( (choice: any) => getChoiceText(choice), [getChoiceText], ); const handleChange = useCallback( async (value: string) => { if (value === emptyValue) { field.onChange(emptyValue); } else { // Find the choice by value and pass it to field.onChange const choice = allChoices?.find( (choice) => getChoiceValue(choice) === value, ); field.onChange(choice ? getChoiceValue(choice) : value); } }, [field, getChoiceValue, emptyValue, allChoices], ); const { getCreateItem, handleChange: handleChangeWithCreateSupport, createElement, } = useSupportCreateSuggestion({ create, createLabel, createValue, createHintValue, onCreate, handleChange, optionText, }); if (isPending) { return ( {label !== "" && label !== false && ( )}
); } const createItem = create || onCreate ? getCreateItem() : null; let finalChoices = fetchError ? [] : allChoices; if (create || onCreate) { finalChoices = [...finalChoices, createItem]; } // Handle reset functionality const handleReset = (e: React.MouseEvent) => { e.stopPropagation(); field.onChange(emptyValue); }; return ( <> {label !== "" && label !== false && ( )}
{createElement} ); }; export type SelectInputProps = ChoicesProps & // Source is optional as SelectInput can be used inside a ReferenceInput that already defines the source Partial & Omit & { emptyText?: string | ReactElement; emptyValue?: any; onChange?: (value: string) => void; } & Omit, "id" | "name" | "children">;