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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const RenderForm = (contextProps: PydanticFormContextProps) => {
}

if (isFullFilled) {
return <div>{t('successfullySent')}</div>;
return <></>;
}

const FormRenderer = formRenderer ?? Form;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,20 @@
new Map<string, object>(),
);
const [formInputData, setFormInputData] = useState<object[]>([]);

const formRef = useRef<string>(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<PydanticFormValidationErrorDetails>();

Expand Down Expand Up @@ -166,58 +160,49 @@

/*
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) => {
let resetPayload = {};

if (payLoad) {
resetPayload = { ...payLoad };
} else {
const currentStepFromHistory = formInputHistory.get(hash);

if (currentStepFromHistory) {
resetPayload = { ...currentStepFromHistory };
}
}
reactHookForm.reset(resetPayload);
});
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<void>((resolve) => {
setTimeout(() => {
resolve();
}, 0);
});
} catch (error) {
console.error('Failed to reactHookFOrm', error);
}
},

[formInputData, formInputHistory, reactHookForm],
[reactHookForm],
);

const addFormInputData = useCallback(
(formInput: object, replaceInsteadOfAdd = false) => {
setFormInputData((currentInputs) => {
const data = replaceInsteadOfAdd
? currentInputs.slice(0, -1)
: currentInputs;
updateHistory(formInput, data);
return [...data, { ...formInput }];
});
const addFormInputData = useCallback(() => {
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(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();
},
[awaitReset, setFormInputData, updateHistory],
);
return [...currentFormInputData, { ...reactHookFormValues }];
});
}, []);

Check warning on line 198 in frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx

View workflow job for this annotation

GitHub Actions / linting-and-prettier

React Hook useCallback has missing dependencies: 'awaitReset', 'reactHookForm', and 'updateHistory'. Either include them or remove the dependency array

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);
setErrorDetails(undefined);
addFormInputData();
window.scrollTo(0, 0);
}, [
reactHookForm,
errorDetails,
addFormInputData,
awaitReset,
setIsSending,
]);
}, []);

Check warning on line 205 in frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx

View workflow job for this annotation

GitHub Actions / linting-and-prettier

React Hook useCallback has a missing dependency: 'addFormInputData'. Either include it or remove the dependency array

const onClientSideError = useCallback(
(data?: FieldValues) => {
Expand All @@ -230,6 +215,16 @@
[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,
Expand All @@ -245,23 +240,12 @@
[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<Map<string, Map<string, unknown>>>(
new Map(),
);
Expand Down Expand Up @@ -299,9 +283,6 @@
);

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,
Expand All @@ -314,74 +295,90 @@
isLoading,
isSending: isSending && isLoadingSchema,
onCancel,
onPrevious: () => goToPreviousStep(reactHookForm?.getValues()),
onPrevious: () => goToPreviousStep(),
pydanticFormSchema,
reactHookForm,
resetErrorDetails,
resetForm,
submitForm,
title,
};

// 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();
awaitReset(errorPayload);
// Restore the data we got the error with and remove it from
// formInputData so we can add it again
setFormInputData((currentData) => {
const nextData = [...currentData];
const errorPayload = nextData.pop();
awaitReset(errorPayload);
return nextData;
});

setErrorDetails(getErrorDetailsFromResponse(apiResponse));
return;
}

awaitReset();
if (apiResponse?.success) {
setIsFullFilled(true);
return;
}

// when we receive a new form from JSON, we fully reset the form
if (apiResponse?.form && rawSchema !== apiResponse.form) {
setRawSchema(apiResponse.form);
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([]);
setFormInputHistory(new Map<string, object>());
awaitReset({});
formRef.current = formKey;
}
}, [awaitReset, formKey]);
}, [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<string, object>());
// 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
// useEffect to handles errors throws by the useApiProvider call
// for instance unexpected 500 errors
useEffect(() => {
if (!error) {
Expand All @@ -395,7 +392,7 @@
});
}, [error]);

// UseEffect to handle locale change
// useEffect to handle locale change
useEffect(() => {
const getLocale = () => {
switch (locale) {
Expand Down