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:
@@ -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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
import Layout from '../components/Layout';
|
import Layout from '../components/Layout';
|
||||||
import RecordModal from '../components/RecordModal';
|
import RecordModal from '../components/RecordModal';
|
||||||
import CreateModal, { hasCreateForm } from '../components/CreateModal';
|
import CreateModal, { hasCreateForm } from '../components/CreateModal';
|
||||||
import { fetchAll } from '../lib/api';
|
import { fetchAll, apiDelete } from '../lib/api';
|
||||||
import { TABLES, STATUS_COLORS } from '../lib/tables';
|
import { TABLES, STATUS_COLORS } from '../lib/tables';
|
||||||
|
|
||||||
function truncate(str, n = 60) {
|
function truncate(str, n = 60) {
|
||||||
@@ -88,6 +88,8 @@ export default function TableView() {
|
|||||||
const [highlightId, setHighlightId] = useState(null);
|
const [highlightId, setHighlightId] = useState(null);
|
||||||
const [modalRecord, setModalRecord] = useState(null);
|
const [modalRecord, setModalRecord] = useState(null);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
|
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -130,6 +132,21 @@ export default function TableView() {
|
|||||||
setRows(prev => [newRecord, ...prev]);
|
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>;
|
if (!meta) return <Layout back="/db"><p className="text-red-500">Unbekannte Tabelle: {tableKey}</p></Layout>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -198,12 +215,13 @@ export default function TableView() {
|
|||||||
{col}
|
{col}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
|
<th className="px-3 py-2.5 w-10" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<tr>
|
<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
|
Keine Einträge gefunden
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -212,7 +230,7 @@ export default function TableView() {
|
|||||||
<tr
|
<tr
|
||||||
key={row.id || i}
|
key={row.id || i}
|
||||||
onClick={() => setModalRecord(row)}
|
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' : ''}`}
|
${highlightId && row.id === highlightId ? 'bg-indigo-50 ring-1 ring-indigo-300' : ''}`}
|
||||||
>
|
>
|
||||||
{meta.columns.map(col => (
|
{meta.columns.map(col => (
|
||||||
@@ -225,6 +243,34 @@ export default function TableView() {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user