Skip to content

Commit e1d3337

Browse files
authored
Merge pull request #164 from workfloworchestrator/fix-form-reset
Fix form reset
2 parents 26905ce + 05ecfa2 commit e1d3337

File tree

6 files changed

+128
-117
lines changed

6 files changed

+128
-117
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'pydantic-forms': patch
3+
---
4+
5+
Fixes restoring and submitting forms

frontend/packages/pydantic-forms/src/components/render/RenderForm.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* Here we define the outline of the form
77
*/
8-
import React, { useEffect } from 'react';
8+
import React from 'react';
99

1010
import { useTranslations } from 'next-intl';
1111

@@ -26,23 +26,10 @@ const RenderForm = (contextProps: PydanticFormContextProps) => {
2626
isSending,
2727
skipSuccessNotice,
2828
loadingComponent,
29-
rhf,
30-
errorDetails,
31-
formInputData,
3229
} = contextProps;
3330

3431
const { formRenderer, footerRenderer, headerRenderer } = config || {};
3532

36-
useEffect(() => {
37-
if (errorDetails) {
38-
// If we have an array restore the values
39-
const lastValues = [...formInputData].pop();
40-
rhf.reset({
41-
...lastValues,
42-
});
43-
}
44-
}, [errorDetails, formInputData]);
45-
4633
const pydanticFormComponents: PydanticFormComponents =
4734
getPydanticFormComponents(
4835
pydanticFormSchema?.properties || {},

frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx

Lines changed: 103 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
import {
3535
Locale,
3636
PydanticFormContextProps,
37+
PydanticFormFieldType,
3738
PydanticFormInitialContextProps,
3839
PydanticFormSchemaRawJson,
3940
PydanticFormValidationErrorDetails,
@@ -61,8 +62,6 @@ function PydanticFormContextProvider({
6162
labelProvider,
6263
customDataProvider,
6364
customDataProviderCacheKey,
64-
formStructureMutator,
65-
fieldDetailProvider,
6665
resetButtonAlternative,
6766
allowUntouchedSubmit,
6867
skipSuccessNotice,
@@ -78,15 +77,15 @@ function PydanticFormContextProvider({
7877

7978
const formRef = useRef<string>(formKey);
8079

81-
const updateHistory = async (
82-
formInput: object,
83-
previousSteps: object[],
84-
) => {
85-
const hashOfPreviousSteps = await getHashForArray(previousSteps);
86-
setFormInputHistory((prevState) =>
87-
prevState.set(hashOfPreviousSteps, formInput),
88-
);
89-
};
80+
const updateHistory = useCallback(
81+
async (formInput: object, previousSteps: object[]) => {
82+
const hashOfPreviousSteps = await getHashForArray(previousSteps);
83+
setFormInputHistory((prevState) =>
84+
prevState.set(hashOfPreviousSteps, formInput),
85+
);
86+
},
87+
[],
88+
);
9089

9190
const goToPreviousStep = (formInput: object) => {
9291
setFormInputData((prevState) => {
@@ -95,19 +94,6 @@ function PydanticFormContextProvider({
9594
});
9695
};
9796

98-
const addFormInputData = useCallback(
99-
(formInput: object, replaceInsteadOfAdd = false) => {
100-
setFormInputData((currentInputs) => {
101-
const data = replaceInsteadOfAdd
102-
? currentInputs.slice(0, -1)
103-
: currentInputs;
104-
updateHistory(formInput, data);
105-
return [...data, formInput];
106-
});
107-
},
108-
[],
109-
);
110-
11197
const [errorDetails, setErrorDetails] =
11298
useState<PydanticFormValidationErrorDetails>();
11399
const [isFullFilled, setIsFullFilled] = useState(false);
@@ -129,17 +115,21 @@ function PydanticFormContextProvider({
129115
error,
130116
} = useApiProvider(formKey, formInputData, apiProvider, metaData);
131117

132-
const [rawSchema, setRawSchema] = useState<PydanticFormSchemaRawJson>();
118+
const emptyRawSchema: PydanticFormSchemaRawJson = useMemo(
119+
() => ({
120+
type: PydanticFormFieldType.OBJECT,
121+
properties: {},
122+
}),
123+
[],
124+
);
125+
126+
const [rawSchema, setRawSchema] =
127+
useState<PydanticFormSchemaRawJson>(emptyRawSchema);
133128
const [hasNext, setHasNext] = useState<boolean>(false);
134129

135130
// extract the JSON schema to a more usable custom schema
136131
const { pydanticFormSchema, isLoading: isParsingSchema } =
137-
usePydanticFormParser(
138-
rawSchema,
139-
formLabels?.labels,
140-
fieldDetailProvider,
141-
formStructureMutator,
142-
);
132+
usePydanticFormParser(rawSchema, formLabels?.labels);
143133

144134
// build validation rules based on custom schema
145135
const zodSchema = useGetZodValidator(
@@ -169,17 +159,39 @@ function PydanticFormContextProvider({
169159
mode: 'all',
170160
defaultValues: initialData,
171161
values: initialData,
172-
shouldUnregister: true,
173162
});
174163

164+
const awaitReset = useCallback(
165+
async (payLoad: FieldValues = {}) => {
166+
rhf.reset(payLoad);
167+
await new Promise((resolve) => setTimeout(resolve, 0)); // wait one tick
168+
},
169+
[rhf],
170+
);
171+
172+
const addFormInputData = useCallback(
173+
(formInput: object, replaceInsteadOfAdd = false) => {
174+
setFormInputData((currentInputs) => {
175+
const data = replaceInsteadOfAdd
176+
? currentInputs.slice(0, -1)
177+
: currentInputs;
178+
updateHistory(formInput, data);
179+
return [...data, { ...formInput }];
180+
});
181+
awaitReset();
182+
},
183+
[awaitReset, setFormInputData, updateHistory],
184+
);
185+
175186
const submitFormFn = useCallback(() => {
176187
setIsSending(true);
177-
const rhfValues = rhf.getValues();
188+
const rhfValues = _.cloneDeep(rhf.getValues());
189+
awaitReset();
178190
// Note. If we don't use cloneDeep here we are adding a reference to the rhfValues
179191
// that changes on every change in the form and triggering effects before we want to.
180-
addFormInputData(_.cloneDeep(rhfValues), !!errorDetails);
192+
addFormInputData(rhfValues, !!errorDetails);
181193
window.scrollTo(0, 0);
182-
}, [rhf, errorDetails, addFormInputData]);
194+
}, [rhf, errorDetails, addFormInputData, awaitReset, setIsSending]);
183195

184196
const onClientSideError = useCallback(
185197
(data?: FieldValues) => {
@@ -198,10 +210,10 @@ function PydanticFormContextProvider({
198210
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
199211
e.preventDefault();
200212
setErrorDetails(undefined);
201-
rhf.reset();
213+
awaitReset();
202214
rhf.trigger();
203215
},
204-
[rhf],
216+
[awaitReset, rhf],
205217
);
206218

207219
const resetErrorDetails = useCallback(() => {
@@ -217,9 +229,9 @@ function PydanticFormContextProvider({
217229
const clearForm = useCallback(() => {
218230
setFormInputData([]);
219231
setIsFullFilled(false);
220-
setRawSchema(undefined);
232+
setRawSchema(emptyRawSchema);
221233
setHasNext(false);
222-
}, []);
234+
}, [emptyRawSchema]);
223235

224236
const PydanticFormContextState = {
225237
// to prevent an issue where the sending state hangs
@@ -253,40 +265,23 @@ function PydanticFormContextProvider({
253265
initialData,
254266
};
255267

256-
/** UseEffects */
257-
useEffect(() => {
258-
if (formKey !== formRef.current) {
259-
// When the formKey changes we need to reset the form input data
260-
setFormInputData([]);
261-
setFormInputHistory(new Map<string, object>());
262-
formRef.current = formKey;
263-
}
264-
}, [formKey]);
265-
266-
// handle successfull submits
268+
// a useeffect for whenever the error response updates
269+
// sometimes we need to update the form,
270+
// some we need to update the errors
267271
useEffect(() => {
268-
if (!isFullFilled) {
272+
if (!apiResponse) {
269273
return;
270274
}
271-
272-
if (onSuccess) {
273-
const values = rhf.getValues();
274-
if (skipSuccessNotice) {
275-
onSuccess(values, apiResponse || {});
276-
} else {
277-
setTimeout(() => {
278-
onSuccess?.(values, apiResponse || {});
279-
}, 1500); // Delay to allow notice to show first
280-
}
275+
// when we receive errors, we append to the scheme
276+
if (apiResponse?.validation_errors) {
277+
// Restore the data we got the error with
278+
const errorPayload = [...formInputData].pop();
279+
awaitReset(errorPayload);
280+
setErrorDetails(getErrorDetailsFromResponse(apiResponse));
281+
return;
281282
}
282283

283-
setFormInputHistory(new Map<string, object>());
284-
}, [apiResponse, isFullFilled, onSuccess, rhf, skipSuccessNotice]);
285-
286-
// a useeffect for whenever the error response updates
287-
// sometimes we need to update the form,
288-
// some we need to update the errors
289-
useEffect(() => {
284+
awaitReset();
290285
if (apiResponse?.success) {
291286
setIsFullFilled(true);
292287
return;
@@ -301,35 +296,52 @@ function PydanticFormContextProvider({
301296
setErrorDetails(undefined);
302297
}
303298

304-
// when we receive errors, we append to the scheme
305-
if (apiResponse?.validation_errors) {
306-
setErrorDetails(getErrorDetailsFromResponse(apiResponse));
307-
}
308-
309299
setIsSending(false);
310300
// eslint-disable-next-line react-hooks/exhaustive-deps
311301
}, [apiResponse]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls
312302
// a useeffect for filling data whenever formdefinition or labels update
313303

304+
// When a formKey changes we reset the form input data
314305
useEffect(() => {
315-
getHashForArray(formInputData).then((hash) => {
316-
const currentStepFromHistory = formInputHistory.get(hash);
306+
if (formKey !== formRef.current) {
307+
// When the formKey changes we need to reset the form input data
308+
setFormInputData([]);
309+
setFormInputHistory(new Map<string, object>());
310+
awaitReset();
311+
formRef.current = formKey;
312+
}
313+
}, [awaitReset, formKey]);
317314

318-
if (currentStepFromHistory) {
319-
rhf.reset(currentStepFromHistory);
315+
// handle successfull submits
316+
useEffect(() => {
317+
if (!isFullFilled) {
318+
return;
319+
}
320+
321+
if (onSuccess) {
322+
const values = rhf.getValues();
323+
if (skipSuccessNotice) {
324+
onSuccess(values, apiResponse || {});
325+
} else {
326+
setTimeout(() => {
327+
onSuccess?.(values, apiResponse || {});
328+
}, 1500); // Delay to allow notice to show first
320329
}
321-
});
322-
}, [formInputData, formInputHistory, rhf]);
330+
}
323331

324-
// this is to show an error whenever there is an unexpected error from the backend
325-
// for instance a 500
332+
setFormInputHistory(new Map<string, object>());
333+
// eslint-disable-next-line react-hooks/exhaustive-deps
334+
}, [apiResponse, isFullFilled]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls
335+
336+
// this handles errors throws by the useApiProvider call
337+
// for instance unexpected 500 errors
326338
useEffect(() => {
327339
if (!error) {
328340
return;
329341
}
330342

331343
setErrorDetails({
332-
detail: 'Er is iets misgegaan bij het verzenden.',
344+
detail: 'Something unexpected went wrong',
333345
source: [],
334346
mapped: {},
335347
});
@@ -350,6 +362,15 @@ function PydanticFormContextProvider({
350362
z.config(getLocale());
351363
}, [locale]);
352364

365+
useEffect(() => {
366+
getHashForArray(formInputData).then((hash) => {
367+
const currentStepFromHistory = formInputHistory.get(hash);
368+
if (currentStepFromHistory) {
369+
awaitReset(currentStepFromHistory);
370+
}
371+
});
372+
}, [awaitReset, formInputData, formInputHistory, rhf]);
373+
353374
return (
354375
<PydanticFormContext.Provider value={PydanticFormContextState}>
355376
{children(PydanticFormContextState)}

frontend/packages/pydantic-forms/src/core/hooks/useGetZodValidator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const getZodValidationObject = (
104104

105105
validationObject[id] = zodRule ?? z.any();
106106
});
107-
return z.object(validationObject);
107+
return validationObject ? z.object(validationObject) : z.any();
108108
};
109109

110110
export const useGetZodValidator = (

frontend/packages/pydantic-forms/src/core/hooks/usePydanticFormParser.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,8 @@ export const parseProperties = (
147147
};
148148

149149
export const usePydanticFormParser = (
150-
rawJsonSchema: PydanticFormSchemaRawJson | undefined,
150+
rawJsonSchema: PydanticFormSchemaRawJson,
151151
formLabels?: Record<string, string>,
152-
fieldDetailProvider?: PydanticFormsContextConfig['fieldDetailProvider'],
153-
formStructureMutator?: PydanticFormsContextConfig['formStructureMutator'],
154152
): {
155153
pydanticFormSchema: PydanticFormSchema | undefined;
156154
isLoading: boolean;
@@ -175,14 +173,11 @@ export const usePydanticFormParser = (
175173
parsedSchema.properties || {},
176174
parsedSchema.required || [],
177175
formLabels,
178-
fieldDetailProvider,
179176
),
180177
};
181178

182-
return formStructureMutator
183-
? formStructureMutator(pydanticFormSchema)
184-
: pydanticFormSchema;
185-
}, [formStructureMutator, parsedSchema, formLabels, fieldDetailProvider]);
179+
return pydanticFormSchema;
180+
}, [parsedSchema, formLabels]);
186181

187182
return {
188183
pydanticFormSchema,

0 commit comments

Comments
 (0)