fix(site): reflect submitting state during batch update (#27630)

> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

## Problem

When bulk updating workspaces, the confirmation modal's **Update**
button never entered a submitting/loading state, so there was no
feedback that the update was actually in progress.

## Root cause

The `BatchUpdateModalForm` shows a spinner when its `isProcessing` prop
is `true`. That prop is fed by `batchActions.isProcessing` from
`useBatchActions`. However, the `isProcessing` value was OR-ing together
every mutation's `isPending` flag **except** `updateAllMutation` — the
one that actually performs the batch update:

```ts
isProcessing:
  favoriteAllMutation.isPending ||
  unfavoriteAllMutation.isPending ||
  startAllMutation.isPending ||
  stopAllMutation.isPending ||
  deleteAllMutation.isPending,
  // updateAllMutation.isPending was missing
```

As a result, the button stayed idle for the entire duration of a bulk
update.

## Fix

Include `updateAllMutation.isPending` in the `isProcessing` derivation
so the button spinner and disabled state correctly reflect an in-flight
batch update.

## Testing

- [ ] Manually verify the Update button shows the spinner and is
disabled while a bulk update runs.
This commit is contained in:
Jake Howell
2026-07-30 00:46:33 +10:00
committed by GitHub
parent 4bf9b9d1e6
commit 0b93731ebf
2 changed files with 59 additions and 2 deletions
@@ -15,7 +15,11 @@ import {
templateVersionsQueryKey,
} from "#/api/queries/templates";
import { workspacesKey } from "#/api/queries/workspaces";
import type { Workspace, WorkspaceAppHealth } from "#/api/typesGenerated";
import type {
Workspace,
WorkspaceAppHealth,
WorkspaceBuild,
} from "#/api/typesGenerated";
import { workspaceChecks } from "#/modules/workspaces/permissions";
import {
MockDefaultOrganization,
@@ -373,6 +377,58 @@ export const BatchUpdateRunningWorkspace: Story = {
},
};
export const BatchUpdateShowsSubmittingState: Story = {
beforeEach: () => {
spyOn(API, "getWorkspaces").mockResolvedValue({
workspaces: updateRunningWorkspaces,
count: updateRunningWorkspaces.length,
});
spyOn(API, "getTemplateVersion").mockResolvedValue(MockTemplateVersion);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const body = within(document.body);
const user = userEvent.setup();
// Hold the mutation pending so the button's submitting state stays
// observable until the story explicitly resolves it.
let resolveUpdate!: () => void;
const pendingUpdate = new Promise<WorkspaceBuild>((resolve) => {
resolveUpdate = () => resolve(MockWorkspaceBuild);
});
spyOn(API, "updateWorkspace").mockReturnValue(pendingUpdate);
await selectWorkspaces(canvas, user, ["1", "2", "3"]);
await openBulkActions(canvas, user);
await user.click(await body.findByRole("menuitem", { name: /Update/ }));
const modal = await body.findByRole("dialog", { name: /Review Updates/i });
await user.click(
within(modal).getByRole("checkbox", {
name: /I acknowledge these risks\./,
}),
);
const updateButton = within(modal).getByRole("button", { name: /Update/ });
await user.click(updateButton);
await step("Button reflects the submitting state", async () => {
await waitFor(() => expect(updateButton).toBeDisabled());
await within(modal).findByText(
"Waiting for workspaces to finish processing",
);
});
await step("Resolving the update clears the submitting state", async () => {
resolveUpdate();
await waitFor(() =>
expect(
body.queryByText("Waiting for workspaces to finish processing"),
).not.toBeInTheDocument(),
);
});
},
};
const ignoreDormantWorkspaces: Workspace[] = [
{ ...MockDormantOutdatedWorkspace, id: "1" },
{ ...MockOutdatedWorkspace, id: "2" },
@@ -141,6 +141,7 @@ export function useBatchActions(
unfavoriteAllMutation.isPending ||
startAllMutation.isPending ||
stopAllMutation.isPending ||
deleteAllMutation.isPending,
deleteAllMutation.isPending ||
updateAllMutation.isPending,
};
}