From 544324f4a274bdc4b14d625d142e209d3be73bad Mon Sep 17 00:00:00 2001 From: Ruben van Leeuwen Date: Tue, 2 Sep 2025 15:21:53 +0200 Subject: [PATCH 1/4] 2134: Fix and simplify data handling when an error occurs --- .../src/core/PydanticFormContextProvider.tsx | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx b/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx index b6682a5..d9963fa 100644 --- a/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx +++ b/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx @@ -74,7 +74,6 @@ function PydanticFormContextProvider({ new Map(), ); const [formInputData, setFormInputData] = useState([]); - const formRef = useRef(formKey); const updateHistory = useCallback( @@ -166,58 +165,46 @@ function PydanticFormContextProvider({ /* This method resets the form and makes sure it waits for the reset to complete - before proceeding. If it finds data in form history, it uses that data to reset the form. + before proceeding. If it finds data in formHistory based on the hash of the previo + us steps, it uses that data to prefill the form. */ - const awaitReset = useCallback( - async (payLoad?: FieldValues) => { - await getHashForArray(formInputData).then((hash) => { + const awaitReset = useCallback(async (payLoad?: FieldValues) => { + await getHashForArray(formInputData) + .then((hash) => { let resetPayload = {}; if (payLoad) { resetPayload = { ...payLoad }; - } else { - const currentStepFromHistory = formInputHistory.get(hash); - - if (currentStepFromHistory) { - resetPayload = { ...currentStepFromHistory }; - } } + reactHookForm.reset(resetPayload); + }) + .catch(() => { + console.error('Failed to hash form input data'); }); - }, - - [formInputData, formInputHistory, reactHookForm], - ); + }, []); - const addFormInputData = useCallback( - (formInput: object, replaceInsteadOfAdd = false) => { - setFormInputData((currentInputs) => { - const data = replaceInsteadOfAdd - ? currentInputs.slice(0, -1) - : currentInputs; - updateHistory(formInput, data); - return [...data, { ...formInput }]; - }); - awaitReset(); - }, - [awaitReset, setFormInputData, updateHistory], - ); + const addFormInputData = useCallback(() => { + setFormInputData((currentInputs) => { + // Note. If we don't use cloneDeep here we are adding a reference to the reactHookFormValues + // that changes on every change in the form and triggering effects before we want to. + const reactHookFormValues = _.cloneDeep(reactHookForm.getValues()); + updateHistory(reactHookFormValues, currentInputs); + return [...currentInputs, { ...reactHookFormValues }]; + }); + // setInErrorState(false); + // We call reset right after using the values to make sure + // values in reactHookForm are cleared. This avoids some + // race condition errors where reacfHookForm data was still set where we did not + /// expect it to be. + awaitReset(); + }, []); const submitFormFn = useCallback(() => { setIsSending(true); - const reactHookFormValues = _.cloneDeep(reactHookForm.getValues()); - awaitReset(); - // Note. If we don't use cloneDeep here we are adding a reference to the reactHookFormValues - // that changes on every change in the form and triggering effects before we want to. - addFormInputData(reactHookFormValues, !!errorDetails); + addFormInputData(); window.scrollTo(0, 0); - }, [ - reactHookForm, - errorDetails, - addFormInputData, - awaitReset, - setIsSending, - ]); + }, []); const onClientSideError = useCallback( (data?: FieldValues) => { @@ -245,23 +232,12 @@ function PydanticFormContextProvider({ [awaitReset, reactHookForm], ); - const resetErrorDetails = useCallback(() => { - setErrorDetails(undefined); - }, []); - const isLoading = isLoadingFormLabels || isLoadingSchema || isParsingSchema || (customDataProvider ? isLoadingCustomData : false); - const clearForm = useCallback(() => { - setFormInputData([]); - setIsFullFilled(false); - setRawSchema(emptyRawSchema); - setHasNext(false); - }, [emptyRawSchema]); - const fieldDataStorageRef = useRef>>( new Map(), ); @@ -299,9 +275,6 @@ function PydanticFormContextProvider({ ); const PydanticFormContextState = { - // to prevent an issue where the sending state hangs - // we check both the SWR hook state as our manual state - clearForm, config, customDataProvider, errorDetails, @@ -317,7 +290,6 @@ function PydanticFormContextProvider({ onPrevious: () => goToPreviousStep(reactHookForm?.getValues()), pydanticFormSchema, reactHookForm, - resetErrorDetails, resetForm, submitForm, title, @@ -332,12 +304,16 @@ function PydanticFormContextProvider({ if (apiResponse?.validation_errors) { // Restore the data we got the error with const errorPayload = [...formInputData].pop(); + setFormInputData((currentData) => { + const newData = [...currentData]; + newData.pop(); + return newData; + }); awaitReset(errorPayload); setErrorDetails(getErrorDetailsFromResponse(apiResponse)); return; } - awaitReset(); if (apiResponse?.success) { setIsFullFilled(true); return; @@ -345,10 +321,12 @@ function PydanticFormContextProvider({ // when we receive a new form from JSON, we fully reset the form if (apiResponse?.form && rawSchema !== apiResponse.form) { + awaitReset(); setRawSchema(apiResponse.form); if (apiResponse.meta) { setHasNext(!!apiResponse.meta.hasNext); } + setErrorDetails(undefined); } @@ -361,10 +339,9 @@ function PydanticFormContextProvider({ if (formKey !== formRef.current) { setFormInputData([]); setFormInputHistory(new Map()); - awaitReset({}); formRef.current = formKey; } - }, [awaitReset, formKey]); + }, [formKey]); // UseEffect to handle successfull submits useEffect(() => { @@ -379,7 +356,7 @@ function PydanticFormContextProvider({ setFormInputHistory(new Map()); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [apiResponse, isFullFilled]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls + }, [isFullFilled]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls // UseEffect to handles errors throws by the useApiProvider call // for instance unexpected 500 errors From c9725c8927767bd95d8a31832835b5bb3851b1fc Mon Sep 17 00:00:00 2001 From: Ruben van Leeuwen Date: Wed, 3 Sep 2025 09:33:35 +0200 Subject: [PATCH 2/4] 2134: Reintroduce restoringHistory. Rearrange awaitReset --- .../src/core/PydanticFormContextProvider.tsx | 126 ++++++++++-------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx b/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx index d9963fa..a7927fd 100644 --- a/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx +++ b/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx @@ -77,22 +77,17 @@ function PydanticFormContextProvider({ const formRef = useRef(formKey); const updateHistory = useCallback( - async (formInput: object, previousSteps: object[]) => { - const hashOfPreviousSteps = await getHashForArray(previousSteps); - setFormInputHistory((prevState) => - prevState.set(hashOfPreviousSteps, formInput), + async (previousStepsData: object[], formInputData: object) => { + const hashOfPreviousSteps = await getHashForArray( + previousStepsData, + ); + setFormInputHistory((currentState) => + currentState.set(hashOfPreviousSteps, formInputData), ); }, [], ); - const goToPreviousStep = (formInput: object) => { - setFormInputData((prevState) => { - updateHistory(formInput, prevState); - return prevState.slice(0, -1); - }); - }; - const [errorDetails, setErrorDetails] = useState(); @@ -168,36 +163,38 @@ function PydanticFormContextProvider({ before proceeding. If it finds data in formHistory based on the hash of the previo us steps, it uses that data to prefill the form. */ - const awaitReset = useCallback(async (payLoad?: FieldValues) => { - await getHashForArray(formInputData) - .then((hash) => { - let resetPayload = {}; - - if (payLoad) { - resetPayload = { ...payLoad }; - } - - reactHookForm.reset(resetPayload); - }) - .catch(() => { - console.error('Failed to hash form input data'); - }); - }, []); + const awaitReset = useCallback( + async (payLoad: FieldValues = {}) => { + try { + reactHookForm.reset(payLoad); + + // This is a workaround to we wait for the reset to complete + // https://gemini.google.com/app/26d8662d603d6322?hl=nl + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 0); + }); + } catch (error) { + console.error('Failed to reactHookFOrm', error); + } + }, + [reactHookForm], + ); const addFormInputData = useCallback(() => { - setFormInputData((currentInputs) => { + setFormInputData((currentFormInputData) => { // Note. If we don't use cloneDeep here we are adding a reference to the reactHookFormValues // that changes on every change in the form and triggering effects before we want to. const reactHookFormValues = _.cloneDeep(reactHookForm.getValues()); - updateHistory(reactHookFormValues, currentInputs); - return [...currentInputs, { ...reactHookFormValues }]; + updateHistory(currentFormInputData, reactHookFormValues); + // We call reset right after using the values to make sure + // values in reactHookForm are cleared. This avoids some + // race condition errors where reactHookForm data was still set where + // we did not expect it to be. + awaitReset(); + return [...currentFormInputData, { ...reactHookFormValues }]; }); - // setInErrorState(false); - // We call reset right after using the values to make sure - // values in reactHookForm are cleared. This avoids some - // race condition errors where reacfHookForm data was still set where we did not - /// expect it to be. - awaitReset(); }, []); const submitFormFn = useCallback(() => { @@ -217,6 +214,16 @@ function PydanticFormContextProvider({ [reactHookForm, submitFormFn], ); + const goToPreviousStep = () => { + setFormInputData((currentFormInputData) => { + // Stores any data that is entered but not submitted yet to be + // able to restore later + const reactHookFormValues = _.cloneDeep(reactHookForm.getValues()); + updateHistory(currentFormInputData, reactHookFormValues); + return currentFormInputData.slice(0, -1); + }); + }; + const submitForm = reactHookForm.handleSubmit( submitFormFn, onClientSideError, @@ -287,7 +294,7 @@ function PydanticFormContextProvider({ isLoading, isSending: isSending && isLoadingSchema, onCancel, - onPrevious: () => goToPreviousStep(reactHookForm?.getValues()), + onPrevious: () => goToPreviousStep(), pydanticFormSchema, reactHookForm, resetForm, @@ -297,19 +304,34 @@ function PydanticFormContextProvider({ // useEffect to handle API responses useEffect(() => { + const restoreHistory = async () => { + await getHashForArray(formInputData) + .then((hash) => { + if (formInputHistory.has(hash)) { + awaitReset(formInputHistory.get(hash) as FieldValues); + } else { + awaitReset(); + } + }) + .catch(() => { + console.error('Failed to hash form input data'); + }); + }; + if (!apiResponse) { return; } - // when we receive errors, we append to the scheme + if (apiResponse?.validation_errors) { - // Restore the data we got the error with - const errorPayload = [...formInputData].pop(); + // Restore the data we got the error with and remove it from + // formInputData so we can add it again setFormInputData((currentData) => { - const newData = [...currentData]; - newData.pop(); - return newData; + const nextData = [...currentData]; + const errorPayload = nextData.pop(); + awaitReset(errorPayload); + return nextData; }); - awaitReset(errorPayload); + setErrorDetails(getErrorDetailsFromResponse(apiResponse)); return; } @@ -319,22 +341,20 @@ function PydanticFormContextProvider({ return; } - // when we receive a new form from JSON, we fully reset the form if (apiResponse?.form && rawSchema !== apiResponse.form) { - awaitReset(); setRawSchema(apiResponse.form); + setErrorDetails(undefined); if (apiResponse.meta) { setHasNext(!!apiResponse.meta.hasNext); } - - setErrorDetails(undefined); + restoreHistory(); } setIsSending(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [apiResponse]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls - // Useeffect to the form input data if the formKey changes + // useEffect to the form input data if the formKey changes useEffect(() => { if (formKey !== formRef.current) { setFormInputData([]); @@ -343,22 +363,22 @@ function PydanticFormContextProvider({ } }, [formKey]); - // UseEffect to handle successfull submits + // useEffect to handle successfull submits useEffect(() => { if (!isFullFilled) { return; } if (onSuccess) { - const values = reactHookForm.getValues(); - onSuccess(values, apiResponse || {}); + const reactHookFormValues = _.cloneDeep(reactHookForm.getValues()); + onSuccess(reactHookFormValues, apiResponse || {}); } setFormInputHistory(new Map()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isFullFilled]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls - // UseEffect to handles errors throws by the useApiProvider call + // useEffect to handles errors throws by the useApiProvider call // for instance unexpected 500 errors useEffect(() => { if (!error) { @@ -372,7 +392,7 @@ function PydanticFormContextProvider({ }); }, [error]); - // UseEffect to handle locale change + // useEffect to handle locale change useEffect(() => { const getLocale = () => { switch (locale) { From 2aba66e6b51e09246a58195cd7abd5faf6407b78 Mon Sep 17 00:00:00 2001 From: Ruben van Leeuwen Date: Wed, 3 Sep 2025 09:40:44 +0200 Subject: [PATCH 3/4] 2134: Fixes resseting errorDetails in the correct spot --- .../pydantic-forms/src/core/PydanticFormContextProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx b/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx index a7927fd..6132316 100644 --- a/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx +++ b/frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx @@ -199,6 +199,7 @@ function PydanticFormContextProvider({ const submitFormFn = useCallback(() => { setIsSending(true); + setErrorDetails(undefined); addFormInputData(); window.scrollTo(0, 0); }, []); @@ -343,7 +344,6 @@ function PydanticFormContextProvider({ if (apiResponse?.form && rawSchema !== apiResponse.form) { setRawSchema(apiResponse.form); - setErrorDetails(undefined); if (apiResponse.meta) { setHasNext(!!apiResponse.meta.hasNext); } From 41646dd372f8b82802c11de213e4ae33c6bfbb65 Mon Sep 17 00:00:00 2001 From: Ruben van Leeuwen Date: Wed, 3 Sep 2025 13:01:46 +0200 Subject: [PATCH 4/4] 2134: Remove successnotice --- .../pydantic-forms/src/components/render/RenderForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/pydantic-forms/src/components/render/RenderForm.tsx b/frontend/packages/pydantic-forms/src/components/render/RenderForm.tsx index eb07d19..117e3b5 100644 --- a/frontend/packages/pydantic-forms/src/components/render/RenderForm.tsx +++ b/frontend/packages/pydantic-forms/src/components/render/RenderForm.tsx @@ -48,7 +48,7 @@ const RenderForm = (contextProps: PydanticFormContextProps) => { } if (isFullFilled) { - return
{t('successfullySent')}
; + return <>; } const FormRenderer = formRenderer ?? Form;