import * as React from "react"; import type { ChangeEvent, ReactElement } from "react"; import { createContext, isValidElement, useContext, useRef, useState, } from "react"; import type { Identifier, OptionText } from "ra-core"; import { useTranslate } from "ra-core"; import set from "lodash/set"; /** * This hook provides support for suggestion creation in inputs which have choices. * * @deprecated Use `useSupportCreateSuggestion` from "ra-core" when available. * * @param options The hook option * @param {ReactElement} options.create A react element which will be rendered when users choose to create a new choice. This component must call the `useCreateSuggestionContext` hook which provides `onCancel`, `onCreate` and `filter`. See the examples. * @param {React.ReactNode|string} options.createLabel Optional. The label for the choice item allowing users to create a new choice. Can be a translation key. Defaults to `ra.action.create`. * @param {React.ReactNode|string} options.createItemLabel Optional. The label for the choice item allowing users to create a new choice when they already entered a filter. Can be a translation key. The function and ttranslation will receive an `item` parameter. Providing this option will turn the create label when there is no filter to be a hint (i.e. a disabled item). * @param {any} options.createValue Optional. The value for the choice item allowing users to create a new choice. Defaults to `@@ra-create`. * @param {any} options.createHintValue Optional. The value for the (disabled) item hinting users on how to create a new choice. Defaults to `@@ra-create-hint`. * @param {String} options.filter Optional. The filter users may have already entered. Useful for autocomplete inputs for example. * @param {OnCreateHandler} options.onCreate Optional. A function which will be called when users choose to create a new choice, if the `create` option wasn't provided. * @param {Function} options.handleChange A function to pass to the input. Receives the same parameter as the original event handler and an additional newItem parameter if a new item was create. * * @returns {UseSupportCreateValue} An object with the following properties: * - getCreateItem: a function which will return the label of the choice for create a new choice. * - handleChange: a function which should be called when the input value changes. It will call the `onCreate` function if the value is the createValue. * - createElement: a React element to render after the input. It will be rendered when users choose to create a new choice. It renders null otherwise. * - getOptionDisabled: a function which should be passed to the input to disable the create choice when the filter is empty (to make it a hint). */ export const useSupportCreateSuggestion = ( options: SupportCreateSuggestionOptions, ): UseSupportCreateValue => { const { create, createLabel = "ra.action.create", createItemLabel, createValue = "@@ra-create", createHintValue = "@@ra-create-hint", optionText = "name", filter, handleChange, onCreate, } = options; const translate = useTranslate(); const [renderOnCreate, setRenderOnCreate] = useState(false); const filterRef = useRef(filter); return { createId: createValue, createHintId: createHintValue, getCreateItem: (filter?: string) => { filterRef.current = filter; return set( { id: createItemLabel && !filter ? createHintValue : createValue, }, typeof optionText === "string" ? optionText : "name", filter && createItemLabel ? typeof createItemLabel === "string" ? translate(createItemLabel, { item: filter, _: createItemLabel, }) : createItemLabel(filter) : typeof createLabel === "string" ? translate(createLabel, { _: createLabel }) : createLabel, ); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any handleChange: async (eventOrValue: MouseEvent | any) => { const value = eventOrValue?.target?.value || eventOrValue; const finalValue = Array.isArray(value) ? [...value].pop() : value; if (finalValue?.id === createValue || finalValue === createValue) { if (!isValidElement(create)) { if (!onCreate) { // this should never happen because the createValue is only added if a create function is provided // @see AutocompleteInput:filterOptions throw new Error( "To create a new option, you must pass an onCreate function or a create element.", ); } const newSuggestion = await onCreate(filter); if (newSuggestion) { handleChange(newSuggestion); return; } } else { setRenderOnCreate(true); return; } } handleChange(eventOrValue); }, createElement: renderOnCreate && isValidElement(create) ? ( setRenderOnCreate(false), onCreate: (item) => { setRenderOnCreate(false); handleChange(item as T); }, }} > {create} ) : null, getOptionDisabled: (option) => // eslint-disable-next-line @typescript-eslint/no-explicit-any (option as any)?.id === createHintValue || option === createHintValue, }; }; /** * @deprecated Use `SupportCreateSuggestionOptions` from "ra-core" when available. */ export interface SupportCreateSuggestionOptions { create?: ReactElement; createValue?: string; createHintValue?: string; createLabel?: React.ReactNode; createItemLabel?: string | ((filter: string) => React.ReactNode); filter?: string; handleChange: (value: T) => void; onCreate?: OnCreateHandler; optionText?: OptionText; } /** * @deprecated Use `UseSupportCreateValue` from "ra-core" when available. */ export interface UseSupportCreateValue { createId: string; createHintId: string; getCreateItem: (filterValue?: string) => { id: Identifier; // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; }; handleChange: (eventOrValue: ChangeEvent | T) => Promise; createElement: ReactElement | null; getOptionDisabled: (option: T) => boolean; } /** * @deprecated Use `CreateSuggestionContext` from "ra-core" when available. */ const CreateSuggestionContext = createContext< CreateSuggestionContextValue | undefined >(undefined); /** * @deprecated Use `CreateSuggestionContextValue` from "ra-core" when available. */ interface CreateSuggestionContextValue { filter?: string; onCreate: (choice: T) => void; onCancel: () => void; } /** * @deprecated Use `useCreateSuggestionContext` from "ra-core" when available. */ export const useCreateSuggestionContext = () => { const context = useContext(CreateSuggestionContext); if (!context) { throw new Error( "useCreateSuggestionContext must be used inside a CreateSuggestionContext.Provider", ); } return context; }; /** * @deprecated Use `OnCreateHandler` from "ra-core" when available. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type OnCreateHandler = (filter?: string) => any | Promise;