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:
@@ -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} />);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user