diff --git a/app/components/form/FullPageForm.tsx b/app/components/form/FullPageForm.tsx index e673af7a5b..2c388b92f3 100644 --- a/app/components/form/FullPageForm.tsx +++ b/app/components/form/FullPageForm.tsx @@ -25,7 +25,11 @@ interface FullPageFormProps { error?: Error form: UseFormReturn loading?: boolean - onSubmit: (values: TFieldValues) => void + /** + * Use await mutateAsync(), otherwise you'll break the logic below that relies + * on knowing when the submit is done. + */ + onSubmit: (values: TFieldValues) => Promise /** Error from the API call */ submitError: ApiError | null /** @@ -53,22 +57,25 @@ export function FullPageForm({ onSubmit, submitError, }: FullPageFormProps) { - const { isSubmitting, isDirty } = form.formState + const { isSubmitting, isDirty, isSubmitSuccessful } = form.formState - /* - Confirms with the user if they want to navigate away - if the form is dirty. Does not intercept everything e.g. - refreshes or closing the tab but serves to reduce - the possibility of a user accidentally losing their progress - */ - const blocker = useBlocker(isDirty) + // Confirms with the user if they want to navigate away if the form is + // dirty. Does not intercept everything e.g. refreshes or closing the tab + // but serves to reduce the possibility of a user accidentally losing their + // progress. + const blocker = useBlocker(isDirty && !isSubmitSuccessful) - // Reset blocker if form is no longer dirty + // Gating on !isSubmitSuccessful above makes the blocker stop blocking nav + // after a successful submit. However, this can take a little time (there is a + // render in between when isSubmitSuccessful is true but the blocker is still + // ready to block), so we also have this useEffect that lets blocked requests + // through if submit is succesful but the blocker hasn't gotten a chance to + // stop blocking yet. useEffect(() => { - if (blocker.state === 'blocked' && !isDirty) { - blocker.reset() + if (blocker.state === 'blocked' && isSubmitSuccessful) { + blocker.proceed() } - }, [blocker, isDirty]) + }, [blocker, isSubmitSuccessful]) const childArray = flattenChildren(children) const actions = pluckFirstOfType(childArray, Form.Actions) @@ -81,24 +88,27 @@ export function FullPageForm({
{ + onSubmit={async (e) => { // This modal being in a portal doesn't prevent the submit event // from bubbling up out of the portal. Normally that's not a // problem, but sometimes (e.g., instance create) we render the // SideModalForm from inside another form, in which case submitting // the inner form submits the outer form unless we stop propagation e.stopPropagation() - // This resets `isDirty` whilst keeping the values meaning - // we are not prevented from navigating away by the blocker - form.reset({} as TFieldValues, { keepValues: true }) - form.handleSubmit(onSubmit)(e) + // Important to await here so isSubmitSuccessful doesn't become true + // until the submit is actually successful. Note you must use await + // mutateAsync() inside onSubmit in order to make this wait + await form.handleSubmit(onSubmit)(e) }} autoComplete="off" > {childArray}
- {blocker ? : null} + {/* rendering of the modal must be gated on isSubmitSuccessful because + there is a brief moment where isSubmitSuccessful is true but the proceed() + hasn't fired yet, which means we get a brief flash of this modal */} + {!isSubmitSuccessful && } {actions && ( diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index cec3644edc..e3d5b4bec5 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -188,7 +188,7 @@ export function CreateInstanceForm() { ? await readBlobAsBase64(values.userData) : undefined - createInstance.mutate({ + await createInstance.mutateAsync({ query: projectSelector, body: { name: values.name, diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 4e0efd6221..ab5fbf4984 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -289,7 +289,7 @@ export const handlers = makeHandlers({ const instances = db.instances.filter((i) => i.project_id === project.id) return paginated(query, instances) }, - instanceCreate({ body, query }) { + async instanceCreate({ body, query }) { const project = lookup.project(query) if (body.name === 'no-default-pool') { diff --git a/package-lock.json b/package-lock.json index 8ffcc52078..0d34467d37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "react-aria": "^3.31.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.12", - "react-hook-form": "^7.47.0", + "react-hook-form": "^7.50.1", "react-is": "^18.2.0", "react-merge-refs": "^2.1.1", "react-router-dom": "^6.21.1", @@ -17918,9 +17918,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.47.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz", - "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", + "version": "7.50.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz", + "integrity": "sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==", "engines": { "node": ">=12.22.0" }, @@ -34226,9 +34226,9 @@ } }, "react-hook-form": { - "version": "7.47.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz", - "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", + "version": "7.50.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz", + "integrity": "sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==", "requires": {} }, "react-hotkeys-hook": { diff --git a/package.json b/package.json index 61d1b4635b..5bd31ed9cc 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "react-aria": "^3.31.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.12", - "react-hook-form": "^7.47.0", + "react-hook-form": "^7.50.1", "react-is": "^18.2.0", "react-merge-refs": "^2.1.1", "react-router-dom": "^6.21.1",