From 1baadaca6141f253510f8b27aa8e06932acdb519 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Sun, 3 May 2026 10:35:34 -0400 Subject: [PATCH] Lift stack state to App; merge Records panel; fix Pivot theme on load - Stack selection lifted to App.jsx: stacks fetched on login, selectedStack state shared via StatusBar (pills) and Pivot (view switching); Stacks page calls onStacksChange to keep list fresh - Pivot: derive selectedView/viewType from props, remove local stack state; toolbar replaced with dedicated layouts sub-bar (h-9, layouts only) - Records panel: merge read-only and override sections into single field list; known cols seeded from record's transformed fields; rule-derived fields (transformed minus data) will be editable in follow-up refactor - Pivot theme: setAttribute moved to after flush() so restore() can't reset it Co-Authored-By: Claude Sonnet 4.6 --- ui/src/App.jsx | 33 ++++++---- ui/src/components/StatusBar.jsx | 22 ++++++- ui/src/pages/Pivot.jsx | 64 +++++------------- ui/src/pages/Records.jsx | 112 ++++++++++++++++++-------------- ui/src/pages/Stacks.jsx | 4 +- 5 files changed, 127 insertions(+), 108 deletions(-) diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 65afbc5..2740847 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -18,7 +18,9 @@ export default function App() { const [authed, setAuthed] = useState(false) const [loginUser, setLoginUser] = useState('') const [sources, setSources] = useState([]) + const [stacks, setStacks] = useState([]) const [source, setSource] = useState(() => localStorage.getItem('selectedSource') || '') + const [selectedStack, setSelectedStack] = useState(null) const [sidebarExpanded, setSidebarExpanded] = useState(() => localStorage.getItem('df_sidebar') !== 'collapsed') // Sets of names whose dfv view is out of sync with current definitions const [staleSources, setStaleSources] = useState(new Set()) @@ -28,14 +30,14 @@ export default function App() { async function handleLogin(user, pass) { setCredentials(user, pass) - await api.getSources().then(s => { - sessionStorage.setItem('df_user', user) - sessionStorage.setItem('df_pass', pass) - setSources(s) - if (!source && s.length > 0) setSource(s[0].name) - setAuthed(true) - setLoginUser(user) - }) + const s = await api.getSources() + sessionStorage.setItem('df_user', user) + sessionStorage.setItem('df_pass', pass) + setSources(s) + if (!source && s.length > 0) setSource(s[0].name) + setAuthed(true) + setLoginUser(user) + api.getStacks().then(setStacks).catch(() => {}) } function handleLogout() { @@ -45,11 +47,17 @@ export default function App() { setAuthed(false) setLoginUser('') setSources([]) + setStacks([]) + setSelectedStack(null) setStaleSources(new Set()) setStaleStacks(new Set()) setReprocessSources(new Set()) } + function refreshStacks() { + api.getStacks().then(setStacks).catch(() => {}) + } + // Load initial stale state from DB once on login useEffect(() => { if (!authed) return @@ -128,7 +136,10 @@ export default function App() { {/* Main */}
- + {(staleSources.size > 0 || staleStacks.size > 0) && (
@@ -187,8 +198,8 @@ export default function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } />
diff --git a/ui/src/components/StatusBar.jsx b/ui/src/components/StatusBar.jsx index f430104..2042699 100644 --- a/ui/src/components/StatusBar.jsx +++ b/ui/src/components/StatusBar.jsx @@ -1,7 +1,7 @@ import { NavLink } from 'react-router-dom' import useTheme from '../theme.jsx' -export default function StatusBar({ sources = [], source, setSource }) { +export default function StatusBar({ sources = [], source, setSource, stacks = [], selectedStack, setSelectedStack }) { const { dark, setDark } = useTheme() return ( @@ -23,6 +23,26 @@ export default function StatusBar({ sources = [], source, setSource }) { title="New source" >+ + {stacks.length > 0 && ( + <> + | + Stacks + {stacks.map(s => ( + + ))} + + )} +
- {stacks.map(s => ( - - ))} -
- - Layouts - + {/* Layouts sub-bar */} +
{layouts.map(l => (
applyLayout(l)} - className={`flex items-center gap-1 text-xs rounded px-2 py-0.5 cursor-pointer border transition-colors + className={`flex items-center gap-1 rounded px-2 py-0.5 cursor-pointer border transition-colors ${activeLayoutId === l.id ? 'bg-blue-50 border-blue-300 text-blue-700' : 'bg-white border-gray-200 text-gray-600 hover:border-gray-400'}`}> @@ -477,7 +448,7 @@ export default function Pivot({ source }) { {activeLayoutId !== null && !showSaveAs && ( )} @@ -490,30 +461,27 @@ export default function Pivot({ source }) { onChange={e => setSaveAsName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleSaveAs(); if (e.key === 'Escape') { setShowSaveAs(false); setSaveAsName('') } }} placeholder="Layout name…" - className="text-xs border border-gray-300 rounded px-2 py-0.5 w-36 focus:outline-none focus:border-blue-400" + className="border border-gray-300 rounded px-2 py-0.5 w-36 focus:outline-none focus:border-blue-400" /> - - + +
) : ( )} {activeLayoutId !== null && ( - + )} - {layoutMsg && {layoutMsg}} + {layoutMsg && {layoutMsg}}
- depth: + depth: {[0, 1, 2, 3].map(d => ( ))} diff --git a/ui/src/pages/Records.jsx b/ui/src/pages/Records.jsx index e91f66b..8b04f6d 100644 --- a/ui/src/pages/Records.jsx +++ b/ui/src/pages/Records.jsx @@ -271,8 +271,11 @@ export default function Records({ source }) { const displayCols = (rows.length > 0 ? Object.keys(rows[0]) : cols).filter(c => !HIDDEN_COLS.has(c)) const visCols = cols.filter(c => !HIDDEN_COLS.has(c)) - // All override cols: known from DB + new ones added this session + // For bulk bar: only established override keys (not all transformed cols) const allOverrideCols = [...new Set([...overrideCols, ...extraCols])] + // For the single-record panel: all transformed fields + any override keys + draft keys + const recordTransformedCols = Object.keys(selectedRecord?.transformed || {}).filter(c => !HIDDEN_COLS.has(c)) + const knownCols = [...new Set([...overrideCols, ...recordTransformedCols, ...Object.keys(overrideDraft)])] const savedOverrides = selectedRecord?.overrides || {} const isDirty = Object.values(overrideDraft).some(v => String(v).trim()) @@ -493,59 +496,74 @@ export default function Records({ source }) {
)} - {/* Read-only transformed fields */} -
- {Object.entries(selectedRecord.transformed || {}).map(([field, val]) => ( -
- {field} - {formatVal(val) ?? } -
- ))} +
+ Fields +
- {/* Override cols — Mappings-style */} -
-
- Override - -
- +
- {allOverrideCols.map((col, idx) => { - const isExtra = idx >= overrideCols.length - const suggestions = [...(globalValues[col] || [])].sort() + {knownCols.map(col => { const val = overrideDraft[col] ?? '' + const placeholder = formatVal(selectedRecord.transformed?.[col]) ?? '' + const suggestions = [...(globalValues[col] || [])].sort() return ( - - + - + + + ) + })} + {extraCols.map((col, i) => { + const val = overrideDraft[col] ?? '' + const suggestions = [...(globalValues[col] || [])].sort() + return ( + + + -
- {isExtra ? ( - { - const newName = e.target.value - setExtraCols(ec => { const c = [...ec]; c[idx - overrideCols.length] = newName; return c }) - if (val) setOverrideDraft(d => { - const n = { ...d } - delete n[col] - if (newName) n[newName] = val - return n - }) - }} - /> - ) : ( - {col} - )} +
+ {col} + setOverrideDraft(d => ({ ...d, [col]: v }))} + onEnter={handleSaveOverrides} + suggestions={suggestions} + /> + + {val && ( + + )} +
+ { + const newName = e.target.value + setExtraCols(ec => { const c = [...ec]; c[i] = newName; return c }) + if (val) setOverrideDraft(d => { + const n = { ...d } + delete n[col] + if (newName) n[newName] = val + return n + }) + }} + /> + + setOverrideDraft(d => ({ ...d, [col]: v }))} @@ -553,7 +571,7 @@ export default function Records({ source }) { suggestions={suggestions} /> + {val && (
-
+