fix(settings): one save model and one confirm per card

The Telegram, ntfy, webhook and email cards saved their enable switch on
tap and the rest of the form on Save; the switch is now part of the form
and the one Save sends it. The display selects confirm a change with a
toast. Destructive confirmations in Settings go through ConfirmButton or
ConfirmDialog with the destructive style. Two structural guards hold both
rules: a card may not mix instant and explicit saves, and no settings file
reaches for a raw AlertDialog without a written reason.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 19:42:53 +02:00
1 parent 93198017d6
commit 3ebef19ac3
32 files changed
+830 -1135

No files matched your search

@@ -27,6 +27,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ConfirmButton } from "@/components/ui/confirm-button";
import { Input } from "@/components/ui/input";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
@@ -311,46 +312,21 @@ export function PasskeyListSection({
>
<Pencil className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground min-h-11 min-w-11 sm:h-8 sm:min-h-0 sm:w-8 sm:min-w-0"
disabled={remove.isPending}
aria-label={t("settings.deletePasskey")}
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("settings.deletePasskey")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.deletePasskeyDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={remove.isPending}
aria-busy={remove.isPending || undefined}
onClick={() => remove.mutate(pk.id)}
>
{remove.isPending && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ConfirmButton
label=""
ariaLabel={t("settings.deletePasskey")}
title={t("settings.deletePasskey")}
body={t("settings.deletePasskeyDescription")}
confirmLabel={t("common.delete")}
onConfirm={() => remove.mutate(pk.id)}
pending={remove.isPending && remove.variables === pk.id}
disabled={remove.isPending}
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-foreground min-h-11 min-w-11 sm:h-8 sm:min-h-0 sm:w-8 sm:min-w-0"
icon={<Trash2 className="h-4 w-4" />}
destructive
/>
</div>
)}
</div>
@@ -13,18 +13,8 @@ import {
X,
} from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { ConfirmButton } from "@/components/ui/confirm-button";
import { Input } from "@/components/ui/input";
import { EmptyState } from "@/components/ui/empty-state";
import { SettingsCard } from "@/components/settings/settings-card";
@@ -308,46 +298,23 @@ export function SecurityKeysCard({
>
<Pencil className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground min-h-11 min-w-11 sm:h-8 sm:min-h-0 sm:w-8 sm:min-w-0"
disabled={remove.isPending}
aria-label={t("settings.security.keys.remove")}
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("settings.security.keys.remove")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.security.keys.removeConfirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("common.cancel")}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={remove.isPending}
aria-busy={remove.isPending || undefined}
onClick={() => remove.mutate(key.id)}
>
{remove.isPending && (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
)}
{t("common.delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ConfirmButton
label=""
ariaLabel={t("settings.security.keys.remove")}
title={t("settings.security.keys.remove")}
body={t("settings.security.keys.removeConfirm")}
confirmLabel={t("common.delete")}
onConfirm={() => remove.mutate(key.id)}
pending={
remove.isPending && remove.variables === key.id
}
disabled={remove.isPending}
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-foreground min-h-11 min-w-11 sm:h-8 sm:min-h-0 sm:w-8 sm:min-w-0"
icon={<Trash2 className="h-4 w-4" />}
destructive
/>
</div>
)}
</div>
@@ -498,6 +498,7 @@ export function TotpCard({
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
data-testid="recovery-regenerate-confirm"
disabled={regenerate.isPending}
aria-busy={regenerate.isPending || undefined}