From e6c86a97fcacbc50c3b826904009ce54157c8a3b Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 21 May 2026 21:53:45 +0200 Subject: [PATCH] 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 --- src/components/RecordModal.jsx | 326 +++++++++++++++++++++++++++++++++ src/lib/api.js | 11 ++ src/lib/tables.js | 53 +++++- src/pages/TableView.jsx | 22 ++- 4 files changed, 408 insertions(+), 4 deletions(-) create mode 100644 src/components/RecordModal.jsx diff --git a/src/components/RecordModal.jsx b/src/components/RecordModal.jsx new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/src/components/RecordModal.jsx @@ -0,0 +1,326 @@ +import { useEffect, useState, useCallback } from 'react'; +import { apiPatch, apiFetchOne } from '../lib/api'; +import { STATUS_COLORS } from '../lib/tables'; + +const READ_ONLY_FIELDS = new Set(['id', 'created_at', 'updated_at']); +const TIMESTAMP_RE = /(_at|_timestamp)$/; + +function isReadOnly(key) { + return READ_ONLY_FIELDS.has(key) || TIMESTAMP_RE.test(key); +} + +function FieldLabel({ name }) { + return ( + + {name.replace(/_/g, ' ')} + + ); +} + +function ReadOnlyValue({ fieldKey, value }) { + if (value == null || value === '') return ; + + if (Array.isArray(value)) { + if (value.length === 0) return []; + return ( +
+ {value.map((v, i) => ( + + {String(v).slice(0, 12)} + + ))} +
+ ); + } + + if (TIMESTAMP_RE.test(fieldKey)) { + return ( + + {new Date(value).toLocaleString('de-DE')} + + ); + } + + if (fieldKey === 'status') { + const cls = STATUS_COLORS[value] || 'bg-slate-100 text-slate-600'; + return {value}; + } + + if (fieldKey === 'id') { + return {value}; + } + + return {String(value)}; +} + +function EditableField({ fieldKey, value, fieldDef, onChange }) { + const { type, options, min, max } = fieldDef; + + if (type === 'select') { + return ( + + ); + } + + if (type === 'textarea') { + return ( +