import React from 'react'; import { useController, type FieldPath, type FieldValues, type UseControllerProps } from 'react-hook-form'; import type { SelectProps, MultiSelectProps } from '@mantine/core'; import { AsyncSelect } from '../custom/selects/AsyncSelect'; import type { AsyncSelectBaseProps, LoadOptionsFn } from '../custom/selects/types'; import { useTranslatedError } from '../useTranslatedError'; // --------------------------------------------------------------------------- // FieldAsyncSelect — RHF-connected Async Select // --------------------------------------------------------------------------- // // Thin RHF wrapper around the standalone AsyncSelect engine. // Adds useController binding + i18n error translation. // // Data Mapping Contract: // Single: RHF stores T | null → maps to Mantine string | null // Multi: RHF stores T[] → maps to Mantine string[] // // Inversion of Control: accepts `loadOptions` callback instead of // hardcoded API endpoint. The component is transport-agnostic. // --------------------------------------------------------------------------- /** Mantine props we manage ourselves */ type ManagedSelectProps = | 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect'; type ManagedMultiSelectProps = | 'data' | 'value' | 'defaultValue' | 'onChange' | 'onBlur' | 'error' | 'filter' | 'name' | 'onSelect'; /** Async-specific props (IoC pattern) */ interface AsyncExtraProps { /** Async callback to load options — (search, page, prevOptions) => Promise */ loadOptions: LoadOptionsFn; /** Pre-loaded objects for edit forms (injected into dropdown regardless of fetch state) */ defaultOptions?: T[]; /** Search debounce delay in ms (default: 300) */ debounceMs?: number; } /** Single-select async RHF props */ export type FieldAsyncSelectSingleProps< T extends Record, TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = AsyncSelectBaseProps & AsyncExtraProps & UseControllerProps & Omit & { multiple?: false; }; /** Multi-select async RHF props */ export type FieldAsyncSelectMultiProps< T extends Record, TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = AsyncSelectBaseProps & AsyncExtraProps & UseControllerProps & Omit & { multiple: true; }; export type FieldAsyncSelectProps< T extends Record, TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = FieldAsyncSelectSingleProps | FieldAsyncSelectMultiProps; // --------------------------------------------------------------------------- // Component Implementation // --------------------------------------------------------------------------- function FieldAsyncSelectInner< T extends Record, TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, >(props: FieldAsyncSelectProps) { const { // RHF controller props name, control, rules, shouldUnregister, defaultValue, disabled, // Object/Async engine props valueKey, labelKey, renderLabel, multiple, filterOption, onSelect: onSelectCallback, loadOptions, defaultOptions, debounceMs, // Remaining Mantine props ...mantineProps } = props; const { field, fieldState: { error }, } = useController({ name, control, rules, shouldUnregister, defaultValue, disabled, }); const translatedError = useTranslatedError(error?.message); const handleChange = (value: any) => { field.onChange(value); onSelectCallback?.(value); }; const engineProps = { valueKey, labelKey, renderLabel, filterOption, loadOptions, defaultOptions, debounceMs, onBlur: field.onBlur, error: translatedError, disabled: field.disabled, }; if (multiple) { return ( multiple {...engineProps} value={field.value ?? []} onChange={handleChange} {...(mantineProps as any)} /> ); } return ( {...engineProps} value={field.value ?? null} onChange={handleChange} {...(mantineProps as any)} /> ); } export const FieldAsyncSelect = React.memo(FieldAsyncSelectInner) as typeof FieldAsyncSelectInner; (FieldAsyncSelect as any).displayName = 'FieldAsyncSelect';