feat(admin): delete button on request link

The request-links panel now has a trash button at the far right of
each row. Click it, the link is gone — cascadeDelete on
intake_link_submissions.link removes the daily counters in the same
transaction, and revalidatePath re-renders the panel without the row.

Hard delete, no confirmation: matches the existing Pause/Resume
pattern (no dialog there either). The icon is a Trash2 from
lucide-react; the title attribute is the only label, by design
owner-only surface.

Phase 9 shape: action takes a scope so revalidation hits the
client admin page the panel is mounted on.
This commit is contained in:
Dave 2026-09-21 14:17:55 +02:00
parent 3a721df094
commit 6cc7ae01f7
2 changed files with 36 additions and 1 deletions

View file

@ -114,6 +114,22 @@ export async function setIntakeLinkNotesAction(input: {
revalidatePath(adminPathForScope(input.scope)); revalidatePath(adminPathForScope(input.scope));
} }
/**
* Hard-delete a request link. Cascades to `intake_link_submissions` rows
* (those use `cascadeDelete: true` on their `link` field, so they go
* with it). The token stops working immediately because there's no row
* left to resolve against.
*/
export async function deleteIntakeLinkAction(input: {
linkId: string;
scope: IntakeLinkScope;
}): Promise<void> {
await requireOwner();
const pb = await getAdminPb();
await pb.collection("intake_links").delete(input.linkId);
revalidatePath(adminPathForScope(input.scope));
}
function intakeUrlFor(token: string): string { function intakeUrlFor(token: string): string {
const base = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; const base = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
return `${base}/intake/${token}`; return `${base}/intake/${token}`;

View file

@ -1,10 +1,11 @@
"use client"; "use client";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { Copy, RefreshCw, Power, PowerOff, Plus } from "lucide-react"; import { Copy, RefreshCw, Power, PowerOff, Plus, Trash2 } from "lucide-react";
import { import {
createIntakeLinkAction, createIntakeLinkAction,
deleteIntakeLinkAction,
rotateIntakeTokenAction, rotateIntakeTokenAction,
setIntakeLinkActiveAction, setIntakeLinkActiveAction,
setIntakeLinkNotesAction, setIntakeLinkNotesAction,
@ -100,6 +101,15 @@ export function IntakeLinksPanel({
}); });
} }
function remove(link: IntakeLinkRecord) {
startTransition(async () => {
await deleteIntakeLinkAction({
linkId: link.id,
scope,
});
});
}
const field = const field =
"w-full rounded-lg border border-border-subtle bg-surface px-3 py-2 text-xs text-text-secondary"; "w-full rounded-lg border border-border-subtle bg-surface px-3 py-2 text-xs text-text-secondary";
@ -220,6 +230,15 @@ export function IntakeLinksPanel({
{link.active ? <PowerOff size={12} strokeWidth={2} /> : <Power size={12} strokeWidth={2} />} {link.active ? <PowerOff size={12} strokeWidth={2} /> : <Power size={12} strokeWidth={2} />}
{link.active ? "Revoke" : "Re-activate"} {link.active ? "Revoke" : "Re-activate"}
</button> </button>
<button
type="button"
disabled={pending}
onClick={() => remove(link)}
className="inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-2 py-1 text-xs text-text-secondary hover:bg-surface-raised disabled:opacity-50 ml-auto"
title="Delete this link permanently"
>
<Trash2 size={12} strokeWidth={2} />
</button>
</div> </div>
<NotesEditor link={link} onSave={saveNotes} /> <NotesEditor link={link} onSave={saveNotes} />