diff --git a/app/frontend/src/javascript/components/accounting/advanced-accounting-form.tsx b/app/frontend/src/javascript/components/accounting/advanced-accounting-form.tsx index ab87ca680..6ebb2b54c 100644 --- a/app/frontend/src/javascript/components/accounting/advanced-accounting-form.tsx +++ b/app/frontend/src/javascript/components/accounting/advanced-accounting-form.tsx @@ -24,9 +24,10 @@ export const AdvancedAccountingForm = ({ regis }, []); return (<> +

toto

{isEnabled && <>
-

{t('app.admin.advanced_accounting_form.title')}

+

{t('app.admin.advanced_accounting_form.title')}

{ register: UseFormRegister, fieldMappingId: number, + formState: FormState } /** * Partial form to map an internal boolean field to an external API providing a string value. */ -export const BooleanMappingForm = ({ register, fieldMappingId }: BooleanMappingFormProps) => { +export const BooleanMappingForm = ({ register, fieldMappingId, formState }: BooleanMappingFormProps) => { const { t } = useTranslation('admin'); return ( @@ -20,10 +21,12 @@ export const BooleanMappingForm = ({ register,
); diff --git a/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx index b30f6afd3..785819bfb 100644 --- a/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx @@ -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 { providerType: ProvidableType, setValue: UseFormSetValue, currentFormValues: Array, + formState: FormState } 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 = ({ register, control, providerType, setValue, currentFormValues }: DataMappingFormProps) => { +export const DataMappingForm = ({ register, control, providerType, setValue, currentFormValues, formState }: DataMappingFormProps) => { const { t } = useTranslation('admin'); const [dataMapping, setDataMapping] = useState(null); const [isOpenTypeMappingModal, updateIsOpenTypeMappingModal] = useImmer>(new Map()); @@ -144,20 +145,24 @@ export const DataMappingForm =
- {providerType === 'OAuth2Provider' && } + {providerType === 'OAuth2Provider' && } {providerType === 'OpenIdConnectProvider' && }
@@ -172,7 +177,9 @@ export const DataMappingForm = diff --git a/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx index 767b43d9d..25e5b5a5b 100644 --- a/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx @@ -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 { control: Control, fieldMappingId: number, + formState: FormState } /** * Partial form for mapping an internal date field to an external API. */ -export const DateMappingForm = ({ control, fieldMappingId }: DateMappingFormProps) => { +export const DateMappingForm = ({ control, fieldMappingId, formState }: DateMappingFormProps) => { const { t } = useTranslation('admin'); // available date formats @@ -44,6 +45,7 @@ export const DateMappingForm = diff --git a/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx index bb1eef3a5..6beee1e13 100644 --- a/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx @@ -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 register: UseFormRegister, control: Control, fieldMappingId: number, + formState: FormState } /** * Partial for to map an internal integer field to an external API providing a string value. */ -export const IntegerMappingForm = ({ register, control, fieldMappingId }: IntegerMappingFormProps) => { +export const IntegerMappingForm = ({ register, control, fieldMappingId, formState }: IntegerMappingFormProps) => { const { t } = useTranslation('admin'); const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath }); @@ -33,11 +34,13 @@ export const IntegerMappingForm =
diff --git a/app/frontend/src/javascript/components/authentication-provider/oauth2-data-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/oauth2-data-mapping-form.tsx index 7ce810dc5..d9b3ce617 100644 --- a/app/frontend/src/javascript/components/authentication-provider/oauth2-data-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/oauth2-data-mapping-form.tsx @@ -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 { register: UseFormRegister, control: Control, index: number, + formState: FormState } /** * 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 = ({ register, control, index }: Oauth2DataMappingFormProps) => { +export const Oauth2DataMappingForm = ({ register, control, index, formState }: Oauth2DataMappingFormProps) => { const { t } = useTranslation('admin'); return ( @@ -24,15 +25,19 @@ export const Oauth2DataMappingForm = } label={t('app.admin.authentication.oauth2_data_mapping_form.api_field')} /> diff --git a/app/frontend/src/javascript/components/authentication-provider/oauth2-form.tsx b/app/frontend/src/javascript/components/authentication-provider/oauth2-form.tsx index b882c3346..bc19f5302 100644 --- a/app/frontend/src/javascript/components/authentication-provider/oauth2-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/oauth2-form.tsx @@ -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 { register: UseFormRegister, + formState: FormState, strategyName?: string, } /** * Partial form to fill the OAuth2 settings for a new/existing authentication provider. */ -export const Oauth2Form = ({ register, strategyName }: Oauth2FormProps) => { +export const Oauth2Form = ({ register, strategyName, formState }: Oauth2FormProps) => { const { t } = useTranslation('admin'); // regular expression to validate the input fields @@ -34,31 +35,37 @@ export const Oauth2Form = ({ 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} /> + rules={{ required: true, pattern: endpointRegex }} + formState={formState} /> + rules={{ required: true, pattern: endpointRegex }} + formState={formState} /> + rules={{ required: true, pattern: urlRegex }} + formState={formState} /> + rules={{ required: true }} + formState={formState} /> + rules={{ required: true }} + formState={formState} /> diff --git a/app/frontend/src/javascript/components/authentication-provider/openid-connect-data-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/openid-connect-data-mapping-form.tsx index ecc4740d1..9e51063ec 100644 --- a/app/frontend/src/javascript/components/authentication-provider/openid-connect-data-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/openid-connect-data-mapping-form.tsx @@ -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 { setValue: UseFormSetValue, currentFormValues: Array, index: number, + formState: FormState } /** * 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 = ({ register, setValue, currentFormValues, index }: OpenidConnectDataMappingFormProps) => { +export const OpenidConnectDataMappingForm = ({ register, setValue, currentFormValues, index, formState }: OpenidConnectDataMappingFormProps) => { const { t } = useTranslation('admin'); const standardConfiguration = { @@ -65,15 +66,18 @@ export const OpenidConnectDataMappingForm = ({ type="hidden" register={register} rules={{ required: true }} + formState={formState} defaultValue="user_info" /> } label={t('app.admin.authentication.openid_connect_data_mapping_form.api_field')} /> diff --git a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx index 830bfc48f..cb8fde7ec 100644 --- a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx @@ -161,41 +161,49 @@ export const OpenidConnectForm = + rules={{ required: false, pattern: urlRegex }} + formState={formState} />

{t('app.admin.authentication.openid_connect_form.client_options')}

{!currentFormValues?.discovery &&
{currentFormValues?.client_auth_method === 'jwks' && }
}
diff --git a/app/frontend/src/javascript/components/authentication-provider/provider-form.tsx b/app/frontend/src/javascript/components/authentication-provider/provider-form.tsx index 97fb8f9c7..80abc0876 100644 --- a/app/frontend/src/javascript/components/authentication-provider/provider-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/provider-form.tsx @@ -99,6 +99,7 @@ export const ProviderForm: React.FC = ({ action, provider, on register={register} disabled={action === 'update'} rules={{ required: true }} + formState={formState} label={t('app.admin.authentication.provider_form.name')} /> = ({ 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' && } - {providableType === 'OAuth2Provider' && } + {providableType === 'OAuth2Provider' && } {providableType === 'OpenIdConnectProvider' && = ({ action, provider, on setValue={setValue} />} {providableType && providableType !== 'DatabaseProvider' && } />} diff --git a/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx index 0f7e5e9df..26f00af5b 100644 --- a/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx @@ -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 { register: UseFormRegister, control: Control, fieldMappingId: number, + formState: FormState } /** * Partial form to map an internal string field to an external API. */ -export const StringMappingForm = ({ register, control, fieldMappingId }: StringMappingFormProps) => { +export const StringMappingForm = ({ register, control, fieldMappingId, formState }: StringMappingFormProps) => { const { t } = useTranslation('admin'); const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath }); @@ -33,10 +34,12 @@ export const StringMappingForm =
diff --git a/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx b/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx index ba0d0bcf8..a8397eeb6 100644 --- a/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx @@ -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 { register: UseFormRegister, control: Control, fieldMappingId: number, + formState: FormState } /** @@ -27,7 +28,7 @@ interface TypeMappingModalProps { * * This component is intended to be used in a react-hook-form context. */ -export const TypeMappingModal = ({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId }:TypeMappingModalProps) => { +export const TypeMappingModal = ({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId, formState }:TypeMappingModalProps) => { const { t } = useTranslation('admin'); return ( @@ -42,10 +43,10 @@ export const TypeMappingModal = - {type === 'integer' && } - {type === 'boolean' && } - {type === 'date' && } - {type === 'string' && } + {type === 'integer' && } + {type === 'boolean' && } + {type === 'date' && } + {type === 'string' && } ); }; diff --git a/app/frontend/src/javascript/components/dashboard/reservations/prepaid-packs-panel.tsx b/app/frontend/src/javascript/components/dashboard/reservations/prepaid-packs-panel.tsx index 5cb5402d6..247e57f37 100644 --- a/app/frontend/src/javascript/components/dashboard/reservations/prepaid-packs-panel.tsx +++ b/app/frontend/src/javascript/components/dashboard/reservations/prepaid-packs-panel.tsx @@ -33,7 +33,7 @@ const PrepaidPacksPanel: React.FC = ({ user, onError }) const [selectedMachine, setSelectedMachine] = useState(null); const [packsModal, setPacksModal] = useState(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 = ({ user, onError })

{t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_info')}

- + {t('app.logged.dashboard.reservations_dashboard.prepaid_packs_panel.cta_button')} diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 337dbaadf..6052208c3 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -186,6 +186,7 @@ export const EventForm: React.FC = ({ action, event, onError, on @@ -291,11 +292,13 @@ export const EventForm: React.FC = ({ 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')} /> handlePriceRemove(price, index)} icon={} /> diff --git a/app/frontend/src/javascript/components/form/abstract-form-item.tsx b/app/frontend/src/javascript/components/form/abstract-form-item.tsx index 3486e1399..5e05a5c4a 100644 --- a/app/frontend/src/javascript/components/form/abstract-form-item.tsx +++ b/app/frontend/src/javascript/components/form/abstract-form-item.tsx @@ -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 extends PropsWithChildren> { +export type AbstractFormItemProps = PropsWithChildren> & { id: string, label?: string|ReactNode, tooltip?: ReactNode, @@ -21,7 +21,7 @@ export interface AbstractFormItemProps extends PropsWithChildren({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps) => { const [isDirty, setIsDirty] = useState(false); - const [fieldError, setFieldError] = useState<{ message: string }>(error); + const [fieldError, setFieldError] = useState<{ message: string }>(null); const [isDisabled, setIsDisabled] = useState(false); useEffect(() => { @@ -29,10 +29,6 @@ export const AbstractFormItem = ({ 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 = ({ 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 = ({ id, label,
} {children}
- {(isDirty && fieldError) &&
{fieldError.message}
} + { fieldError &&
{fieldError.message}
} + {(isDirty && error) &&
{error.message}
} {(isDirty && warning) &&
{warning.message}
} )); diff --git a/app/frontend/src/javascript/components/form/form-checklist.tsx b/app/frontend/src/javascript/components/form/form-checklist.tsx index 5a35fe4ee..c8d71083a 100644 --- a/app/frontend/src/javascript/components/form/form-checklist.tsx +++ b/app/frontend/src/javascript/components/form/form-checklist.tsx @@ -9,7 +9,7 @@ import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item'; import { FabButton } from '../base/fab-button'; import { ChecklistOption } from '../../models/select'; -interface FormChecklistProps extends FormControlledComponent, AbstractFormItemProps { +type FormChecklistProps = FormControlledComponent & AbstractFormItemProps & { defaultValue?: Array, options: Array>, onChange?: (values: Array) => void, diff --git a/app/frontend/src/javascript/components/form/form-file-upload.tsx b/app/frontend/src/javascript/components/form/form-file-upload.tsx index 369f2cf52..253974e96 100644 --- a/app/frontend/src/javascript/components/form/form-file-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-file-upload.tsx @@ -13,7 +13,7 @@ import { FilePdf, Trash } from 'phosphor-react'; import { FileType } from '../../models/file'; import FileUploadLib from '../../lib/file-upload'; -interface FormFileUploadProps extends FormComponent, AbstractFormItemProps { +type FormFileUploadProps = FormComponent & AbstractFormItemProps & { setValue: UseFormSetValue, defaultFile?: FileType, accept?: string, diff --git a/app/frontend/src/javascript/components/form/form-image-upload.tsx b/app/frontend/src/javascript/components/form/form-image-upload.tsx index 6adea670b..1b7d174ef 100644 --- a/app/frontend/src/javascript/components/form/form-image-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-image-upload.tsx @@ -14,7 +14,7 @@ import { Trash } from 'phosphor-react'; import { ImageType } from '../../models/file'; import FileUploadLib from '../../lib/file-upload'; -interface FormImageUploadProps extends FormComponent, FormControlledComponent, AbstractFormItemProps { +type FormImageUploadProps = FormComponent & FormControlledComponent & AbstractFormItemProps & { setValue: UseFormSetValue, defaultImage?: ImageType, accept?: string, diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index 28c7b30fc..ee9261a56 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -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 extends FormComponent, AbstractFormItemProps { +type FormInputProps = FormComponent & AbstractFormItemProps & { icon?: ReactNode, addOn?: ReactNode, addOnAction?: (event: React.MouseEvent) => void, @@ -22,13 +22,14 @@ interface FormInputProps extends FormComponent) => void, nullable?: boolean, ariaLabel?: string, - maxLength?: number + maxLength?: number, + getValues?: UseFormGetValues } /** * This component is a template for an input component to use within React Hook Form */ -export const FormInput = ({ 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) => { +export const FormInput = ({ 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) => { const [characterCount, setCharacterCount] = useState(0); /** @@ -68,6 +69,13 @@ export const FormInput = ({ 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 || ''}`, diff --git a/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx b/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx index 474a21732..0381108ae 100644 --- a/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-multi-file-upload.tsx @@ -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 extends FormComponent, FormControlledComponent, AbstractFormItemProps { +type FormMultiFileUploadProps = FormComponent & FormControlledComponent & AbstractFormItemProps & { setValue: UseFormSetValue, addButtonLabel: ReactNode, accept: string diff --git a/app/frontend/src/javascript/components/form/form-multi-image-upload.tsx b/app/frontend/src/javascript/components/form/form-multi-image-upload.tsx index 4028b26cb..24b638235 100644 --- a/app/frontend/src/javascript/components/form/form-multi-image-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-multi-image-upload.tsx @@ -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 extends FormComponent, FormControlledComponent, AbstractFormItemProps { +type FormMultiImageUploadProps = FormComponent & FormControlledComponent & AbstractFormItemProps & { setValue: UseFormSetValue, addButtonLabel: ReactNode } diff --git a/app/frontend/src/javascript/components/form/form-multi-select.tsx b/app/frontend/src/javascript/components/form/form-multi-select.tsx index 5c06a4cb8..8d337e894 100644 --- a/app/frontend/src/javascript/components/form/form-multi-select.tsx +++ b/app/frontend/src/javascript/components/form/form-multi-select.tsx @@ -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 extends FormControlledComponent, AbstractFormItemProps { +type CommonProps = FormControlledComponent & AbstractFormItemProps & { valuesDefault?: Array, onChange?: (values: Array) => void, placeholder?: string, diff --git a/app/frontend/src/javascript/components/form/form-rich-text.tsx b/app/frontend/src/javascript/components/form/form-rich-text.tsx index cbb880eeb..54b476aaa 100644 --- a/app/frontend/src/javascript/components/form/form-rich-text.tsx +++ b/app/frontend/src/javascript/components/form/form-rich-text.tsx @@ -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 extends FormControlledComponent, AbstractFormItemProps { +type FormRichTextProps = FormControlledComponent & AbstractFormItemProps & { valueDefault?: string, limit?: number, heading?: boolean, diff --git a/app/frontend/src/javascript/components/form/form-select.tsx b/app/frontend/src/javascript/components/form/form-select.tsx index 3ca94aeda..9559e2b7a 100644 --- a/app/frontend/src/javascript/components/form/form-select.tsx +++ b/app/frontend/src/javascript/components/form/form-select.tsx @@ -9,7 +9,7 @@ import { FormControlledComponent } from '../../models/form-component'; import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item'; import { SelectOption } from '../../models/select'; -interface FormSelectProps extends FormControlledComponent, AbstractFormItemProps { +type FormSelectProps = FormControlledComponent & AbstractFormItemProps & { options: Array>, valueDefault?: TOptionValue, onChange?: (value: TOptionValue) => void, diff --git a/app/frontend/src/javascript/components/form/form-switch.tsx b/app/frontend/src/javascript/components/form/form-switch.tsx index 1f691efdc..cb1970df6 100644 --- a/app/frontend/src/javascript/components/form/form-switch.tsx +++ b/app/frontend/src/javascript/components/form/form-switch.tsx @@ -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 extends FormControlledComponent, AbstractFormItemProps { +type FormSwitchProps = FormControlledComponent & AbstractFormItemProps & { defaultValue?: boolean, onChange?: (value: boolean) => void, } diff --git a/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx b/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx index fce90e414..29c415f41 100644 --- a/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx +++ b/app/frontend/src/javascript/components/invoices/vat-settings-modal.tsx @@ -38,7 +38,7 @@ enableMapSet(); export const VatSettingsModal: React.FC = ({ isOpen, toggleModal, onError, onSuccess }) => { const { t } = useTranslation('admin'); - const { handleSubmit, reset, control, register } = useForm>(); + const { handleSubmit, reset, control, register, formState } = useForm>(); 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 = ({ isOpen, togg = ({ action, machine, onErr diff --git a/app/frontend/src/javascript/components/machines/machines-list.tsx b/app/frontend/src/javascript/components/machines/machines-list.tsx index 76d59e97d..7cf6adacd 100644 --- a/app/frontend/src/javascript/components/machines/machines-list.tsx +++ b/app/frontend/src/javascript/components/machines/machines-list.tsx @@ -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 = ({ onError, onSuccess, category: null }); + const [banner, setBanner] = useState>({}); + + // 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 = ({ 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 = ({ onError, onSuccess, return (
- {/* TODO: Condition to display editorial block */} - {false && + {banner.machines_banner_active && Lorem ipsum dolor sit amet

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.

'} - 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} /> }
diff --git a/app/frontend/src/javascript/components/plan-categories/plan-category-form.tsx b/app/frontend/src/javascript/components/plan-categories/plan-category-form.tsx index 3c04875a5..3d9e34dbb 100644 --- a/app/frontend/src/javascript/components/plan-categories/plan-category-form.tsx +++ b/app/frontend/src/javascript/components/plan-categories/plan-category-form.tsx @@ -22,7 +22,7 @@ interface PlanCategoryFormProps { const PlanCategoryForm: React.FC = ({ action, category, onSuccess, onError }) => { const { t } = useTranslation('admin'); - const { register, control, handleSubmit } = useForm({ defaultValues: { ...category } }); + const { register, control, handleSubmit, formState } = useForm({ 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 = ({ action, category, o return ( - + diff --git a/app/frontend/src/javascript/components/spaces/space-form.tsx b/app/frontend/src/javascript/components/spaces/space-form.tsx index d15c8e9f6..89a61e58a 100644 --- a/app/frontend/src/javascript/components/spaces/space-form.tsx +++ b/app/frontend/src/javascript/components/spaces/space-form.tsx @@ -94,6 +94,7 @@ export const SpaceForm: React.FC = ({ action, space, onError, on diff --git a/app/frontend/src/javascript/components/store/product-form.tsx b/app/frontend/src/javascript/components/store/product-form.tsx index 1c27246d5..bdb49b51e 100644 --- a/app/frontend/src/javascript/components/store/product-form.tsx +++ b/app/frontend/src/javascript/components/store/product-form.tsx @@ -254,7 +254,7 @@ export const ProductForm: React.FC = ({ product, title, onSucc
-

{t('app.admin.store.product_form.assigning_machines')}

+

{t('app.admin.store.product_form.assigning_machines')}

{t('app.admin.store.product_form.assigning_machines_info')}

diff --git a/app/frontend/src/javascript/components/store/store-product.tsx b/app/frontend/src/javascript/components/store/store-product.tsx index e7fec16b6..8175e7acc 100644 --- a/app/frontend/src/javascript/components/store/store-product.tsx +++ b/app/frontend/src/javascript/components/store/store-product.tsx @@ -45,7 +45,7 @@ export const StoreProduct: React.FC = ({ 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 = ({ productSlug, current
{product.product_images_attributes.map(i => (
- setShowImage(i.id)} src={i.thumb_attachment_url} /> + setShowImage(i.id as number)} src={i.thumb_attachment_url} />
))}
diff --git a/app/frontend/src/javascript/components/store/store-settings.tsx b/app/frontend/src/javascript/components/store/store-settings.tsx index d313afa65..8139dacea 100644 --- a/app/frontend/src/javascript/components/store/store-settings.tsx +++ b/app/frontend/src/javascript/components/store/store-settings.tsx @@ -31,6 +31,7 @@ export const StoreSettings: React.FC = ({ onError, onSuccess .then(settings => { const data = SettingLib.bulkMapToObject(settings); reset(data); + console.log(data); }) .catch(onError); }, []); diff --git a/app/frontend/src/javascript/components/trainings/training-form.tsx b/app/frontend/src/javascript/components/trainings/training-form.tsx index acaf4fa6e..75a493894 100644 --- a/app/frontend/src/javascript/components/trainings/training-form.tsx +++ b/app/frontend/src/javascript/components/trainings/training-form.tsx @@ -113,6 +113,7 @@ export const TrainingForm: React.FC = ({ action, training, on diff --git a/app/frontend/src/javascript/components/user/change-password.tsx b/app/frontend/src/javascript/components/user/change-password.tsx index 7c3899691..faf2c8794 100644 --- a/app/frontend/src/javascript/components/user/change-password.tsx +++ b/app/frontend/src/javascript/components/user/change-password.tsx @@ -33,7 +33,7 @@ export const ChangePassword = ({ register, onE const [isConfirmedPassword, setIsConfirmedPassword] = React.useState(false); const [isPrivileged, setIsPrivileged] = React.useState(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 = ({ register, onE type="password" register={passwordRegister} rules={{ required: true }} + formState={passwordFormState} label={t('app.shared.change_password.confirm_current')} /> {t('app.shared.change_password.confirm')} diff --git a/app/frontend/src/javascript/components/user/change-role-modal.tsx b/app/frontend/src/javascript/components/user/change-role-modal.tsx index 6183f4b4a..ee946e213 100644 --- a/app/frontend/src/javascript/components/user/change-role-modal.tsx +++ b/app/frontend/src/javascript/components/user/change-role-modal.tsx @@ -40,7 +40,7 @@ type selectGroupOption = { value: number, label: string }; */ export const ChangeRoleModal: React.FC = ({ isOpen, toggleModal, user, onSuccess, onError }) => { const { t } = useTranslation('admin'); - const { control, handleSubmit } = useForm({ defaultValues: { role: user.role, groupId: user.group_id } }); + const { control, handleSubmit, formState } = useForm({ defaultValues: { role: user.role, groupId: user.group_id } }); const [groups, setGroups] = useState>([]); @@ -104,14 +104,16 @@ export const ChangeRoleModal: React.FC = ({ isOpen, toggle control={control} id="role" label={t('app.admin.change_role_modal.new_role')} - rules={{ required: true }} /> + rules={{ required: true }} + formState={formState} /> + rules={{ required: true }} + formState={formState} /> ); diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 9c9238e68..52cddf0aa 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -208,6 +208,7 @@ export const UserProfileForm: React.FC = ({ action, size, label={t('app.shared.user_profile_form.date_of_birth')} disabled={isDisabled} rules={{ required: true }} + formState={formState} type="date" /> = ({ action, size, register={register} disabled={isDisabled} rules={{ required: fieldsSettings.get('address_required') === 'true' }} + formState={formState} label={t('app.shared.user_profile_form.address')} />
diff --git a/app/frontend/src/javascript/models/form-component.ts b/app/frontend/src/javascript/models/form-component.ts index 5c1188585..9f8af3fa3 100644 --- a/app/frontend/src/javascript/models/form-component.ts +++ b/app/frontend/src/javascript/models/form-component.ts @@ -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 { +interface AbstractFormComponentCommon { error?: { message: string }, - warning?: { message: string }, - rules?: ruleTypes, - formState?: FormState; + warning?: { message: string } } -export interface FormComponent extends AbstractFormComponent { +type AbstractFormComponentRules = + { rules: ruleTypes, formState: FormState } | + { rules?: never, formState?: FormState }; + +export type AbstractFormComponent = AbstractFormComponentCommon & AbstractFormComponentRules; + +export type FormComponent = AbstractFormComponent & { register: UseFormRegister, } -export interface FormControlledComponent extends AbstractFormComponent { +export type FormControlledComponent = AbstractFormComponent & { control: Control }