From 6694d35a74a71f8f6e2344c3bf06fd126adec54b Mon Sep 17 00:00:00 2001 From: Georgi2704 Date: Tue, 4 Nov 2025 09:32:51 +0100 Subject: [PATCH 1/3] Fix validation error not resetting when form field has change --- .../src/core/PydanticFormHandler.tsx | 12 ++++--- .../pydantic-forms/src/core/ReactHookForm.tsx | 11 +++++- .../src/core/hooks/usePydanticForm.tsx | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx b/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx index e6836ed..69f4278 100644 --- a/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx +++ b/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx @@ -11,11 +11,11 @@ import { getHashForArray } from '@/utils'; import { ReactHookForm } from './ReactHookForm'; export const PydanticFormHandler = ({ - formKey, - onCancel, - onSuccess, - title, -}: PydanticFormHandlerProps) => { + formKey, + onCancel, + onSuccess, + title, + }: PydanticFormHandlerProps) => { const config = useGetConfig(); const [formStep, setStep] = useState(); const formStepsRef = useRef([]); @@ -68,6 +68,7 @@ export const PydanticFormHandler = ({ isLoading, pydanticFormSchema, defaultValues, + handleRemoveValidationError } = usePydanticForm( formKey, config, @@ -117,6 +118,7 @@ export const PydanticFormHandler = ({ onPrevious={onPrevious} pydanticFormSchema={pydanticFormSchema} title={title} + handleRemoveValidationError={handleRemoveValidationError} /> ); diff --git a/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx b/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx index 6e41ec1..9cfc7be 100644 --- a/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx +++ b/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx @@ -5,7 +5,7 @@ * * Here we define the outline of the form */ -import React from 'react'; +import React, {useEffect} from 'react'; import type { FieldValues } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; @@ -36,6 +36,7 @@ export interface ReactHookFormProps { onPrevious: () => void; pydanticFormSchema?: PydanticFormSchema; title?: string; + handleRemoveValidationError: (location: string[]) => void; } export const ReactHookForm = ({ @@ -51,6 +52,7 @@ export const ReactHookForm = ({ onPrevious, pydanticFormSchema, title, + handleRemoveValidationError }: ReactHookFormProps) => { const config = useGetConfig(); const t = useTranslations('renderForm'); @@ -73,6 +75,13 @@ export const ReactHookForm = ({ values: initialValues || defaultValues, }); + useEffect(() => { + const reactHookFormWatch = reactHookForm.watch((_, { name }) => + name && handleRemoveValidationError([name]) + ); + return reactHookFormWatch.unsubscribe; + }, [handleRemoveValidationError, reactHookForm]); + if (apiError) { console.error('API Error:', apiError); return ErrorComponent; diff --git a/frontend/packages/pydantic-forms/src/core/hooks/usePydanticForm.tsx b/frontend/packages/pydantic-forms/src/core/hooks/usePydanticForm.tsx index 1796da6..62686de 100644 --- a/frontend/packages/pydantic-forms/src/core/hooks/usePydanticForm.tsx +++ b/frontend/packages/pydantic-forms/src/core/hooks/usePydanticForm.tsx @@ -25,6 +25,32 @@ export interface UsePydanticFormReturn { isSending: boolean; pydanticFormSchema?: PydanticFormSchema; defaultValues: FieldValues; + handleRemoveValidationError: (location: string[]) => void; +} + +const removeValidationErrorByLoc = ( + validationErrors: PydanticFormValidationErrorDetails | null, + locToRemove: (string | number)[] +): PydanticFormValidationErrorDetails | null => { + + if (!validationErrors) return null; + + const newSource = validationErrors.source.filter( + (err) => JSON.stringify(err.loc) !== JSON.stringify(locToRemove) + ); + + const topKey = locToRemove[0]?.toString(); + const newMapped = { ...validationErrors.mapped }; + + if (topKey && newMapped[topKey]) { + delete newMapped[topKey]; + } + + return { + ...validationErrors, + source: newSource, + mapped: newMapped, + }; } export function usePydanticForm( @@ -37,6 +63,7 @@ export function usePydanticForm( response: PydanticFormSuccessResponse, ) => void, formStep?: FieldValues, + ): UsePydanticFormReturn { const emptyRawSchema: PydanticFormSchemaRawJson = useMemo( () => ({ @@ -131,6 +158,12 @@ export function usePydanticForm( // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiResponse]); + const handleRemoveValidationError = (location: string[]) => { + setValidationErrorsDetails((prev) => + removeValidationErrorByLoc(prev, location) + ); + } + return { validationErrorsDetails, apiError, @@ -140,5 +173,6 @@ export function usePydanticForm( pydanticFormSchema, defaultValues, isSending, + handleRemoveValidationError }; } From 338ab1cd23ef84264943602142296b8f93ceda9e Mon Sep 17 00:00:00 2001 From: Georgi2704 Date: Mon, 10 Nov 2025 15:04:52 +0100 Subject: [PATCH 2/3] Validation error improvements --- .../src/core/PydanticFormHandler.tsx | 12 ++++---- .../pydantic-forms/src/core/ReactHookForm.tsx | 10 +++---- .../src/core/WrapFieldElement.tsx | 25 ++++++++++++++-- .../src/core/hooks/usePydanticForm.tsx | 30 +++++++++---------- 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx b/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx index 69f4278..5b179b6 100644 --- a/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx +++ b/frontend/packages/pydantic-forms/src/core/PydanticFormHandler.tsx @@ -11,11 +11,11 @@ import { getHashForArray } from '@/utils'; import { ReactHookForm } from './ReactHookForm'; export const PydanticFormHandler = ({ - formKey, - onCancel, - onSuccess, - title, - }: PydanticFormHandlerProps) => { + formKey, + onCancel, + onSuccess, + title, +}: PydanticFormHandlerProps) => { const config = useGetConfig(); const [formStep, setStep] = useState(); const formStepsRef = useRef([]); @@ -68,7 +68,7 @@ export const PydanticFormHandler = ({ isLoading, pydanticFormSchema, defaultValues, - handleRemoveValidationError + handleRemoveValidationError, } = usePydanticForm( formKey, config, diff --git a/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx b/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx index 9cfc7be..0fb1049 100644 --- a/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx +++ b/frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx @@ -5,7 +5,7 @@ * * Here we define the outline of the form */ -import React, {useEffect} from 'react'; +import React, { useEffect } from 'react'; import type { FieldValues } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; @@ -36,7 +36,7 @@ export interface ReactHookFormProps { onPrevious: () => void; pydanticFormSchema?: PydanticFormSchema; title?: string; - handleRemoveValidationError: (location: string[]) => void; + handleRemoveValidationError: (location: string) => void; } export const ReactHookForm = ({ @@ -52,7 +52,7 @@ export const ReactHookForm = ({ onPrevious, pydanticFormSchema, title, - handleRemoveValidationError + handleRemoveValidationError, }: ReactHookFormProps) => { const config = useGetConfig(); const t = useTranslations('renderForm'); @@ -76,8 +76,8 @@ export const ReactHookForm = ({ }); useEffect(() => { - const reactHookFormWatch = reactHookForm.watch((_, { name }) => - name && handleRemoveValidationError([name]) + const reactHookFormWatch = reactHookForm.watch( + (_, { name }) => name && handleRemoveValidationError(name), ); return reactHookFormWatch.unsubscribe; }, [handleRemoveValidationError, reactHookForm]); diff --git a/frontend/packages/pydantic-forms/src/core/WrapFieldElement.tsx b/frontend/packages/pydantic-forms/src/core/WrapFieldElement.tsx index ea5c260..3fa9c11 100644 --- a/frontend/packages/pydantic-forms/src/core/WrapFieldElement.tsx +++ b/frontend/packages/pydantic-forms/src/core/WrapFieldElement.tsx @@ -2,7 +2,21 @@ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { FieldWrap } from '@/components/fields'; -import type { PydanticFormControlledElement, PydanticFormField } from '@/types'; +import { useGetValidationErrors } from '@/core/hooks'; +import { + PydanticFormControlledElement, + PydanticFormField, + PydanticFormValidationErrorDetails, +} from '@/types'; + +const getValidationErrorMsg = ( + errorResponse: PydanticFormValidationErrorDetails | null, + path: string, +): string | undefined => { + return errorResponse?.source?.find( + (err) => err.loc.map(String).join('.') === path, + )?.msg; +}; export const WrapFieldElement = ({ PydanticFormControlledElement, @@ -14,6 +28,8 @@ export const WrapFieldElement = ({ extraTriggerFields?: string[]; }) => { const { control, trigger } = useFormContext(); + const validationErrorDetails = useGetValidationErrors(); + return ( void; + handleRemoveValidationError: (location: string) => void; } const removeValidationErrorByLoc = ( validationErrors: PydanticFormValidationErrorDetails | null, - locToRemove: (string | number)[] + locToRemove: string, ): PydanticFormValidationErrorDetails | null => { - if (!validationErrors) return null; - const newSource = validationErrors.source.filter( - (err) => JSON.stringify(err.loc) !== JSON.stringify(locToRemove) - ); + const newSource = validationErrors.source.filter((err) => { + const locPath = err.loc.join('.'); // e.g. "contact_persons.0.email" + return locPath !== locToRemove; + }); - const topKey = locToRemove[0]?.toString(); + const [topKey] = locToRemove.split('.'); // e.g. "contact_persons" const newMapped = { ...validationErrors.mapped }; if (topKey && newMapped[topKey]) { @@ -51,7 +51,7 @@ const removeValidationErrorByLoc = ( source: newSource, mapped: newMapped, }; -} +}; export function usePydanticForm( formKey: string, @@ -63,7 +63,6 @@ export function usePydanticForm( response: PydanticFormSuccessResponse, ) => void, formStep?: FieldValues, - ): UsePydanticFormReturn { const emptyRawSchema: PydanticFormSchemaRawJson = useMemo( () => ({ @@ -158,11 +157,12 @@ export function usePydanticForm( // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiResponse]); - const handleRemoveValidationError = (location: string[]) => { - setValidationErrorsDetails((prev) => - removeValidationErrorByLoc(prev, location) - ); - } + const handleRemoveValidationError = (location: string) => { + setValidationErrorsDetails((prev) => { + const updatedErrors = removeValidationErrorByLoc(prev, location); + return updatedErrors; + }); + }; return { validationErrorsDetails, @@ -173,6 +173,6 @@ export function usePydanticForm( pydanticFormSchema, defaultValues, isSending, - handleRemoveValidationError + handleRemoveValidationError, }; } From 829bd8c2276d4c5d71becc973f3328757380a97c Mon Sep 17 00:00:00 2001 From: Georgi2704 Date: Mon, 10 Nov 2025 15:23:16 +0100 Subject: [PATCH 3/3] Add changeset --- frontend/.changeset/spicy-kings-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 frontend/.changeset/spicy-kings-attack.md diff --git a/frontend/.changeset/spicy-kings-attack.md b/frontend/.changeset/spicy-kings-attack.md new file mode 100644 index 0000000..11123e3 --- /dev/null +++ b/frontend/.changeset/spicy-kings-attack.md @@ -0,0 +1,5 @@ +--- +'pydantic-forms': minor +--- + +Fix validation error not resetting when form field has change