From dc46af8d31f6a5d74c1712e40332dfd85771606c Mon Sep 17 00:00:00 2001 From: Experiments DB Dev Date: Thu, 23 Apr 2026 12:59:34 -0400 Subject: [PATCH] feat: inline cell editing and prev/next day navigation in day view Clicking any table cell opens an inline input (text or textarea per field type). Enter or blur saves; Escape cancels. Unchanged values skip the API call. Custom fields merge with existing custom_fields; builtin fields use their key directly. Prev/next buttons at the bottom navigate to adjacent days that have data for the selected field, preserving the ?field= param. Day list is fetched via getCalendar and sorted chronologically. Tests: 41 total (added 15 new tests for cell editing and day nav). Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/ExperimentDayView.jsx | 186 +++++++++++++++--- frontend/tests/ExperimentDayView.test.jsx | 218 +++++++++++++++++++++- 2 files changed, 380 insertions(+), 24 deletions(-) diff --git a/frontend/src/pages/ExperimentDayView.jsx b/frontend/src/pages/ExperimentDayView.jsx index 9b83ba7..fddf645 100644 --- a/frontend/src/pages/ExperimentDayView.jsx +++ b/frontend/src/pages/ExperimentDayView.jsx @@ -1,11 +1,11 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useParams, useNavigate, useSearchParams, Link } from 'react-router-dom'; import { format } from 'date-fns'; import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, } from 'recharts'; -import { experimentsApi } from '../api/client'; +import { experimentsApi, dailyStatusesApi } from '../api/client'; import Button from '../components/ui/Button'; import Modal from '../components/ui/Modal'; import DailyStatusForm from '../components/DailyStatusForm'; @@ -23,8 +23,50 @@ function cellText(value) { return String(value); } +// ── Inline cell editor ──────────────────────────────────────────────────────── + +function CellEdit({ value, field, saving, onSave, onCancel }) { + const [val, setVal] = useState(value ?? ''); + const ref = useRef(null); + + useEffect(() => { + ref.current?.focus(); + ref.current?.select(); + }, []); + + function handleKey(e) { + if (e.key === 'Escape') { e.preventDefault(); onCancel(); } + if (e.key === 'Enter' && field.type !== 'textarea') { e.preventDefault(); onSave(val); } + } + + const cls = 'w-full text-sm border border-blue-400 rounded px-1.5 py-0.5 focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white disabled:opacity-60'; + + if (field.type === 'textarea') { + return ( +