77 */
88import { cloneElement , useEffect , type ReactElement , type ReactNode } from 'react'
99import type { FieldValues , UseFormReturn } from 'react-hook-form'
10- import { useBlocker , type unstable_Blocker as Blocker } from 'react-router-dom'
10+ import { useBlocker , type Blocker } from 'react-router-dom'
1111
1212import type { ApiError } from '@oxide/api'
1313import { Modal , PageHeader , PageTitle } from '@oxide/ui'
@@ -25,7 +25,11 @@ interface FullPageFormProps<TFieldValues extends FieldValues> {
2525 error ?: Error
2626 form : UseFormReturn < TFieldValues >
2727 loading ?: boolean
28- onSubmit : ( values : TFieldValues ) => void
28+ /**
29+ * Use await mutateAsync(), otherwise you'll break the logic below that relies
30+ * on knowing when the submit is done.
31+ */
32+ onSubmit : ( values : TFieldValues ) => Promise < void >
2933 /** Error from the API call */
3034 submitError : ApiError | null
3135 /**
@@ -53,22 +57,25 @@ export function FullPageForm<TFieldValues extends FieldValues>({
5357 onSubmit,
5458 submitError,
5559} : FullPageFormProps < TFieldValues > ) {
56- const { isSubmitting, isDirty } = form . formState
60+ const { isSubmitting, isDirty, isSubmitSuccessful } = form . formState
5761
58- /*
59- Confirms with the user if they want to navigate away
60- if the form is dirty. Does not intercept everything e.g.
61- refreshes or closing the tab but serves to reduce
62- the possibility of a user accidentally losing their progress
63- */
64- const blocker = useBlocker ( isDirty )
62+ // Confirms with the user if they want to navigate away if the form is
63+ // dirty. Does not intercept everything e.g. refreshes or closing the tab
64+ // but serves to reduce the possibility of a user accidentally losing their
65+ // progress.
66+ const blocker = useBlocker ( isDirty && ! isSubmitSuccessful )
6567
66- // Reset blocker if form is no longer dirty
68+ // Gating on !isSubmitSuccessful above makes the blocker stop blocking nav
69+ // after a successful submit. However, this can take a little time (there is a
70+ // render in between when isSubmitSuccessful is true but the blocker is still
71+ // ready to block), so we also have this useEffect that lets blocked requests
72+ // through if submit is succesful but the blocker hasn't gotten a chance to
73+ // stop blocking yet.
6774 useEffect ( ( ) => {
68- if ( blocker . state === 'blocked' && ! isDirty ) {
69- blocker . reset ( )
75+ if ( blocker . state === 'blocked' && isSubmitSuccessful ) {
76+ blocker . proceed ( )
7077 }
71- } , [ blocker , isDirty ] )
78+ } , [ blocker , isSubmitSuccessful ] )
7279
7380 const childArray = flattenChildren ( children )
7481 const actions = pluckFirstOfType ( childArray , Form . Actions )
@@ -81,24 +88,27 @@ export function FullPageForm<TFieldValues extends FieldValues>({
8188 < form
8289 className = "ox-form pb-20"
8390 id = { id }
84- onSubmit = { ( e ) => {
91+ onSubmit = { async ( e ) => {
8592 // This modal being in a portal doesn't prevent the submit event
8693 // from bubbling up out of the portal. Normally that's not a
8794 // problem, but sometimes (e.g., instance create) we render the
8895 // SideModalForm from inside another form, in which case submitting
8996 // the inner form submits the outer form unless we stop propagation
9097 e . stopPropagation ( )
91- // This resets `isDirty` whilst keeping the values meaning
92- // we are not prevented from navigating away by the blocker
93- form . reset ( { } as TFieldValues , { keepValues : true } )
94- form . handleSubmit ( onSubmit ) ( e )
98+ // Important to await here so isSubmitSuccessful doesn't become true
99+ // until the submit is actually successful. Note you must use await
100+ // mutateAsync() inside onSubmit in order to make this wait
101+ await form . handleSubmit ( onSubmit ) ( e )
95102 } }
96103 autoComplete = "off"
97104 >
98105 { childArray }
99106 </ form >
100107
101- { blocker ? < ConfirmNavigation blocker = { blocker } /> : null }
108+ { /* rendering of the modal must be gated on isSubmitSuccessful because
109+ there is a brief moment where isSubmitSuccessful is true but the proceed()
110+ hasn't fired yet, which means we get a brief flash of this modal */ }
111+ { ! isSubmitSuccessful && < ConfirmNavigation blocker = { blocker } /> }
102112
103113 { actions && (
104114 < PageActions >
0 commit comments