row-delete-migration-button.tsx117 lines · main
1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useState } from 'react';
5
6interface Props {
7 requestId: string;
8}
9
10interface ErrorBody {
11 code?: string;
12 message?: string;
13}
14
15/**
16 * Compact trash-icon delete control for the migration request list row.
17 * Two-click: icon arms a confirm pill; second click sends the DELETE.
18 * Hard delete (api cascades audit-event rows).
19 */
20export function RowDeleteMigrationButton({ requestId }: Props) {
21 const router = useRouter();
22 const [confirming, setConfirming] = useState(false);
23 const [busy, setBusy] = useState(false);
24 const [error, setError] = useState<string | null>(null);
25
26 async function del(e: React.MouseEvent) {
27 e.preventDefault();
28 e.stopPropagation();
29 setBusy(true);
30 setError(null);
31 try {
32 const res = await fetch(`/api/v1/migration-requests/${requestId}`, {
33 method: 'DELETE',
34 credentials: 'include',
35 });
36 if (!res.ok) {
37 const body = (await res.json().catch(() => ({}))) as ErrorBody;
38 throw new Error(body.message ?? body.code ?? `http ${res.status}`);
39 }
40 router.refresh();
41 } catch (err) {
42 setError(err instanceof Error ? err.message : 'delete failed');
43 setBusy(false);
44 setConfirming(false);
45 }
46 }
47
48 if (!confirming) {
49 return (
50 <button
51 type="button"
52 aria-label={`delete migration request ${requestId}`}
53 title="delete migration request"
54 onClick={(e) => {
55 e.preventDefault();
56 e.stopPropagation();
57 setConfirming(true);
58 }}
59 className="rounded-md border border-transparent p-1.5 text-[var(--color-text-subtle)] transition hover:border-[var(--color-error)] hover:text-[var(--color-error)]"
60 >
61 <TrashIcon />
62 </button>
63 );
64 }
65
66 return (
67 <div className="flex items-center gap-1.5 rounded-md border border-[var(--color-error)] bg-[var(--color-bg)] px-1.5 py-1">
68 <span className="font-mono text-[10px] text-[var(--color-text)]">delete?</span>
69 <button
70 type="button"
71 disabled={busy}
72 onClick={del}
73 className="rounded-md bg-[var(--color-error)] px-1.5 py-0.5 font-mono text-[10px] text-[var(--color-text-inverse)] disabled:opacity-30"
74 >
75 {busy ? '…' : 'yes'}
76 </button>
77 <button
78 type="button"
79 disabled={busy}
80 onClick={(e) => {
81 e.preventDefault();
82 e.stopPropagation();
83 setConfirming(false);
84 setError(null);
85 }}
86 className="rounded-md border border-[var(--color-border)] px-1.5 py-0.5 font-mono text-[10px] text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
87 >
88 no
89 </button>
90 {error ? (
91 <span className="font-mono text-[10px] text-[var(--color-error)]">{error}</span>
92 ) : null}
93 </div>
94 );
95}
96
97function TrashIcon() {
98 return (
99 <svg
100 width="14"
101 height="14"
102 viewBox="0 0 24 24"
103 fill="none"
104 stroke="currentColor"
105 strokeWidth="2"
106 strokeLinecap="round"
107 strokeLinejoin="round"
108 aria-hidden
109 >
110 <polyline points="3 6 5 6 21 6" />
111 <path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6" />
112 <path d="M10 11v6" />
113 <path d="M14 11v6" />
114 <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
115 </svg>
116 );
117}