Re-check the selected source and version against the live list
The selection is restored from localStorage, but it was only validated once at mount. Deregistering the selected source left App holding an id that no longer exists, so every subsequent call 404'd "Source not found" with no way out but a reload — Setup.deleteSource clears its own selectedSource and never tells App. Deleting the selected version had the same shape. Move both checks into effects keyed on the lists themselves. A sourcesLoaded flag keeps the source effect from firing against the initial empty array and wiping the restored id before the fetch resolves. Also coerce both refresh callbacks to an array. A 401 returns an error object, and data.some() then threw inside an unhandled promise, stranding the selection instead of clearing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c5b12aac76
commit
e6e2faeb37
@ -112,7 +112,10 @@ Theme state lives in `ui/src/theme.jsx` — a React context (`ThemeContext`) wit
|
|||||||
- Operation panel (Scale/Recode/Clone) SQL generation and dim_period JOIN are complete; UI wiring to API still needs completion
|
- Operation panel (Scale/Recode/Clone) SQL generation and dim_period JOIN are complete; UI wiring to API still needs completion
|
||||||
- Load progress bar is jittery — needs throttle (~10 updates/sec)
|
- Load progress bar is jittery — needs throttle (~10 updates/sec)
|
||||||
- Default pivot layout should be configurable per source (currently hardcodes first 2 dimensions)
|
- Default pivot layout should be configurable per source (currently hardcodes first 2 dimensions)
|
||||||
- Source/version selection doesn't persist across page reload
|
- Source/version selection persists in `localStorage` (`pf_sourceId` / `pf_versionId`,
|
||||||
|
`App.jsx`). It is re-validated against the live list whenever that list changes, so a
|
||||||
|
deregistered source or deleted version re-points at the first remaining one instead of
|
||||||
|
leaving a dead id that 404s every call
|
||||||
- Col_meta / version schema drift: if col_meta roles change after a version's forecast table is created, SQL and DDL go out of sync — workaround is to delete and recreate the version
|
- Col_meta / version schema drift: if col_meta roles change after a version's forecast table is created, SQL and DDL go out of sync — workaround is to delete and recreate the version
|
||||||
|
|
||||||
## Deferred (not in v1)
|
## Deferred (not in v1)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export default function App() {
|
|||||||
const [sidebarExpanded, setSidebarExpanded] = useState(() => localStorage.getItem('pf_sidebar') !== 'collapsed')
|
const [sidebarExpanded, setSidebarExpanded] = useState(() => localStorage.getItem('pf_sidebar') !== 'collapsed')
|
||||||
|
|
||||||
const [sources, setSources] = useState([])
|
const [sources, setSources] = useState([])
|
||||||
|
const [sourcesLoaded, setSourcesLoaded] = useState(false)
|
||||||
const [sourceId, setSourceId] = useState(() => localStorage.getItem('pf_sourceId') || '')
|
const [sourceId, setSourceId] = useState(() => localStorage.getItem('pf_sourceId') || '')
|
||||||
const [versions, setVersions] = useState([])
|
const [versions, setVersions] = useState([])
|
||||||
const [versionId, setVersionId] = useState(() => localStorage.getItem('pf_versionId') || '')
|
const [versionId, setVersionId] = useState(() => localStorage.getItem('pf_versionId') || '')
|
||||||
@ -21,37 +22,49 @@ export default function App() {
|
|||||||
|
|
||||||
const refreshSources = useCallback(async () => {
|
const refreshSources = useCallback(async () => {
|
||||||
const data = await fetch('/api/sources').then(r => r.json())
|
const data = await fetch('/api/sources').then(r => r.json())
|
||||||
setSources(data)
|
const list = Array.isArray(data) ? data : []
|
||||||
return data
|
setSources(list)
|
||||||
|
setSourcesLoaded(true)
|
||||||
|
return list
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const refreshVersions = useCallback(async (sid) => {
|
const refreshVersions = useCallback(async (sid) => {
|
||||||
const id = sid ?? sourceId
|
const id = sid ?? sourceId
|
||||||
if (!id) { setVersions([]); return [] }
|
if (!id) { setVersions([]); return [] }
|
||||||
const data = await fetch(`/api/sources/${id}/versions`).then(r => r.json())
|
const data = await fetch(`/api/sources/${id}/versions`).then(r => r.json())
|
||||||
setVersions(data)
|
const list = Array.isArray(data) ? data : []
|
||||||
return data
|
setVersions(list)
|
||||||
|
return list
|
||||||
}, [sourceId])
|
}, [sourceId])
|
||||||
|
|
||||||
|
useEffect(() => { refreshSources() }, [])
|
||||||
|
|
||||||
|
// The selection is restored from localStorage and survives a deregister, so it
|
||||||
|
// has to be re-checked against the list itself rather than only at mount:
|
||||||
|
// deleting the selected source otherwise leaves a dead id behind and every
|
||||||
|
// call 404s "Source not found" until the page is reloaded.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshSources().then(data => {
|
if (!sourcesLoaded) return
|
||||||
if (data.length === 0) { setSourceId(''); return }
|
if (sources.length === 0) { setSourceId(''); return }
|
||||||
if (!sourceId || !data.some(s => String(s.id) === String(sourceId))) {
|
if (!sourceId || !sources.some(s => String(s.id) === String(sourceId))) {
|
||||||
setSourceId(String(data[0].id))
|
setSourceId(String(sources[0].id))
|
||||||
}
|
}
|
||||||
})
|
}, [sources, sourcesLoaded, sourceId])
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sourceId) { setVersions([]); setVersionId(''); return }
|
if (!sourceId) { setVersions([]); setVersionId(''); return }
|
||||||
refreshVersions(sourceId).then(data => {
|
refreshVersions(sourceId)
|
||||||
if (data.length === 0) { setVersionId(''); return }
|
|
||||||
if (!versionId || !data.some(v => String(v.id) === String(versionId))) {
|
|
||||||
setVersionId(String(data[0].id))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [sourceId])
|
}, [sourceId])
|
||||||
|
|
||||||
|
// Same reasoning as sources: a deleted version must not stay selected.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sourceId) return
|
||||||
|
if (versions.length === 0) { setVersionId(''); return }
|
||||||
|
if (!versionId || !versions.some(v => String(v.id) === String(versionId))) {
|
||||||
|
setVersionId(String(versions[0].id))
|
||||||
|
}
|
||||||
|
}, [versions, sourceId, versionId])
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
sources, sourceId, setSourceId,
|
sources, sourceId, setSourceId,
|
||||||
versions, versionId, setVersionId,
|
versions, versionId, setVersionId,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user