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
48 changes: 29 additions & 19 deletions app/components/form/FullPageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ interface FullPageFormProps<TFieldValues extends FieldValues> {
error?: Error
form: UseFormReturn<TFieldValues>
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<void>
/** Error from the API call */
submitError: ApiError | null
/**
Expand Down Expand Up @@ -53,22 +57,25 @@ export function FullPageForm<TFieldValues extends FieldValues>({
onSubmit,
submitError,
}: FullPageFormProps<TFieldValues>) {
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)
Expand All @@ -81,24 +88,27 @@ export function FullPageForm<TFieldValues extends FieldValues>({
<form
className="ox-form pb-20"
id={id}
onSubmit={(e) => {
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}
</form>

{blocker ? <ConfirmNavigation blocker={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 && <ConfirmNavigation blocker={blocker} />}

{actions && (
<PageActions>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function CreateInstanceForm() {
? await readBlobAsBase64(values.userData)
: undefined

createInstance.mutate({
await createInstance.mutateAsync({
query: projectSelector,
body: {
name: values.name,
Expand Down
2 changes: 1 addition & 1 deletion libs/api-mocks/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down