1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(bug) show error validation message

This commit is contained in:
Karen 2023-01-25 11:05:16 +01:00 committed by Sylvain
parent 5658e0aeed
commit 4dde127203
38 changed files with 167 additions and 82 deletions

View File

@ -24,9 +24,10 @@ export const AdvancedAccountingForm = <TFieldValues extends FieldValues>({ regis
}, []);
return (<>
<p>toto</p>
{isEnabled && <>
<header>
<p className="title">{t('app.admin.advanced_accounting_form.title')}</p>
<p className="title" role="heading">{t('app.admin.advanced_accounting_form.title')}</p>
</header>
<div className="content">
<FormInput register={register}

View File

@ -1,4 +1,4 @@
import { UseFormRegister } from 'react-hook-form';
import { UseFormRegister, FormState } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FormInput } from '../form/form-input';
@ -6,12 +6,13 @@ import { FormInput } from '../form/form-input';
export interface BooleanMappingFormProps<TFieldValues> {
register: UseFormRegister<TFieldValues>,
fieldMappingId: number,
formState: FormState<TFieldValues>
}
/**
* Partial form to map an internal boolean field to an external API providing a string value.
*/
export const BooleanMappingForm = <TFieldValues extends FieldValues>({ register, fieldMappingId }: BooleanMappingFormProps<TFieldValues>) => {
export const BooleanMappingForm = <TFieldValues extends FieldValues>({ register, fieldMappingId, formState }: BooleanMappingFormProps<TFieldValues>) => {
const { t } = useTranslation('admin');
return (
@ -20,10 +21,12 @@ export const BooleanMappingForm = <TFieldValues extends FieldValues>({ register,
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.true_value`}
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.boolean_mapping_form.true_value')} />
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.false_value`}
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.boolean_mapping_form.false_value')} />
</div>
);

View File

@ -3,7 +3,7 @@ import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path, FieldPathVal
import { FieldValues } from 'react-hook-form/dist/types/fields';
import AuthProviderAPI from '../../api/auth-provider';
import { AuthenticationProviderMapping, MappingFields, mappingType, ProvidableType } from '../../models/authentication-provider';
import { Control, UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
import { Control, UnpackNestedValue, UseFormSetValue, FormState } from 'react-hook-form/dist/types/form';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
import { useTranslation } from 'react-i18next';
@ -19,6 +19,7 @@ export interface DataMappingFormProps<TFieldValues, TContext extends object> {
providerType: ProvidableType,
setValue: UseFormSetValue<TFieldValues>,
currentFormValues: Array<AuthenticationProviderMapping>,
formState: FormState<TFieldValues>
}
type selectModelFieldOption = { value: string, label: string };
@ -26,7 +27,7 @@ type selectModelFieldOption = { value: string, label: string };
/**
* Partial form to define the mapping of the data between the API of the authentication provider and the application internals.
*/
export const DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, providerType, setValue, currentFormValues }: DataMappingFormProps<TFieldValues, TContext>) => {
export const DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, providerType, setValue, currentFormValues, formState }: DataMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const [dataMapping, setDataMapping] = useState<MappingFields>(null);
const [isOpenTypeMappingModal, updateIsOpenTypeMappingModal] = useImmer<Map<number, boolean>>(new Map());
@ -144,20 +145,24 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
<FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" />
<div className="local-data">
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_model`}
control={control} rules={{ required: true }}
control={control}
rules={{ required: true }}
formState={formState}
options={buildModelOptions()}
label={t('app.admin.authentication.data_mapping_form.model')}/>
<FormSelect id={`auth_provider_mappings_attributes.${index}.local_field`}
options={buildFieldOptions(output, index)}
control={control}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.data_mapping_form.field')} />
</div>
<div className="remote-data">
{providerType === 'OAuth2Provider' && <Oauth2DataMappingForm register={register} control={control} index={index} />}
{providerType === 'OAuth2Provider' && <Oauth2DataMappingForm register={register} control={control} index={index} formState={formState} />}
{providerType === 'OpenIdConnectProvider' && <OpenidConnectDataMappingForm register={register}
index={index}
setValue={setValue}
formState={formState}
currentFormValues={currentFormValues} />}
</div>
</div>
@ -172,7 +177,9 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
type={getDataType(output, index)}
isOpen={isOpenTypeMappingModal.get(index)}
toggleModal={toggleTypeMappingModal(index)}
control={control} register={register}
control={control}
register={register}
formState={formState}
fieldMappingId={index} />
</div>
</div>

View File

@ -1,17 +1,18 @@
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FormSelect } from '../form/form-select';
import { Control } from 'react-hook-form/dist/types/form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
export interface DateMappingFormProps<TFieldValues, TContext extends object> {
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
formState: FormState<TFieldValues>
}
/**
* Partial form for mapping an internal date field to an external API.
*/
export const DateMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ control, fieldMappingId }: DateMappingFormProps<TFieldValues, TContext>) => {
export const DateMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ control, fieldMappingId, formState }: DateMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
// available date formats
@ -44,6 +45,7 @@ export const DateMappingForm = <TFieldValues extends FieldValues, TContext exten
<FormSelect id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.format`}
control={control}
rules={{ required: true }}
formState={formState}
options={dateFormats}
label={t('app.admin.authentication.date_mapping_form.date_format')} />
</div>

View File

@ -1,5 +1,5 @@
import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
@ -9,12 +9,13 @@ export interface IntegerMappingFormProps<TFieldValues, TContext extends object>
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
formState: FormState<TFieldValues>
}
/**
* Partial for to map an internal integer field to an external API providing a string value.
*/
export const IntegerMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId }: IntegerMappingFormProps<TFieldValues, TContext>) => {
export const IntegerMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId, formState }: IntegerMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath<TFieldValues> });
@ -33,11 +34,13 @@ export const IntegerMappingForm = <TFieldValues extends FieldValues, TContext ex
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.from`}
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.integer_mapping_form.mapping_from')} />
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.to`}
register={register}
type="number"
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.integer_mapping_form.mapping_to')} />
</div>
<div className="actions">

View File

@ -1,5 +1,5 @@
import { UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FormInput } from '../form/form-input';
import { FormSelect } from '../form/form-select';
@ -10,13 +10,14 @@ interface Oauth2DataMappingFormProps<TFieldValues, TContext extends object> {
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
index: number,
formState: FormState<TFieldValues>
}
/**
* Partial form to set the data mapping for an OAuth 2.0 provider.
* The data mapping is the way to bind data from the authentication provider API to the Fab-manager's database
*/
export const Oauth2DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, index }: Oauth2DataMappingFormProps<TFieldValues, TContext>) => {
export const Oauth2DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, index, formState }: Oauth2DataMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
return (
@ -24,15 +25,19 @@ export const Oauth2DataMappingForm = <TFieldValues extends FieldValues, TContext
<FormInput id={`auth_provider_mappings_attributes.${index}.api_endpoint`}
register={register}
rules={{ required: true }}
formState={formState}
placeholder="/api/resource..."
label={t('app.admin.authentication.oauth2_data_mapping_form.api_endpoint_url')} />
<FormSelect id={`auth_provider_mappings_attributes.${index}.api_data_type`}
options={[{ label: 'JSON', value: 'json' }]}
control={control} rules={{ required: true }}
control={control}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.oauth2_data_mapping_form.api_type')} />
<FormInput id={`auth_provider_mappings_attributes.${index}.api_field`}
register={register}
rules={{ required: true }}
formState={formState}
placeholder="field_name..."
tooltip={<HtmlTranslate trKey="app.admin.authentication.oauth2_data_mapping_form.api_field_help_html" />}
label={t('app.admin.authentication.oauth2_data_mapping_form.api_field')} />

View File

@ -1,18 +1,19 @@
import { FormInput } from '../form/form-input';
import { UseFormRegister } from 'react-hook-form';
import { UseFormRegister, FormState } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FabOutputCopy } from '../base/fab-output-copy';
interface Oauth2FormProps<TFieldValues> {
register: UseFormRegister<TFieldValues>,
formState: FormState<TFieldValues>,
strategyName?: string,
}
/**
* Partial form to fill the OAuth2 settings for a new/existing authentication provider.
*/
export const Oauth2Form = <TFieldValues extends FieldValues>({ register, strategyName }: Oauth2FormProps<TFieldValues>) => {
export const Oauth2Form = <TFieldValues extends FieldValues>({ register, strategyName, formState }: Oauth2FormProps<TFieldValues>) => {
const { t } = useTranslation('admin');
// regular expression to validate the input fields
@ -34,31 +35,37 @@ export const Oauth2Form = <TFieldValues extends FieldValues>({ register, strateg
register={register}
placeholder="https://sso.example.net..."
label={t('app.admin.authentication.oauth2_form.common_url')}
rules={{ required: true, pattern: urlRegex }} />
rules={{ required: true, pattern: urlRegex }}
formState={formState} />
<FormInput id="providable_attributes.authorization_endpoint"
register={register}
placeholder="/oauth2/auth..."
label={t('app.admin.authentication.oauth2_form.authorization_endpoint')}
rules={{ required: true, pattern: endpointRegex }} />
rules={{ required: true, pattern: endpointRegex }}
formState={formState} />
<FormInput id="providable_attributes.token_endpoint"
register={register}
placeholder="/oauth2/token..."
label={t('app.admin.authentication.oauth2_form.token_acquisition_endpoint')}
rules={{ required: true, pattern: endpointRegex }} />
rules={{ required: true, pattern: endpointRegex }}
formState={formState} />
<FormInput id="providable_attributes.profile_url"
register={register}
placeholder="https://exemple.net/user..."
label={t('app.admin.authentication.oauth2_form.profile_edition_url')}
tooltip={t('app.admin.authentication.oauth2_form.profile_edition_url_help')}
rules={{ required: true, pattern: urlRegex }} />
rules={{ required: true, pattern: urlRegex }}
formState={formState} />
<FormInput id="providable_attributes.client_id"
register={register}
label={t('app.admin.authentication.oauth2_form.client_identifier')}
rules={{ required: true }} />
rules={{ required: true }}
formState={formState} />
<FormInput id="providable_attributes.client_secret"
register={register}
label={t('app.admin.authentication.oauth2_form.client_secret')}
rules={{ required: true }} />
rules={{ required: true }}
formState={formState} />
<FormInput id="providable_attributes.scopes" register={register}
placeholder="profile,email..."
label={t('app.admin.authentication.oauth2_form.scopes')} />

View File

@ -3,7 +3,7 @@ import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FormInput } from '../form/form-input';
import { HtmlTranslate } from '../base/html-translate';
import { useTranslation } from 'react-i18next';
import { UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
import { UnpackNestedValue, UseFormSetValue, FormState } from 'react-hook-form/dist/types/form';
import { FabButton } from '../base/fab-button';
import { FieldPathValue } from 'react-hook-form/dist/types/path';
import { AuthenticationProviderMapping } from '../../models/authentication-provider';
@ -13,13 +13,14 @@ interface OpenidConnectDataMappingFormProps<TFieldValues> {
setValue: UseFormSetValue<TFieldValues>,
currentFormValues: Array<AuthenticationProviderMapping>,
index: number,
formState: FormState<TFieldValues>
}
/**
* Partial form to set the data mapping for an OpenID Connect provider.
* The data mapping is the way to bind data from the OIDC claims to the Fab-manager's database
*/
export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({ register, setValue, currentFormValues, index }: OpenidConnectDataMappingFormProps<TFieldValues>) => {
export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({ register, setValue, currentFormValues, index, formState }: OpenidConnectDataMappingFormProps<TFieldValues>) => {
const { t } = useTranslation('admin');
const standardConfiguration = {
@ -65,15 +66,18 @@ export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({
type="hidden"
register={register}
rules={{ required: true }}
formState={formState}
defaultValue="user_info" />
<FormInput id={`auth_provider_mappings_attributes.${index}.api_data_type`}
type="hidden"
register={register}
rules={{ required: true }}
formState={formState}
defaultValue="json" />
<FormInput id={`auth_provider_mappings_attributes.${index}.api_field`}
register={register}
rules={{ required: true }}
formState={formState}
placeholder="claim..."
tooltip={<HtmlTranslate trKey="app.admin.authentication.openid_connect_data_mapping_form.api_field_help_html" />}
label={t('app.admin.authentication.openid_connect_data_mapping_form.api_field')} />

View File

@ -161,41 +161,49 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
placeholder="https://sso.exemple.com/my-account"
label={t('app.admin.authentication.openid_connect_form.profile_edition_url')}
tooltip={t('app.admin.authentication.openid_connect_form.profile_edition_url_help')}
rules={{ required: false, pattern: urlRegex }} />
rules={{ required: false, pattern: urlRegex }}
formState={formState} />
<h4>{t('app.admin.authentication.openid_connect_form.client_options')}</h4>
<FormInput id="providable_attributes.client__identifier"
label={t('app.admin.authentication.openid_connect_form.client__identifier')}
rules={{ required: true }}
formState={formState}
register={register} />
<FormInput id="providable_attributes.client__secret"
label={t('app.admin.authentication.openid_connect_form.client__secret')}
rules={{ required: true }}
formState={formState}
register={register} />
{!currentFormValues?.discovery && <div className="client-options-without-discovery">
<FormInput id="providable_attributes.client__authorization_endpoint"
label={t('app.admin.authentication.openid_connect_form.client__authorization_endpoint')}
placeholder="/authorize"
rules={{ required: !currentFormValues?.discovery, pattern: endpointRegex }}
formState={formState}
register={register} />
<FormInput id="providable_attributes.client__token_endpoint"
label={t('app.admin.authentication.openid_connect_form.client__token_endpoint')}
placeholder="/token"
rules={{ required: !currentFormValues?.discovery, pattern: endpointRegex }}
formState={formState}
register={register} />
<FormInput id="providable_attributes.client__userinfo_endpoint"
label={t('app.admin.authentication.openid_connect_form.client__userinfo_endpoint')}
placeholder="/userinfo"
rules={{ required: !currentFormValues?.discovery, pattern: endpointRegex }}
formState={formState}
register={register} />
{currentFormValues?.client_auth_method === 'jwks' && <FormInput id="providable_attributes.client__jwks_uri"
label={t('app.admin.authentication.openid_connect_form.client__jwks_uri')}
rules={{ required: currentFormValues.client_auth_method === 'jwks', pattern: endpointRegex }}
formState={formState}
placeholder="/jwk"
register={register} />}
<FormInput id="providable_attributes.client__end_session_endpoint"
label={t('app.admin.authentication.openid_connect_form.client__end_session_endpoint')}
tooltip={t('app.admin.authentication.openid_connect_form.client__end_session_endpoint_help')}
rules={{ pattern: endpointRegex }}
formState={formState}
register={register} />
</div>}
</div>

View File

@ -99,6 +99,7 @@ export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, on
register={register}
disabled={action === 'update'}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.provider_form.name')} />
<FormSelect id="providable_type"
control={control}
@ -106,9 +107,10 @@ export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, on
label={t('app.admin.authentication.provider_form.authentication_type')}
onChange={onProvidableTypeChange}
disabled={action === 'update'}
rules={{ required: true }} />
rules={{ required: true }}
formState={formState} />
{providableType === 'DatabaseProvider' && <DatabaseForm register={register} />}
{providableType === 'OAuth2Provider' && <Oauth2Form register={register} strategyName={strategyName} />}
{providableType === 'OAuth2Provider' && <Oauth2Form register={register} strategyName={strategyName} formState={formState} />}
{providableType === 'OpenIdConnectProvider' && <OpenidConnectForm register={register}
control={control}
currentFormValues={output.providable_attributes as OpenIdConnectProvider}
@ -116,6 +118,7 @@ export const ProviderForm: React.FC<ProviderFormProps> = ({ action, provider, on
setValue={setValue} />}
{providableType && providableType !== 'DatabaseProvider' && <DataMappingForm register={register}
control={control}
formState={formState}
providerType={providableType}
setValue={setValue}
currentFormValues={output.auth_provider_mappings_attributes as Array<AuthenticationProviderMapping>} />}

View File

@ -1,5 +1,5 @@
import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
@ -9,12 +9,13 @@ export interface StringMappingFormProps<TFieldValues, TContext extends object> {
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
formState: FormState<TFieldValues>
}
/**
* Partial form to map an internal string field to an external API.
*/
export const StringMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId }: StringMappingFormProps<TFieldValues, TContext>) => {
export const StringMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, fieldMappingId, formState }: StringMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath<TFieldValues> });
@ -33,10 +34,12 @@ export const StringMappingForm = <TFieldValues extends FieldValues, TContext ext
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.from`}
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.string_mapping_form.mapping_from')} />
<FormInput id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.mapping.${index}.to`}
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.authentication.string_mapping_form.mapping_to')} />
</div>
<div className="actions">

View File

@ -2,7 +2,7 @@ import { FabModal } from '../base/fab-modal';
import { useTranslation } from 'react-i18next';
import { IntegerMappingForm } from './integer-mapping-form';
import { UseFormRegister } from 'react-hook-form';
import { Control } from 'react-hook-form/dist/types/form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { mappingType } from '../../models/authentication-provider';
import { BooleanMappingForm } from './boolean-mapping-form';
@ -19,6 +19,7 @@ interface TypeMappingModalProps<TFieldValues, TContext extends object> {
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
fieldMappingId: number,
formState: FormState<TFieldValues>
}
/**
@ -27,7 +28,7 @@ interface TypeMappingModalProps<TFieldValues, TContext extends object> {
*
* This component is intended to be used in a react-hook-form context.
*/
export const TypeMappingModal = <TFieldValues extends FieldValues, TContext extends object>({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId }:TypeMappingModalProps<TFieldValues, TContext>) => {
export const TypeMappingModal = <TFieldValues extends FieldValues, TContext extends object>({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId, formState }:TypeMappingModalProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
return (
@ -42,10 +43,10 @@ export const TypeMappingModal = <TFieldValues extends FieldValues, TContext exte
id={`auth_provider_mappings_attributes.${fieldMappingId}.transformation.type`}
type="hidden"
defaultValue={type} />
{type === 'integer' && <IntegerMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
{type === 'boolean' && <BooleanMappingForm register={register} fieldMappingId={fieldMappingId} />}
{type === 'date' && <DateMappingForm control={control} fieldMappingId={fieldMappingId} />}
{type === 'string' && <StringMappingForm register={register} control={control} fieldMappingId={fieldMappingId} />}
{type === 'integer' && <IntegerMappingForm register={register} control={control} fieldMappingId={fieldMappingId} formState={formState} />}
{type === 'boolean' && <BooleanMappingForm register={register} fieldMappingId={fieldMappingId} formState={formState} />}
{type === 'date' && <DateMappingForm control={control} fieldMappingId={fieldMappingId} formState={formState} />}
{type === 'string' && <StringMappingForm register={register} control={control} fieldMappingId={fieldMappingId} formState={formState} />}
</FabModal>
);
};

View File

@ -33,7 +33,7 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
const [selectedMachine, setSelectedMachine] = useState<Machine>(null);
const [packsModal, setPacksModal] = useState<boolean>(false);
const { handleSubmit, control } = useForm<{ machine_id: number }>();
const { handleSubmit, control, formState } = useForm<{ machine_id: number }>();
useEffect(() => {
UserPackAPI.index({ user_id: user.id })
@ -125,7 +125,7 @@ const PrepaidPacksPanel: React.FC<PrepaidPacksPanelProps> = ({ user, onError })
<div className='prepaid-packs-cta'>
<p>{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_info')}</p>
<form onSubmit={handleSubmit(onBuyPack)}>
<FormSelect options={buildMachinesOptions(machines)} control={control} id="machine_id" rules={{ required: true }} label={t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.select_machine')} />
<FormSelect options={buildMachinesOptions(machines)} control={control} id="machine_id" rules={{ required: true }} formState={formState} label={t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.select_machine')} />
<FabButton className='is-black' type="submit">
{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_button')}
</FabButton>

View File

@ -186,6 +186,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
<FormRichText control={control}
id="description"
rules={{ required: true }}
formState={formState}
label={t('app.admin.event_form.description')}
limit={null}
heading bulletList blockquote link video image />
@ -291,11 +292,13 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
control={control}
id={`event_price_categories_attributes.${index}.price_category_id`}
rules={{ required: true }}
formState={formState}
label={t('app.admin.event_form.fare_class')} />
<FormInput id={`event_price_categories_attributes.${index}.amount`}
register={register}
type="number"
rules={{ required: true }}
formState={formState}
label={t('app.admin.event_form.price')}
addOn={FormatLib.currencySymbol()} />
<FabButton className="remove-price is-main" onClick={() => handlePriceRemove(price, index)} icon={<Trash size={20} />} />

View File

@ -4,7 +4,7 @@ import { AbstractFormComponent } from '../../models/form-component';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { get as _get } from 'lodash';
export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<AbstractFormComponent<TFieldValues>> {
export type AbstractFormItemProps<TFieldValues> = PropsWithChildren<AbstractFormComponent<TFieldValues>> & {
id: string,
label?: string|ReactNode,
tooltip?: ReactNode,
@ -21,7 +21,7 @@ export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<A
*/
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps<TFieldValues>) => {
const [isDirty, setIsDirty] = useState<boolean>(false);
const [fieldError, setFieldError] = useState<{ message: string }>(error);
const [fieldError, setFieldError] = useState<{ message: string }>(null);
const [isDisabled, setIsDisabled] = useState<boolean>(false);
useEffect(() => {
@ -29,10 +29,6 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
setFieldError(_get(formState?.errors, id));
}, [formState]);
useEffect(() => {
setFieldError(error);
}, [error]);
useEffect(() => {
if (typeof disabled === 'function') {
setIsDisabled(disabled(id));
@ -44,7 +40,7 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
// Compose classnames from props
const classNames = [
`${className || ''}`,
`${isDirty && fieldError ? 'is-incorrect' : ''}`,
`${(isDirty && error) || fieldError ? 'is-incorrect' : ''}`,
`${isDirty && warning ? 'is-warned' : ''}`,
`${rules && rules.required ? 'is-required' : ''}`,
`${isDisabled ? 'is-disabled' : ''}`
@ -79,7 +75,8 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
</div>}
{children}
</div>
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
{ fieldError && <div className="form-item-error">{fieldError.message}</div> }
{(isDirty && error) && <div className="form-item-error">{error.message}</div> }
{(isDirty && warning) && <div className="form-item-warning">{warning.message}</div> }
</>
));

View File

@ -9,7 +9,7 @@ import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
import { FabButton } from '../base/fab-button';
import { ChecklistOption } from '../../models/select';
interface FormChecklistProps<TFieldValues, TOptionValue, TContext extends object> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormChecklistProps<TFieldValues, TOptionValue, TContext extends object> = FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
defaultValue?: Array<TOptionValue>,
options: Array<ChecklistOption<TOptionValue>>,
onChange?: (values: Array<TOptionValue>) => void,

View File

@ -13,7 +13,7 @@ import { FilePdf, Trash } from 'phosphor-react';
import { FileType } from '../../models/file';
import FileUploadLib from '../../lib/file-upload';
interface FormFileUploadProps<TFieldValues> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
type FormFileUploadProps<TFieldValues> = FormComponent<TFieldValues> & AbstractFormItemProps<TFieldValues> & {
setValue: UseFormSetValue<TFieldValues>,
defaultFile?: FileType,
accept?: string,

View File

@ -14,7 +14,7 @@ import { Trash } from 'phosphor-react';
import { ImageType } from '../../models/file';
import FileUploadLib from '../../lib/file-upload';
interface FormImageUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormImageUploadProps<TFieldValues, TContext extends object> = FormComponent<TFieldValues> & FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
setValue: UseFormSetValue<TFieldValues>,
defaultImage?: ImageType,
accept?: string,

View File

@ -1,13 +1,13 @@
import { ReactNode, useCallback, useState } from 'react';
import { ReactNode, useCallback, useState, useEffect } from 'react';
import * as React from 'react';
import { FieldPathValue } from 'react-hook-form';
import { FieldPathValue, UseFormGetValues } from 'react-hook-form';
import { debounce as _debounce } from 'lodash';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FieldPath } from 'react-hook-form/dist/types/path';
import { FormComponent } from '../../models/form-component';
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldValues>, AbstractFormItemProps<TFieldValues> {
type FormInputProps<TFieldValues, TInputType> = FormComponent<TFieldValues> & AbstractFormItemProps<TFieldValues> & {
icon?: ReactNode,
addOn?: ReactNode,
addOnAction?: (event: React.MouseEvent<HTMLButtonElement>) => void,
@ -22,13 +22,14 @@ interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldV
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
nullable?: boolean,
ariaLabel?: string,
maxLength?: number
maxLength?: number,
getValues?: UseFormGetValues<FieldValues>
}
/**
* This component is a template for an input component to use within React Hook Form
*/
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength }: FormInputProps<TFieldValues, TInputType>) => {
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, getValues, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength }: FormInputProps<TFieldValues, TInputType>) => {
const [characterCount, setCharacterCount] = useState(0);
/**
@ -68,6 +69,13 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
}
};
// If maxLength and getValues is provided, uses input ref to initiate the countdown of characters
useEffect(() => {
if (getValues && maxLength) {
setCharacterCount(getValues(id).length);
}
}, [maxLength, getValues]);
// Compose classnames from props
const classNames = [
`${className || ''}`,

View File

@ -11,7 +11,7 @@ import { FileType } from '../../models/file';
import { UnpackNestedValue } from 'react-hook-form/dist/types';
import { FieldPathValue } from 'react-hook-form/dist/types/path';
interface FormMultiFileUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormMultiFileUploadProps<TFieldValues, TContext extends object> = FormComponent<TFieldValues> & FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
setValue: UseFormSetValue<TFieldValues>,
addButtonLabel: ReactNode,
accept: string

View File

@ -11,7 +11,7 @@ import { ImageType } from '../../models/file';
import { UnpackNestedValue } from 'react-hook-form/dist/types';
import { FieldPathValue } from 'react-hook-form/dist/types/path';
interface FormMultiImageUploadProps<TFieldValues, TContext extends object> extends FormComponent<TFieldValues>, FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormMultiImageUploadProps<TFieldValues, TContext extends object> = FormComponent<TFieldValues> & FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
setValue: UseFormSetValue<TFieldValues>,
addButtonLabel: ReactNode
}

View File

@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
import { Controller, FieldPathValue, Path } from 'react-hook-form';
import { UnpackNestedValue } from 'react-hook-form/dist/types/form';
interface CommonProps<TFieldValues, TContext extends object, TOptionValue> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type CommonProps<TFieldValues, TContext extends object, TOptionValue> = FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
valuesDefault?: Array<TOptionValue>,
onChange?: (values: Array<TOptionValue>) => void,
placeholder?: string,

View File

@ -8,7 +8,7 @@ import { Controller, Path } from 'react-hook-form';
import { FieldPath } from 'react-hook-form/dist/types/path';
import { FieldPathValue, UnpackNestedValue } from 'react-hook-form/dist/types';
interface FormRichTextProps<TFieldValues, TContext extends object> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormRichTextProps<TFieldValues, TContext extends object> = FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
valueDefault?: string,
limit?: number,
heading?: boolean,

View File

@ -9,7 +9,7 @@ import { FormControlledComponent } from '../../models/form-component';
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
import { SelectOption } from '../../models/select';
interface FormSelectProps<TFieldValues, TContext extends object, TOptionValue, TOptionLabel> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormSelectProps<TFieldValues, TContext extends object, TOptionValue, TOptionLabel> = FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
options: Array<SelectOption<TOptionValue, TOptionLabel>>,
valueDefault?: TOptionValue,
onChange?: (value: TOptionValue) => void,

View File

@ -5,7 +5,7 @@ import { Controller, Path } from 'react-hook-form';
import Switch from 'react-switch';
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
interface FormSwitchProps<TFieldValues, TContext extends object> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
type FormSwitchProps<TFieldValues, TContext extends object> = FormControlledComponent<TFieldValues, TContext> & AbstractFormItemProps<TFieldValues> & {
defaultValue?: boolean,
onChange?: (value: boolean) => void,
}

View File

@ -38,7 +38,7 @@ enableMapSet();
export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
const { t } = useTranslation('admin');
const { handleSubmit, reset, control, register } = useForm<Record<SettingName, SettingValue>>();
const { handleSubmit, reset, control, register, formState } = useForm<Record<SettingName, SettingValue>>();
const isActive = useWatch({ control, name: 'invoice_VAT-active' });
const generalRate = useWatch({ control, name: 'invoice_VAT-rate' });
@ -108,11 +108,13 @@ export const VatSettingsModal: React.FC<VatSettingsModalProps> = ({ isOpen, togg
<FormInput register={register}
id="invoice_VAT-name"
rules={{ required: true }}
formState={formState}
tooltip={t('app.admin.vat_settings_modal.VAT_name_help')}
label={t('app.admin.vat_settings_modal.VAT_name')} />
<FormInput register={register}
id="invoice_VAT-rate"
rules={{ required: true }}
formState={formState}
tooltip={t('app.admin.vat_settings_modal.VAT_rate_help')}
type='number'
step={0.001}

View File

@ -127,12 +127,14 @@ export const MachineForm: React.FC<MachineFormProps> = ({ action, machine, onErr
<FormRichText control={control}
id="description"
rules={{ required: true }}
formState={formState}
label={t('app.admin.machine_form.description')}
limit={null}
heading bulletList blockquote link image video />
<FormRichText control={control}
id="spec"
rules={{ required: true }}
formState={formState}
label={t('app.admin.machine_form.technical_specifications')}
limit={null}
heading bulletList link />

View File

@ -11,6 +11,9 @@ import { MachineCard } from './machine-card';
import { MachinesFilters } from './machines-filters';
import { User } from '../../models/user';
import { EditorialBlock } from '../editorial-block/editorial-block';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
import { SettingValue, machineBannerSettings } from '../../models/setting';
declare const Application: IApplication;
@ -41,6 +44,17 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
category: null
});
const [banner, setBanner] = useState<Record<string, SettingValue>>({});
// fetch Banner text and button from API
const fetchBanner = async () => {
SettingAPI.query(machineBannerSettings)
.then(settings => {
setBanner({ ...SettingLib.bulkMapToObject(settings) });
})
.catch(onError);
};
// retrieve the full list of machines on component mount
useEffect(() => {
MachineAPI.index()
@ -49,6 +63,7 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
MachineCategoryAPI.index()
.then(data => setMachineCategories(data))
.catch(e => onError(e));
fetchBanner();
}, []);
// filter the machines shown when the full list was retrieved
@ -96,12 +111,11 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
return (
<div className="machines-list">
{/* TODO: Condition to display editorial block */}
{false &&
{banner.machines_banner_active &&
<EditorialBlock
text={'<h3>Lorem ipsum dolor sit amet</h3><p>Consectetur adipiscing elit. In eget eros sed odio tristique cursus. Quisque pretium tortor vel lorem tempor, eu egestas lorem laoreet. Pellentesque arcu lectus, rutrum eu volutpat nec, luctus eget sapien. Sed ligula tortor, blandit eget purus sit sed.</p>'}
cta={'Pif paf pouf'}
url={'https://www.plop.io'} />
text={banner.machines_banner_text}
cta={banner.machines_banner_cta_active && banner.machines_banner_cta_label}
url={banner.machines_banner_cta_active && banner.machines_banner_cta_url} />
}
<MachinesFilters onFilterChangedBy={handleFilterChangedBy} machineCategories={machineCategories}/>
<div className="all-machines">

View File

@ -22,7 +22,7 @@ interface PlanCategoryFormProps {
const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const { register, control, handleSubmit } = useForm<PlanCategory>({ defaultValues: { ...category } });
const { register, control, handleSubmit, formState } = useForm<PlanCategory>({ defaultValues: { ...category } });
/**
* The action has been confirmed by the user.
* Push the created/updated plan-category to the API.
@ -48,7 +48,7 @@ const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, o
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput id='name' register={register} rules={{ required: 'true' }} label={t('app.admin.plan_category_form.name')} />
<FormInput id='name' register={register} rules={{ required: 'true' }} formState={formState} label={t('app.admin.plan_category_form.name')} />
<FormRichText control={control} id="description" label={t('app.admin.plan_category_form.description')} limit={100} />

View File

@ -94,6 +94,7 @@ export const SpaceForm: React.FC<SpaceFormProps> = ({ action, space, onError, on
<FormRichText control={control}
id="description"
rules={{ required: true }}
formState={formState}
label={t('app.admin.space_form.description')}
limit={null}
heading bulletList blockquote link video image />

View File

@ -254,7 +254,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<section>
<header>
<p className="title">{t('app.admin.store.product_form.assigning_machines')}</p>
<p className="title" role="heading">{t('app.admin.store.product_form.assigning_machines')}</p>
<p className="description">{t('app.admin.store.product_form.assigning_machines_info')}</p>
</header>
<div className="content">

View File

@ -45,7 +45,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, current
setProduct(data);
const productImage = _.find(data.product_images_attributes, { is_main: true });
if (productImage) {
setShowImage(productImage.id);
setShowImage(productImage.id as number);
}
setToCartCount(data.quantity_min ? data.quantity_min : 1);
setDisplayToggle(descContainer.current.offsetHeight < descContainer.current.scrollHeight);
@ -132,7 +132,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, current
<div className='thumbnails'>
{product.product_images_attributes.map(i => (
<div key={i.id} className={`picture ${i.id === showImage ? 'is-active' : ''}`}>
<img alt='' onClick={() => setShowImage(i.id)} src={i.thumb_attachment_url} />
<img alt='' onClick={() => setShowImage(i.id as number)} src={i.thumb_attachment_url} />
</div>
))}
</div>

View File

@ -31,6 +31,7 @@ export const StoreSettings: React.FC<StoreSettingsProps> = ({ onError, onSuccess
.then(settings => {
const data = SettingLib.bulkMapToObject(settings);
reset(data);
console.log(data);
})
.catch(onError);
}, []);

View File

@ -113,6 +113,7 @@ export const TrainingForm: React.FC<TrainingFormProps> = ({ action, training, on
<FormRichText control={control}
id="description"
rules={{ required: true }}
formState={formState}
label={t('app.admin.training_form.description')}
limit={null}
heading bulletList blockquote link />

View File

@ -33,7 +33,7 @@ export const ChangePassword = <TFieldValues extends FieldValues>({ register, onE
const [isConfirmedPassword, setIsConfirmedPassword] = React.useState<boolean>(false);
const [isPrivileged, setIsPrivileged] = React.useState<boolean>(false);
const { handleSubmit, register: passwordRegister } = useForm<{ password: string }>();
const { handleSubmit, register: passwordRegister, formState: passwordFormState } = useForm<{ password: string }>();
useEffect(() => {
MemberAPI.current().then(operator => {
@ -106,6 +106,7 @@ export const ChangePassword = <TFieldValues extends FieldValues>({ register, onE
type="password"
register={passwordRegister}
rules={{ required: true }}
formState={passwordFormState}
label={t('app.shared.change_password.confirm_current')} />
<FabButton type="submit">
{t('app.shared.change_password.confirm')}

View File

@ -40,7 +40,7 @@ type selectGroupOption = { value: number, label: string };
*/
export const ChangeRoleModal: React.FC<ChangeRoleModalProps> = ({ isOpen, toggleModal, user, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const { control, handleSubmit } = useForm<RoleFormData>({ defaultValues: { role: user.role, groupId: user.group_id } });
const { control, handleSubmit, formState } = useForm<RoleFormData>({ defaultValues: { role: user.role, groupId: user.group_id } });
const [groups, setGroups] = useState<Array<Group>>([]);
@ -104,14 +104,16 @@ export const ChangeRoleModal: React.FC<ChangeRoleModalProps> = ({ isOpen, toggle
control={control}
id="role"
label={t('app.admin.change_role_modal.new_role')}
rules={{ required: true }} />
rules={{ required: true }}
formState={formState} />
<FormSelect options={buildGroupsOptions()}
control={control}
disabled={!canChangeGroup()}
id="groupId"
label={t('app.admin.change_role_modal.new_group')}
tooltip={t('app.admin.change_role_modal.new_group_help')}
rules={{ required: true }} />
rules={{ required: true }}
formState={formState} />
</form>
</FabModal>
);

View File

@ -208,6 +208,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
label={t('app.shared.user_profile_form.date_of_birth')}
disabled={isDisabled}
rules={{ required: true }}
formState={formState}
type="date" />
<FormInput id="profile_attributes.phone"
register={register}
@ -230,6 +231,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
register={register}
disabled={isDisabled}
rules={{ required: fieldsSettings.get('address_required') === 'true' }}
formState={formState}
label={t('app.shared.user_profile_form.address')} />
</div>
</div>

View File

@ -2,7 +2,7 @@ import { UseFormRegister, Validate } from 'react-hook-form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
export type ruleTypes = {
required?: boolean | string,
required?: boolean | string | { value: boolean, message: string },
pattern?: RegExp | { value: RegExp, message: string },
minLength?: number | { value: number, message: string },
maxLength?: number | { value: number, message: string },
@ -16,17 +16,21 @@ export type ruleTypes = {
* Automatic error handling is done through the `formState` prop.
* Even for manual error/warning, the `formState` prop is required, because it is used to determine if the field is dirty.
*/
export interface AbstractFormComponent<TFieldValues> {
interface AbstractFormComponentCommon {
error?: { message: string },
warning?: { message: string },
rules?: ruleTypes,
formState?: FormState<TFieldValues>;
warning?: { message: string }
}
export interface FormComponent<TFieldValues> extends AbstractFormComponent<TFieldValues> {
type AbstractFormComponentRules<TFieldValues> =
{ rules: ruleTypes, formState: FormState<TFieldValues> } |
{ rules?: never, formState?: FormState<TFieldValues> };
export type AbstractFormComponent<TFieldValues> = AbstractFormComponentCommon & AbstractFormComponentRules<TFieldValues>;
export type FormComponent<TFieldValues> = AbstractFormComponent<TFieldValues> & {
register: UseFormRegister<TFieldValues>,
}
export interface FormControlledComponent<TFieldValues, TContext extends object> extends AbstractFormComponent<TFieldValues> {
export type FormControlledComponent<TFieldValues, TContext extends object> = AbstractFormComponent<TFieldValues> & {
control: Control<TFieldValues, TContext>
}