feat(web): delete reminder server action wired to detail page

This commit is contained in:
yiekheng 2026-05-09 23:46:23 +08:00
parent 83a19d4800
commit 6916f5a0ed
3 changed files with 56 additions and 29 deletions

View File

@ -0,0 +1,42 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { eq } from "drizzle-orm";
import { reminders } from "@cmbot/db";
import { db } from "@/lib/db";
import { getSeededOperator } from "@/lib/operator";
import { checkRateLimit } from "@/lib/rate-limit";
async function rateLimit(key: string) {
const h = await headers();
const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? h.get("x-real-ip") ?? "unknown";
const r = await checkRateLimit(`${key}:${ip}`, { max: 30, windowSec: 10 });
if (r.limited) throw new Error("Too many requests");
}
export async function deleteReminderAction(formData: FormData): Promise<void> {
await rateLimit("delete-reminder");
const reminderId = formData.get("reminderId");
if (typeof reminderId !== "string") return;
const op = await getSeededOperator();
const reminder = await db.query.reminders.findFirst({
where: (r, { eq }) => eq(r.id, reminderId),
});
if (!reminder) return;
// Verify ownership via the account
const account = await db.query.whatsappAccounts.findFirst({
where: (a, { eq, and }) => and(eq(a.id, reminder.accountId), eq(a.operatorId, op.id)),
});
if (!account) return;
// Cascading FKs (reminder_runs + reminder_targets + reminder_messages) clean up.
// pg-boss job for this reminder will fire and find the row gone (soft cancel).
await db.delete(reminders).where(eq(reminders.id, reminderId));
revalidatePath("/reminders" as any);
redirect("/reminders" as any);
}

View File

@ -12,24 +12,14 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { deleteReminderAction } from "@/actions/reminders";
interface DeleteDialogProps { interface DeleteDialogProps {
deleteAction: () => Promise<void>; reminderId: string;
} }
export function DeleteDialog({ deleteAction }: DeleteDialogProps) { export function DeleteDialog({ reminderId }: DeleteDialogProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [pending, setPending] = useState(false);
async function handleConfirm() {
setPending(true);
try {
await deleteAction();
} finally {
setPending(false);
setOpen(false);
}
}
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
@ -48,14 +38,16 @@ export function DeleteDialog({ deleteAction }: DeleteDialogProps) {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<form action={deleteReminderAction} className="flex gap-2">
<input type="hidden" name="reminderId" value={reminderId} />
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
disabled={pending} type="submit"
onClick={handleConfirm}
> >
{pending ? "Deleting…" : "Delete"} Delete
</Button> </Button>
</form>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -73,13 +73,6 @@ function StatusPill({ status }: { status: string }) {
); );
} }
// ---------------------------------------------------------------------------
// Server action (no-op placeholder — real delete wired in Task 19)
// ---------------------------------------------------------------------------
async function _deleteReminderStub() {
"use server";
// wired in Task 19
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Page // Page
@ -242,7 +235,7 @@ export default async function ReminderDetailPage({ params }: Props) {
{/* Action footer */} {/* Action footer */}
<div className="flex items-center justify-end pt-2 border-t"> <div className="flex items-center justify-end pt-2 border-t">
<DeleteDialog deleteAction={_deleteReminderStub} /> <DeleteDialog reminderId={reminder.id} />
</div> </div>
</div> </div>
); );