Skip to content

Commit 4464677

Browse files
authored
Don't reset submit button loading state right before nav (#3345)
Noticed while doing #3344. Form submit button loading state is true only when the mutation `isPending`. But there's a brief moment after the mutation completes but before the success nav happens where that's false and the button is on screen. What that looks like is the button flipping back to clickable right before the nav. That's stupid, so I gave every button where it makes sense `loading={isPending || isSuccess}`. There's some risk we could have a form where that is not appropriate (and there are a couple already — see `createNic` on the instance networking tab) but it's very easy for agents to get it right. I added a note to AGENTS.md about it. ### Before https://github.com/user-attachments/assets/fc4aadf6-7ba2-4e68-af01-be2a4dda5e2f ### After https://github.com/user-attachments/assets/7dd7d2de-5f34-48be-821b-58e2bb8f767a
1 parent 9e4d320 commit 4464677

44 files changed

Lines changed: 60 additions & 50 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Before starting a feature, skim an existing page or form with similar behavior and mirror the conventions—this codebase is intentionally conventional. Look for similar pages in `app/pages` and forms in `app/forms` to use as templates.
66
- `@oxide/api` is at `app/api` and `@oxide/api-mocks` is at `mock-api/index.ts`.
77
- The language server often has out of date errors. TypeScript 7 is extremely fast, so confirm errors that come from the language server by running `npm run tsc`
8+
- This repo uses oxfmt and oxlint, not prettier or eslint
89
- Use Node.js 22+, then install deps and start the mock-backed dev server (skip if `npm run dev` is already running in another terminal):
910

1011
```sh
@@ -49,6 +50,7 @@
4950
# Mutations & UI flow
5051

5152
- Wrap writes in `useApiMutation`, use `confirmAction` to guard destructive intent, and surface results with `addToast`.
53+
- When a form's `onSuccess` always navigates away, pass `loading={mutation.isPending || mutation.isSuccess}` to the form shell. `isPending` alone flips false before the navigation unmounts the modal, so the button's spinner animates back out right before close. Skip `isSuccess` if the form can stay open and be reused after success, or if the mutation lives in a component that survives the modal (e.g., a tab page with `{open && <Modal/>}`) — there success closes the modal synchronously so `isPending` alone is glitch-free, and a sticky `isSuccess` would strand a spinner on next open.
5254
- Mutation error display depends on context. In forms, errors display inline via `submitError={mutation.error}` — do not add `onError` with a toast to the `useApiMutation` call. In `confirmAction`/`confirmDelete` flows, the confirm modal catches the error and shows a toast using `errorTitle` — do not also add `onError` on the mutation, or the user will see two toasts. For standalone actions (fire-and-forget `mutate` calls not wrapped in a confirm modal or form), use `onError` on the mutation to show an error toast.
5355
- Keep page scaffolding consistent: `PageHeader`, `PageTitle`, `DocsPopover`, `RefreshButton`, `PropertiesTable`, and `CardBlock` provide the expected layout for new system pages.
5456
- When a page should be discoverable from the command palette, extend `useQuickActions` with the new entry so it appears in the quick actions menu (see `app/pages/ProjectsPage.tsx:100-115`).

app/components/AttachEphemeralIpModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const AttachEphemeralIpModal = ({
8585
submitLabel="Attach"
8686
submitDisabled={submitDisabled}
8787
submitError={instanceEphemeralIpAttach.error}
88-
loading={instanceEphemeralIpAttach.isPending}
88+
loading={instanceEphemeralIpAttach.isPending || instanceEphemeralIpAttach.isSuccess}
8989
onSubmit={({ pool }) => {
9090
instanceEphemeralIpAttach.mutate({
9191
path: { instance },

app/components/AttachFloatingIpModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const AttachFloatingIpModal = ({
9191
onDismiss={onDismiss}
9292
submitLabel="Attach floating IP"
9393
submitError={floatingIpAttach.error}
94-
loading={floatingIpAttach.isPending}
94+
loading={floatingIpAttach.isPending || floatingIpAttach.isSuccess}
9595
title="Attach floating IP"
9696
onSubmit={() =>
9797
floatingIpAttach.mutate({

app/forms/anti-affinity-group-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function CreateAntiAffinityGroupForm() {
6262
body: { ...values, failureDomain: 'sled' },
6363
})
6464
}
65-
loading={createAntiAffinityGroup.isPending}
65+
loading={createAntiAffinityGroup.isPending || createAntiAffinityGroup.isSuccess}
6666
submitError={createAntiAffinityGroup.error}
6767
submitLabel="Add group"
6868
>

app/forms/anti-affinity-group-edit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default function EditAntiAffintyGroupForm() {
7777
body: values,
7878
})
7979
}}
80-
loading={editAntiAffinityGroup.isPending}
80+
loading={editAntiAffinityGroup.isPending || editAntiAffinityGroup.isSuccess}
8181
submitError={editAntiAffinityGroup.error}
8282
submitLabel="Edit group"
8383
>

app/forms/disk-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export function CreateDiskSideModalForm({
191191
createDisk.mutate({ query: { project }, body })
192192
}
193193
}}
194-
loading={createDisk.isPending}
194+
loading={createDisk.isPending || createDisk.isSuccess}
195195
submitError={createDisk.error}
196196
>
197197
<NameField

app/forms/external-subnet-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default function CreateExternalSubnetSideModalForm() {
107107
body: { name, description, allocator },
108108
})
109109
}}
110-
loading={createExternalSubnet.isPending}
110+
loading={createExternalSubnet.isPending || createExternalSubnet.isSuccess}
111111
submitError={createExternalSubnet.error}
112112
>
113113
<NameField name="name" control={form.control} />

app/forms/external-subnet-edit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default function EditExternalSubnetSideModalForm() {
9898
body: { name, description },
9999
})
100100
}}
101-
loading={editExternalSubnet.isPending}
101+
loading={editExternalSubnet.isPending || editExternalSubnet.isSuccess}
102102
submitError={editExternalSubnet.error}
103103
>
104104
<FormMetadata resource={subnet}>

app/forms/firewall-rules-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export default function CreateFirewallRuleForm() {
122122
},
123123
})
124124
}}
125-
loading={updateRules.isPending}
125+
loading={updateRules.isPending || updateRules.isSuccess}
126126
submitError={updateRules.error}
127127
submitLabel="Add rule"
128128
>

app/forms/firewall-rules-edit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default function EditFirewallRuleForm() {
132132
}
133133
// validationSchema={validationSchema}
134134
// validateOnBlur
135-
loading={updateRules.isPending}
135+
loading={updateRules.isPending || updateRules.isSuccess}
136136
submitError={updateRules.error}
137137
>
138138
<FormMetadata resource={originalRule} />

0 commit comments

Comments
 (0)