feat: delete rows via trash icon in all tables

Each row gets a trash can button that appears on hover. Clicking it
shows an inline confirm (✓ / ✕) to prevent accidental deletion. On
confirm, calls DELETE /endpoint/:id and removes the row from state.
If the deleted record is open in the modal, the modal closes too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 19:24:37 +02:00
parent 211bd464d2
commit 6c8eaab034

View File

@@ -1,9 +1,9 @@
import { useEffect, useState, useMemo } from 'react';
import { useEffect, useState, useMemo, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import RecordModal from '../components/RecordModal';
import CreateModal, { hasCreateForm } from '../components/CreateModal';
import { fetchAll } from '../lib/api';
import { fetchAll, apiDelete } from '../lib/api';
import { TABLES, STATUS_COLORS } from '../lib/tables';
function truncate(str, n = 60) {
@@ -88,6 +88,8 @@ export default function TableView() {
const [highlightId, setHighlightId] = useState(null);
const [modalRecord, setModalRecord] = useState(null);
const [showCreate, setShowCreate] = useState(false);
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
@@ -130,6 +132,21 @@ export default function TableView() {
setRows(prev => [newRecord, ...prev]);
}
const handleDelete = useCallback(async (id, e) => {
e.stopPropagation();
setDeleting(true);
try {
await apiDelete(meta.endpoint, id);
setRows(prev => prev.filter(r => r.id !== id));
setConfirmDeleteId(null);
if (modalRecord?.id === id) setModalRecord(null);
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
} finally {
setDeleting(false);
}
}, [meta, modalRecord]);
if (!meta) return <Layout back="/db"><p className="text-red-500">Unbekannte Tabelle: {tableKey}</p></Layout>;
return (
@@ -198,12 +215,13 @@ export default function TableView() {
{col}
</th>
))}
<th className="px-3 py-2.5 w-10" />
</tr>
</thead>
<tbody>
{filtered.length === 0 && (
<tr>
<td colSpan={meta.columns.length} className="text-center py-10 text-slate-400">
<td colSpan={meta.columns.length + 1} className="text-center py-10 text-slate-400">
Keine Einträge gefunden
</td>
</tr>
@@ -212,7 +230,7 @@ export default function TableView() {
<tr
key={row.id || i}
onClick={() => setModalRecord(row)}
className={`border-b border-slate-100 hover:bg-slate-50 transition-colors cursor-pointer
className={`group border-b border-slate-100 hover:bg-slate-50 transition-colors cursor-pointer
${highlightId && row.id === highlightId ? 'bg-indigo-50 ring-1 ring-indigo-300' : ''}`}
>
{meta.columns.map(col => (
@@ -225,6 +243,34 @@ export default function TableView() {
/>
</td>
))}
{/* Delete cell */}
<td className="px-3 py-2 text-right align-middle" onClick={e => e.stopPropagation()}>
{confirmDeleteId === row.id ? (
<span className="flex items-center justify-end gap-1">
<button
onClick={e => handleDelete(row.id, e)}
disabled={deleting}
className="text-xs text-white bg-red-500 hover:bg-red-600 rounded px-1.5 py-0.5 font-medium disabled:opacity-50"
>
{deleting ? '…' : '✓'}
</button>
<button
onClick={e => { e.stopPropagation(); setConfirmDeleteId(null); }}
className="text-xs text-slate-500 hover:text-slate-700 bg-slate-100 hover:bg-slate-200 rounded px-1.5 py-0.5"
>
</button>
</span>
) : (
<button
onClick={e => { e.stopPropagation(); setConfirmDeleteId(row.id); }}
className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-400 transition-colors p-1 rounded"
title="Löschen"
>
🗑
</button>
)}
</td>
</tr>
))}
</tbody>