fix(web): client-component delete/unpair cards on accounts/[id]
The DialogTrigger asChild + transparent button overlay pattern wasn't
emitting a clickable button in the rendered DOM under radix-ui 1.4 +
Next 16 (server component context), so Delete and Unpair both became
no-ops. Replace each with a small client component that:
- holds open-state for the confirm Dialog
- drives the Card itself as the click target via role='button',
tabIndex, onClick, and Enter/Space keydown handlers
- calls the server action through useTransition
The Card stays a div (no <button> wrapping a Card → satisfies the
existing static-guard test). Removed the unused inline Dialog imports
and unpair/delete icons from the page.
Also trim the forgot-password dialog body to one sentence per request
('don't write too detail').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5d583d9194
commit
6759ca8131
104
apps/web/src/app/accounts/[id]/delete-account-card.tsx
Normal file
104
apps/web/src/app/accounts/[id]/delete-account-card.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { ChevronRightIcon, Loader2Icon, Trash2Icon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { deleteAccountAction } from "@/actions/accounts";
|
||||||
|
|
||||||
|
interface DeleteAccountCardProps {
|
||||||
|
accountId: string;
|
||||||
|
accountLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteAccountCard({
|
||||||
|
accountId,
|
||||||
|
accountLabel,
|
||||||
|
}: DeleteAccountCardProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [pending, start] = useTransition();
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
start(async () => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("accountId", accountId);
|
||||||
|
await deleteAccountAction(fd);
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<Card
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Delete account"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="transition-all hover:shadow-md hover:ring-destructive/30 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<CardContent className="flex items-center justify-between gap-4 py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-9 items-center justify-center rounded-lg bg-destructive/10">
|
||||||
|
<Trash2Icon className="size-4 text-destructive" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-destructive">
|
||||||
|
Delete Account
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Remove the account and all its reminders, groups, and history
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete this account permanently?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<strong>{accountLabel}</strong> will be removed along with its
|
||||||
|
synced groups, scheduled reminders, and all run history. This
|
||||||
|
cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter showCloseButton>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="ghost" size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={confirm}
|
||||||
|
>
|
||||||
|
{pending ? (
|
||||||
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2Icon className="size-4" />
|
||||||
|
)}
|
||||||
|
Yes, delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,6 @@ import Link from "next/link";
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
Trash2Icon,
|
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
SmartphoneIcon,
|
SmartphoneIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
@ -10,7 +9,6 @@ import {
|
|||||||
DatabaseIcon,
|
DatabaseIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
PowerIcon,
|
PowerIcon,
|
||||||
PowerOffIcon,
|
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -20,23 +18,12 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { AccountStatusBadge } from "@/components/account-status-badge";
|
import { AccountStatusBadge } from "@/components/account-status-badge";
|
||||||
import { getSeededOperator } from "@/lib/operator";
|
import { getSeededOperator } from "@/lib/operator";
|
||||||
import { getAccount } from "@/lib/queries";
|
import { getAccount } from "@/lib/queries";
|
||||||
import {
|
import { pairAccountAction } from "@/actions/accounts";
|
||||||
unpairAccountAction,
|
import { DeleteAccountCard } from "./delete-account-card";
|
||||||
pairAccountAction,
|
import { UnpairAccountCard } from "./unpair-account-card";
|
||||||
deleteAccountAction,
|
|
||||||
} from "@/actions/accounts";
|
|
||||||
|
|
||||||
interface AccountDetailPageProps {
|
interface AccountDetailPageProps {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
@ -156,110 +143,11 @@ export default async function AccountDetailPage({ params }: AccountDetailPagePro
|
|||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Unpair — transparent <button> overlay (sibling of Card,
|
<UnpairAccountCard accountId={account.id} accountLabel={account.label} />
|
||||||
inside a relative wrapper). Same pattern as Delete below. */}
|
|
||||||
<Dialog>
|
|
||||||
<div className="relative">
|
|
||||||
<Card className="transition-all hover:shadow-md hover:ring-amber-500/30 cursor-pointer">
|
|
||||||
<CardContent className="flex items-center justify-between gap-4 py-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex size-9 items-center justify-center rounded-lg bg-amber-500/10">
|
|
||||||
<PowerOffIcon className="size-4 text-amber-600 dark:text-amber-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">Unpair</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Disconnect from WhatsApp; keep the account so you can re-pair later
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Unpair WhatsApp"
|
|
||||||
className="absolute inset-0 w-full rounded-xl bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
||||||
/>
|
|
||||||
</DialogTrigger>
|
|
||||||
</div>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Unpair this account?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<strong>{account.label}</strong> will disconnect from WhatsApp and
|
|
||||||
scheduled reminders using it will stop firing until you re-pair.
|
|
||||||
The account itself is kept; reminders and other data are not deleted.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter showCloseButton>
|
|
||||||
<form action={unpairAccountAction}>
|
|
||||||
<input type="hidden" name="accountId" value={account.id} />
|
|
||||||
<Button type="submit" variant="default" size="sm">
|
|
||||||
<PowerOffIcon />
|
|
||||||
Yes, unpair
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete — transparent <button> overlay opens the dialog.
|
<DeleteAccountCard accountId={account.id} accountLabel={account.label} />
|
||||||
The button lives as a sibling of <Card> (inside a relative
|
|
||||||
wrapper) instead of inside the Card. Radix's asChild-driven
|
|
||||||
DialogTrigger stops emitting the underlying button when the
|
|
||||||
wrapper Card adds an `absolute inset-0` sibling on the same
|
|
||||||
stacking context, so we mirror the working pattern from the
|
|
||||||
Pair/Re-pair card above. */}
|
|
||||||
<Dialog>
|
|
||||||
<div className="relative">
|
|
||||||
<Card className="transition-all hover:shadow-md hover:ring-destructive/30 cursor-pointer">
|
|
||||||
<CardContent className="flex items-center justify-between gap-4 py-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex size-9 items-center justify-center rounded-lg bg-destructive/10">
|
|
||||||
<Trash2Icon className="size-4 text-destructive" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-destructive">Delete Account</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Remove the account and all its reminders, groups, and history
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Delete account"
|
|
||||||
className="absolute inset-0 w-full rounded-xl bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive focus-visible:ring-offset-2"
|
|
||||||
/>
|
|
||||||
</DialogTrigger>
|
|
||||||
</div>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Delete this account permanently?</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<strong>{account.label}</strong> will be removed along with its
|
|
||||||
synced groups, scheduled reminders, and all run history. This cannot be
|
|
||||||
undone.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter showCloseButton>
|
|
||||||
<form action={deleteAccountAction}>
|
|
||||||
<input type="hidden" name="accountId" value={account.id} />
|
|
||||||
<Button type="submit" variant="destructive" size="sm">
|
|
||||||
<Trash2Icon />
|
|
||||||
Yes, delete
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
102
apps/web/src/app/accounts/[id]/unpair-account-card.tsx
Normal file
102
apps/web/src/app/accounts/[id]/unpair-account-card.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { ChevronRightIcon, Loader2Icon, PowerOffIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { unpairAccountAction } from "@/actions/accounts";
|
||||||
|
|
||||||
|
interface UnpairAccountCardProps {
|
||||||
|
accountId: string;
|
||||||
|
accountLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnpairAccountCard({
|
||||||
|
accountId,
|
||||||
|
accountLabel,
|
||||||
|
}: UnpairAccountCardProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [pending, start] = useTransition();
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
start(async () => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("accountId", accountId);
|
||||||
|
await unpairAccountAction(fd);
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<Card
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Unpair WhatsApp"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="transition-all hover:shadow-md hover:ring-amber-500/30 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<CardContent className="flex items-center justify-between gap-4 py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex size-9 items-center justify-center rounded-lg bg-amber-500/10">
|
||||||
|
<PowerOffIcon className="size-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Unpair</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Disconnect from WhatsApp; keep the account so you can re-pair later
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRightIcon className="size-4 text-muted-foreground/60" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Unpair this account?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<strong>{accountLabel}</strong> will disconnect from WhatsApp and
|
||||||
|
scheduled reminders using it will stop firing until you re-pair.
|
||||||
|
The account itself is kept; reminders and other data are not
|
||||||
|
deleted.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter showCloseButton>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="ghost" size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
disabled={pending}
|
||||||
|
onClick={confirm}
|
||||||
|
>
|
||||||
|
{pending ? (
|
||||||
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<PowerOffIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
Yes, unpair
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -84,14 +84,7 @@ export function LoginFormClient({ next }: { next: string }) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Forgot your password?</DialogTitle>
|
<DialogTitle>Forgot your password?</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Self-service password reset is intentionally disabled on
|
Contact your administrator to reset it.
|
||||||
this deployment. To recover access, contact an
|
|
||||||
administrator. They can reset your password from
|
|
||||||
Settings → Users, or run{" "}
|
|
||||||
<code className="font-mono text-[0.75rem]">
|
|
||||||
./scripts/set-password.sh <username>
|
|
||||||
</code>{" "}
|
|
||||||
from the tools container.
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter showCloseButton>
|
<DialogFooter showCloseButton>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user