Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/.changeset/spicy-kings-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pydantic-forms': minor
---

Fix validation error not resetting when form field has change
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const PydanticFormHandler = ({
isLoading,
pydanticFormSchema,
defaultValues,
handleRemoveValidationError,
} = usePydanticForm(
formKey,
config,
Expand Down Expand Up @@ -117,6 +118,7 @@ export const PydanticFormHandler = ({
onPrevious={onPrevious}
pydanticFormSchema={pydanticFormSchema}
title={title}
handleRemoveValidationError={handleRemoveValidationError}
/>
</PydanticFormValidationErrorContext.Provider>
);
Expand Down
11 changes: 10 additions & 1 deletion frontend/packages/pydantic-forms/src/core/ReactHookForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -36,6 +36,7 @@ export interface ReactHookFormProps {
onPrevious: () => void;
pydanticFormSchema?: PydanticFormSchema;
title?: string;
handleRemoveValidationError: (location: string) => void;
}

export const ReactHookForm = ({
Expand All @@ -51,6 +52,7 @@ export const ReactHookForm = ({
onPrevious,
pydanticFormSchema,
title,
handleRemoveValidationError,
}: ReactHookFormProps) => {
const config = useGetConfig();
const t = useTranslations('renderForm');
Expand All @@ -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;
Expand Down
25 changes: 23 additions & 2 deletions frontend/packages/pydantic-forms/src/core/WrapFieldElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -14,6 +28,8 @@ export const WrapFieldElement = ({
extraTriggerFields?: string[];
}) => {
const { control, trigger } = useFormContext();
const validationErrorDetails = useGetValidationErrors();

return (
<Controller
name={pydanticFormField.id}
Expand All @@ -32,7 +48,12 @@ export const WrapFieldElement = ({
<FieldWrap
pydanticFormField={pydanticFormField}
isInvalid={fieldState.invalid}
frontendValidationMessage={fieldState.error?.message}
frontendValidationMessage={
getValidationErrorMsg(
validationErrorDetails,
pydanticFormField.id,
) ?? fieldState.error?.message
}
>
<PydanticFormControlledElement
onChange={onChangeHandle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,34 @@ export interface UsePydanticFormReturn {
isSending: boolean;
pydanticFormSchema?: PydanticFormSchema;
defaultValues: FieldValues;
handleRemoveValidationError: (location: string) => void;
}

const removeValidationErrorByLoc = (
validationErrors: PydanticFormValidationErrorDetails | null,
locToRemove: string,
): PydanticFormValidationErrorDetails | null => {
if (!validationErrors) return null;

const newSource = validationErrors.source.filter((err) => {
const locPath = err.loc.join('.'); // e.g. "contact_persons.0.email"
return locPath !== locToRemove;
});

const [topKey] = locToRemove.split('.'); // e.g. "contact_persons"
const newMapped = { ...validationErrors.mapped };

if (topKey && newMapped[topKey]) {
delete newMapped[topKey];
}

return {
...validationErrors,
source: newSource,
mapped: newMapped,
};
};

export function usePydanticForm(
formKey: string,
config: PydanticFormConfig,
Expand Down Expand Up @@ -131,6 +157,13 @@ export function usePydanticForm(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiResponse]);

const handleRemoveValidationError = (location: string) => {
setValidationErrorsDetails((prev) => {
const updatedErrors = removeValidationErrorByLoc(prev, location);
return updatedErrors;
});
};

return {
validationErrorsDetails,
apiError,
Expand All @@ -140,5 +173,6 @@ export function usePydanticForm(
pydanticFormSchema,
defaultValues,
isSending,
handleRemoveValidationError,
};
}