Files
trackgo-fe/packages/ui/src/components/Form/fields/local-select.field.tsx
T

154 lines
4.8 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 { 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';