163 lines
4.7 KiB
TypeScript
163 lines
4.7 KiB
TypeScript
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<T> {
|
|
/** Async callback to load options — (search, page, prevOptions) => Promise */
|
|
loadOptions: LoadOptionsFn<T>;
|
|
|
|
/** 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<string, any>,
|
|
TFieldValues extends FieldValues = FieldValues,
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
> = AsyncSelectBaseProps<T> &
|
|
AsyncExtraProps<T> &
|
|
UseControllerProps<TFieldValues, TName> &
|
|
Omit<SelectProps, ManagedSelectProps> & {
|
|
multiple?: false;
|
|
};
|
|
|
|
/** Multi-select async RHF props */
|
|
export type FieldAsyncSelectMultiProps<
|
|
T extends Record<string, any>,
|
|
TFieldValues extends FieldValues = FieldValues,
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
> = AsyncSelectBaseProps<T> &
|
|
AsyncExtraProps<T> &
|
|
UseControllerProps<TFieldValues, TName> &
|
|
Omit<MultiSelectProps, ManagedMultiSelectProps> & {
|
|
multiple: true;
|
|
};
|
|
|
|
export type FieldAsyncSelectProps<
|
|
T extends Record<string, any>,
|
|
TFieldValues extends FieldValues = FieldValues,
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
> =
|
|
| FieldAsyncSelectSingleProps<T, TFieldValues, TName>
|
|
| FieldAsyncSelectMultiProps<T, TFieldValues, TName>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Component Implementation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function FieldAsyncSelectInner<
|
|
T extends Record<string, any>,
|
|
TFieldValues extends FieldValues = FieldValues,
|
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
>(props: FieldAsyncSelectProps<T, TFieldValues, TName>) {
|
|
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<TFieldValues, TName>({
|
|
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 (
|
|
<AsyncSelect<T>
|
|
multiple
|
|
{...engineProps}
|
|
value={field.value ?? []}
|
|
onChange={handleChange}
|
|
{...(mantineProps as any)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AsyncSelect<T>
|
|
{...engineProps}
|
|
value={field.value ?? null}
|
|
onChange={handleChange}
|
|
{...(mantineProps as any)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const FieldAsyncSelect = React.memo(FieldAsyncSelectInner) as typeof FieldAsyncSelectInner;
|
|
(FieldAsyncSelect as any).displayName = 'FieldAsyncSelect';
|