feat: replace ObjectSelect with LocalSelect and introduce custom FieldSelect wrappers for object-based RHF state management
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
useController,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
type UseControllerProps,
|
||||
} from 'react-hook-form';
|
||||
import type { SelectProps, MultiSelectProps } from '@mantine/core';
|
||||
import { LocalSelect } from '../custom/selects/LocalSelect';
|
||||
import type { LocalSelectBaseProps } from '../custom/selects/types';
|
||||
import { useTranslatedError } from '../useTranslatedError';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FieldLocalSelect — RHF-connected Local Select
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Follows the same architectural pattern as the existing `FieldSelect`:
|
||||
// Mantine Component → withRHF HOC → FieldXxx
|
||||
//
|
||||
// But instead of using the generic withRHF factory (which assumes string values),
|
||||
// we use a manual useController binding with an object interception layer.
|
||||
// The actual rendering is delegated to the standalone LocalSelect engine
|
||||
// in `custom/selects/LocalSelect.tsx`.
|
||||
//
|
||||
// Data Mapping Contract:
|
||||
// Single: RHF stores T | null → maps to Mantine string | null
|
||||
// Multi: RHF stores T[] → maps to Mantine string[]
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** 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';
|
||||
|
||||
/** Single-select RHF props — stores T | null */
|
||||
export type FieldLocalSelectSingleProps<
|
||||
T extends Record<string, any>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = LocalSelectBaseProps<T> &
|
||||
UseControllerProps<TFieldValues, TName> &
|
||||
Omit<SelectProps, ManagedSelectProps> & {
|
||||
multiple?: false;
|
||||
};
|
||||
|
||||
/** Multi-select RHF props — stores T[] */
|
||||
export type FieldLocalSelectMultiProps<
|
||||
T extends Record<string, any>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = LocalSelectBaseProps<T> &
|
||||
UseControllerProps<TFieldValues, TName> &
|
||||
Omit<MultiSelectProps, ManagedMultiSelectProps> & {
|
||||
multiple: true;
|
||||
};
|
||||
|
||||
/** Discriminated union based on `multiple` */
|
||||
export type FieldLocalSelectProps<
|
||||
T extends Record<string, any>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> =
|
||||
| FieldLocalSelectSingleProps<T, TFieldValues, TName>
|
||||
| FieldLocalSelectMultiProps<T, TFieldValues, TName>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component Implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function FieldLocalSelectInner<
|
||||
T extends Record<string, any>,
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(props: FieldLocalSelectProps<T, TFieldValues, TName>) {
|
||||
const {
|
||||
// RHF controller props
|
||||
name,
|
||||
control,
|
||||
rules,
|
||||
shouldUnregister,
|
||||
defaultValue,
|
||||
disabled,
|
||||
// LocalSelect engine props
|
||||
options,
|
||||
valueKey,
|
||||
labelKey,
|
||||
renderLabel,
|
||||
multiple,
|
||||
filterOption,
|
||||
onSelect: onSelectCallback,
|
||||
// Remaining Mantine props
|
||||
...mantineProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
field,
|
||||
fieldState: { error },
|
||||
} = useController<TFieldValues, TName>({
|
||||
name,
|
||||
control,
|
||||
rules,
|
||||
shouldUnregister,
|
||||
defaultValue,
|
||||
disabled,
|
||||
});
|
||||
|
||||
// Translate the error message (handles JSON i18n payloads)
|
||||
const translatedError = useTranslatedError(error?.message);
|
||||
|
||||
// Intercept onChange to pass full objects to RHF
|
||||
const handleChange = (value: any) => {
|
||||
field.onChange(value);
|
||||
onSelectCallback?.(value);
|
||||
};
|
||||
|
||||
// Build engine props based on single/multi mode
|
||||
if (multiple) {
|
||||
return (
|
||||
<LocalSelect<T>
|
||||
multiple
|
||||
options={options}
|
||||
valueKey={valueKey}
|
||||
labelKey={labelKey}
|
||||
renderLabel={renderLabel}
|
||||
filterOption={filterOption}
|
||||
value={field.value ?? []}
|
||||
onChange={handleChange}
|
||||
onBlur={field.onBlur}
|
||||
error={translatedError}
|
||||
disabled={field.disabled}
|
||||
{...(mantineProps as any)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LocalSelect<T>
|
||||
options={options}
|
||||
valueKey={valueKey}
|
||||
labelKey={labelKey}
|
||||
renderLabel={renderLabel}
|
||||
filterOption={filterOption}
|
||||
value={field.value ?? null}
|
||||
onChange={handleChange}
|
||||
onBlur={field.onBlur}
|
||||
error={translatedError}
|
||||
disabled={field.disabled}
|
||||
{...(mantineProps as any)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FieldLocalSelect = React.memo(FieldLocalSelectInner) as typeof FieldLocalSelectInner;
|
||||
(FieldLocalSelect as any).displayName = 'FieldLocalSelect';
|
||||
Reference in New Issue
Block a user