fix(web): disable delete confirms while pending + Signing in… feedback (#70)

The delete dialogs (DeleteObjectDialog and the shared
DeleteConfirmDialog) left their confirm button enabled during the
in-flight request, so a double-click fired a second DELETE that 404'd
and surfaced a spurious error. Disable cancel + confirm while pending
and swap the confirm label to a new actions.deleting ("Deleting…" /
"Tar bort…").

The login button disabled itself during login.isPending but kept the
"Sign in" label; it now shows auth.signingIn ("Signing in…" /
"Loggar in…") so slow networks get visible feedback.

Each fix is covered by a gated-MSW (or gated-promise) test asserting
the pending label + disabled state before releasing the request.

Closes #70

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:35:27 +02:00
parent 091a1a651d
commit 27205c65ef
8 changed files with 95 additions and 10 deletions
@@ -20,6 +20,31 @@ test("delete-in-use shows the in-use count and keeps the dialog open", async ()
expect(dialog.getByText("Delete this term?")).toBeInTheDocument();
});
test("confirm is disabled and labelled Deleting… while pending", async () => {
let resolve!: () => void;
const onConfirm = vi.fn(
() =>
new Promise<void>((r) => {
resolve = r;
}),
);
renderApp(<DeleteConfirmDialog description="Delete this term?" onConfirm={onConfirm} />);
await userEvent.click(screen.getByRole("button", { name: /delete/i }));
const dialog = within(document.body);
const buttons = await dialog.findAllByRole("button", { name: /delete/i });
await userEvent.click(buttons[buttons.length - 1]);
const pending = await dialog.findByRole("button", { name: /deleting/i });
expect(pending).toBeDisabled();
expect(dialog.getByRole("button", { name: /cancel/i })).toBeDisabled();
expect(onConfirm).toHaveBeenCalledTimes(1);
resolve();
await waitFor(() => expect(dialog.queryByText("Delete this term?")).toBeNull());
});
test("a clean confirm closes the dialog", async () => {
const onConfirm = vi.fn(() => Promise.resolve());
renderApp(<DeleteConfirmDialog description="Delete this term?" onConfirm={onConfirm} />);
+8 -2
View File
@@ -28,10 +28,12 @@ export function DeleteConfirmDialog({
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [pending, setPending] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const confirm = async () => {
setMessage(null);
setPending(true);
try {
await onConfirm();
} catch (err) {
@@ -40,6 +42,8 @@ export function DeleteConfirmDialog({
const { key, opts } = errorMessageKey(err);
setMessage(t(key, opts));
return;
} finally {
setPending(false);
}
setOpen(false);
};
@@ -62,8 +66,10 @@ export function DeleteConfirmDialog({
</p>
)}
<AlertDialogFooter>
<AlertDialogCancel>{t("form.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={confirm}>{t("actions.delete")}</AlertDialogAction>
<AlertDialogCancel disabled={pending}>{t("form.cancel")}</AlertDialogCancel>
<AlertDialogAction disabled={pending} onClick={confirm}>
{pending ? t("actions.deleting") : t("actions.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>