Add RecordModal: row-click detail popup with inline editing

- Click any table row to open a full-detail popup
- Editable fields (text, textarea, select, number) with PATCH save
- Read-only display for IDs, timestamps, arrays
- Pictures table fetches words via /pictures/:id/words in modal
- Stop propagation on linked-field chips so they don't trigger modal
- apiPatch + apiFetchOne helpers in api.js
- editableFields + fetchRelated config in tables.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 21:53:45 +02:00
parent 74082cd333
commit e6c86a97fc
4 changed files with 408 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import { useEffect, useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import RecordModal from '../components/RecordModal';
import { fetchAll } from '../lib/api';
import { TABLES, STATUS_COLORS } from '../lib/tables';
@@ -22,7 +23,7 @@ function CellValue({ col, value, linkedFields, navigate }) {
{value.slice(0, 3).map(id => (
<button
key={id}
onClick={() => targetTable && navigate(`/db/${targetTable}?id=${id}`)}
onClick={e => { e.stopPropagation(); targetTable && navigate(`/db/${targetTable}?id=${id}`); }}
className="text-xs bg-indigo-50 text-indigo-600 rounded px-1.5 py-0.5 hover:bg-indigo-100 font-mono"
title={String(id)}
>
@@ -41,7 +42,7 @@ function CellValue({ col, value, linkedFields, navigate }) {
const targetTable = linkedFields[col];
return (
<button
onClick={() => navigate(`/db/${targetTable}?id=${value}`)}
onClick={e => { e.stopPropagation(); navigate(`/db/${targetTable}?id=${value}`); }}
className="text-xs bg-indigo-50 text-indigo-600 rounded px-1.5 py-0.5 hover:bg-indigo-100 font-mono"
title={value}
>
@@ -84,6 +85,7 @@ export default function TableView() {
const [statusFilter, setStatusFilter] = useState('');
const [textFilter, setTextFilter] = useState('');
const [highlightId, setHighlightId] = useState(null);
const [modalRecord, setModalRecord] = useState(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
@@ -118,6 +120,10 @@ export default function TableView() {
});
}, [rows, statusFilter, textFilter]);
function handleRecordSaved(updated) {
setRows(prev => prev.map(r => (r.id === updated.id ? { ...r, ...updated } : r)));
}
if (!meta) return <Layout back="/db"><p className="text-red-500">Unbekannte Tabelle: {tableKey}</p></Layout>;
return (
@@ -191,7 +197,8 @@ export default function TableView() {
{filtered.map((row, i) => (
<tr
key={row.id || i}
className={`border-b border-slate-100 hover:bg-slate-50 transition-colors
onClick={() => setModalRecord(row)}
className={`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 => (
@@ -210,6 +217,15 @@ export default function TableView() {
</table>
</div>
)}
{modalRecord && (
<RecordModal
record={modalRecord}
meta={meta}
onClose={() => setModalRecord(null)}
onSaved={handleRecordSaved}
/>
)}
</Layout>
);
}