From 7627cfcb8ac0767a18de780ec56096977c8e7760 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:43:11 +0200 Subject: [PATCH 1/2] refactor: add a formfield component --- .../questionnaires/form/QuestionnaireForm.tsx | 127 +++++++----------- next/src/components/ui/form/FormField.tsx | 94 +++++++++++++ 2 files changed, 145 insertions(+), 76 deletions(-) create mode 100644 next/src/components/ui/form/FormField.tsx diff --git a/next/src/components/questionnaires/form/QuestionnaireForm.tsx b/next/src/components/questionnaires/form/QuestionnaireForm.tsx index 1dd01d8c7..36e009ad1 100644 --- a/next/src/components/questionnaires/form/QuestionnaireForm.tsx +++ b/next/src/components/questionnaires/form/QuestionnaireForm.tsx @@ -1,11 +1,11 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useNavigate } from '@tanstack/react-router' -import { Controller, type SubmitHandler, useForm } from 'react-hook-form' +import { type SubmitHandler, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import Checkbox from '@/components/ui/form/Checkbox' -import Field from '@/components/ui/form/Field' import Form from '@/components/ui/form/Form' +import FormField from '@/components/ui/form/FormField' import Input from '@/components/ui/form/Input' import Label from '@/components/ui/form/Label' import RadioGroup from '@/components/ui/form/RadioGroup' @@ -76,31 +76,24 @@ export default function QuestionnaireForm({ isSubmitted={isSubmitted} validateLabel={submitLabel} > - ( - - - + name="title" + label={t('common.title')} + required + > + {({ field: { value, onChange } }) => ( + )} - /> - + + ( + noField + > + {({ field, fieldState: { error } }) => ( <>
@@ -150,68 +143,50 @@ export default function QuestionnaireForm({ ) : null} )} - /> + +
- ( - - - + > + {({ field: { value, onBlur, onChange } }) => ( + )} - /> +
+
- ( - - - + > + {({ field: { value, onBlur, onChange } }) => ( + )} - /> +
) diff --git a/next/src/components/ui/form/FormField.tsx b/next/src/components/ui/form/FormField.tsx new file mode 100644 index 000000000..60d699ca8 --- /dev/null +++ b/next/src/components/ui/form/FormField.tsx @@ -0,0 +1,94 @@ +import { + type Control, + Controller, + type ControllerRenderProps, + type FieldPath, + type FieldPathValue, + type FieldValues, + type RegisterOptions, +} from 'react-hook-form' + +import Field, { type Props as FieldProps } from './Field' + +type RenderProps> = { + field: ControllerRenderProps + fieldState: { + invalid: boolean + isDirty: boolean + isTouched: boolean + error?: FieldProps['error'] + } +} + +type FormFieldProps> = { + control: Control + name: N + defaultValue?: FieldPathValue + label?: FieldProps['label'] + description?: FieldProps['description'] + required?: boolean + disabled?: boolean + rules?: Omit< + RegisterOptions, + 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled' + > + noField?: boolean + /** Input component such as Select, NumberFieldm etc.. */ + children: (props: RenderProps) => React.ReactNode +} + +/** + * A component that encapsulate both form and controller layout + */ +export default function FormField< + T extends FieldValues, + N extends FieldPath, +>({ + control, + name, + defaultValue, + label, + description, + required = false, + disabled = false, + rules, + noField = false, + children, +}: Readonly>) { + return ( + { + const renderProps: RenderProps = { + field, + fieldState: { invalid, isTouched, isDirty, error }, + } + // VTL editor field do not have + if (noField) { + return <>{children(renderProps)} + } + return ( + + {children(renderProps)} + + ) + }} + /> + ) +} From 7af6922635eca44ddd2b9ba4b43fdfefb09407b1 Mon Sep 17 00:00:00 2001 From: Mailine Nguyen <64129348+MailineN@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:50:52 +0200 Subject: [PATCH 2/2] feat: add variable form --- .../variables/form/VariableForm.tsx | 429 +++++++----------- 1 file changed, 166 insertions(+), 263 deletions(-) diff --git a/next/src/components/variables/form/VariableForm.tsx b/next/src/components/variables/form/VariableForm.tsx index 056830904..822c43525 100644 --- a/next/src/components/variables/form/VariableForm.tsx +++ b/next/src/components/variables/form/VariableForm.tsx @@ -1,10 +1,10 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useNavigate } from '@tanstack/react-router' import { t } from 'i18next' -import { Controller, type SubmitHandler, useForm } from 'react-hook-form' +import { type SubmitHandler, useForm } from 'react-hook-form' -import Field from '@/components/ui/form/Field' import Form from '@/components/ui/form/Form' +import FormField from '@/components/ui/form/FormField' import Input from '@/components/ui/form/Input' import NumberField from '@/components/ui/form/NumberField' import RadioGroup from '@/components/ui/form/RadioGroup' @@ -106,120 +106,86 @@ export default function VariableForm({ validateLabel={submitLabel} >
- ( - - - + > + {({ field: { value, onBlur, onChange } }) => ( + )} - /> +
+ {selectedType === VariableType.External ? ( - ( - - - + name="isDeletedOnReset" + label={t('variable.isDeletedOnReset')} + > + {({ field: { ref, value, onBlur, onChange } }) => ( + )} - /> + ) : null} - ( - - onChange(convertToValidName(v))} - /> - + name="name" + label={t('variable.name')} + required + > + {({ field: { value, onChange } }) => ( + onChange(convertToValidName(v))} + /> )} - /> - + + ( - - - + name="description" + label={t('variable.description')} + required + > + {({ field: { value, onChange } }) => ( + )} - /> + + {selectedType === VariableType.Calculated ? ( - + {({ field: { name, value, onChange }, - fieldState: { invalid, isTouched, isDirty, error }, + fieldState: { isDirty, error, invalid, isTouched }, }) => ( )} - /> + ) : null} - ( - - - options={[ - { label: t('common.questionnaire'), value: '' }, - ...Array.from(scopes ?? new Map()).map( - ([id, name]) => ({ - label: name, - value: id, - }), - ), - ]} - value={value} - onChange={onChange} - /> - + > + {({ field: { value, onChange } }) => ( + + options={[ + { label: t('common.questionnaire'), value: '' }, + ...Array.from(scopes ?? new Map()).map( + ([id, name]) => ({ + label: name, + value: id, + }), + ), + ]} + value={value} + onChange={onChange} + /> )} - /> - + + ( - - - options={datatypeTypeNameOptions} - value={value} - onChange={onChange} - disabled={isDatatypeTypeNameDisabled} - /> - + > + {({ field: { value, onChange } }) => ( + + options={datatypeTypeNameOptions} + value={value} + onChange={onChange} + disabled={isDatatypeTypeNameDisabled} + /> )} - /> + + {selectedTypeName === DatatypeType.Date ? ( - ( - - - options={dateFormatOptions} - value={value as DateFormat | undefined} - onChange={onChange} - /> - + > + {({ field: { value, onChange } }) => ( + + options={dateFormatOptions} + value={value as DateFormat | undefined} + onChange={onChange} + /> )} - /> + ) : null} + {selectedTypeName === DatatypeType.Numeric ? ( <> - ( - - - + > + {({ field: { ref, value, onChange } }) => ( + )} - /> - + + ( - - - + > + {({ field: { ref, value, onChange } }) => ( + )} - /> - + + ( - - - + label={t('variable.precision')} + > + {({ field: { ref, value, onChange } }) => ( + )} - /> + ) : null} + {selectedTypeName === DatatypeType.Text ? ( - ( - - - + label={t('variable.maxLength')} + required + > + {({ field: { ref, value, onChange } }) => ( + )} - /> + ) : null} )