1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

fix: capture popup non-persistance

- Save and restore dialog open/closed state in sessionStorage (App.jsx)
- Persist form data: locator, error, busy, jobStatus, jobUid (CaptureDialog.jsx)
- Auto-resume polling if capture job was in progress before page refresh
- Only clear form on fresh user click, not when restoring from refresh
- Clean up sessionStorage when capture completes successfully

Fixes: Capture pop-up disappears on page refresh with unsaved data
This commit is contained in:
TheGeneralist 2026-07-03 13:31:46 +02:00 committed by GitHub
parent 2502de45b6
commit d6b52ba06c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 158 additions and 51 deletions

View file

@ -76,7 +76,15 @@ export default function App() {
const [searchBusy, setSearchBusy] = useState(false)
const [runs, setRuns] = useState([])
const [tagNodes, setTagNodes] = useState([])
const [captureDialogOpen, setCaptureDialogOpen] = useState(false)
const [captureDialogOpen, setCaptureDialogOpen] = useState(() => {
const saved = sessionStorage.getItem('captureDialogOpen')
return saved === 'true'
})
// Persist captureDialogOpen to sessionStorage
useEffect(() => {
sessionStorage.setItem('captureDialogOpen', captureDialogOpen)
}, [captureDialogOpen])
const loadEntries = useCallback(async (aid, q, tag) => {
if (!aid) return

View file

@ -3,12 +3,89 @@ import { submitCapture, pollCaptureJob } from '../api'
export default function CaptureDialog({ open, archiveId, onClose, onCaptured }) {
const dialogRef = useRef(null)
const [locator, setLocator] = useState('')
const [error, setError] = useState(null)
const [busy, setBusy] = useState(false)
const [jobStatus, setJobStatus] = useState(null) // null | 'running' | 'completed' | 'failed'
const isFirstRenderRef = useRef(true)
const hasResumedPollingRef = useRef(false)
const [locator, setLocator] = useState(() => {
const saved = sessionStorage.getItem('captureDialogLocator')
return saved || ''
})
const [error, setError] = useState(() => {
const saved = sessionStorage.getItem('captureDialogError')
return saved || null
})
const [busy, setBusy] = useState(() => {
const saved = sessionStorage.getItem('captureDialogBusy')
return saved === 'true'
})
const [jobStatus, setJobStatus] = useState(() => {
const saved = sessionStorage.getItem('captureDialogJobStatus')
return saved || null
})
const [jobUid, setJobUid] = useState(() => {
const saved = sessionStorage.getItem('captureDialogJobUid')
return saved || null
})
const pollRef = useRef(null)
// Persist state to sessionStorage
useEffect(() => {
sessionStorage.setItem('captureDialogLocator', locator)
}, [locator])
useEffect(() => {
sessionStorage.setItem('captureDialogError', error || '')
}, [error])
useEffect(() => {
sessionStorage.setItem('captureDialogBusy', busy)
}, [busy])
useEffect(() => {
sessionStorage.setItem('captureDialogJobStatus', jobStatus || '')
}, [jobStatus])
useEffect(() => {
sessionStorage.setItem('captureDialogJobUid', jobUid || '')
}, [jobUid])
// On mount, resume polling if a job was in progress before page refresh
useEffect(() => {
if (hasResumedPollingRef.current) return
if (!jobUid || jobStatus !== 'running' || !archiveId) return
hasResumedPollingRef.current = true
// Resume polling for the saved job
pollRef.current = setInterval(async () => {
try {
const updated = await pollCaptureJob(archiveId, jobUid)
if (updated.status === 'completed') {
clearInterval(pollRef.current)
pollRef.current = null
setBusy(false)
setJobStatus('completed')
clearCaptureState()
dialogRef.current?.close()
onCaptured()
} else if (updated.status === 'failed') {
clearInterval(pollRef.current)
pollRef.current = null
setBusy(false)
setJobStatus('failed')
setError(updated.error_text || 'Capture failed.')
}
// pending / running: keep polling
} catch (pollErr) {
clearInterval(pollRef.current)
pollRef.current = null
setBusy(false)
setError(pollErr.message)
}
}, 500)
}, [jobUid, jobStatus, archiveId, onCaptured])
// Handle dialog close event
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
@ -20,21 +97,41 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
return () => dialog.removeEventListener('close', handleClose)
}, [onClose])
// Handle open/close from parent
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (open) {
setLocator('')
setError(null)
setJobStatus(null)
setBusy(false)
clearInterval(pollRef.current)
// Only clear state if this is a fresh open from user click (not a restore on first render)
if (!isFirstRenderRef.current) {
setLocator('')
setError(null)
setJobStatus(null)
setBusy(false)
setJobUid(null)
clearInterval(pollRef.current)
}
isFirstRenderRef.current = false
if (!dialog.open) dialog.showModal()
} else {
if (dialog.open) dialog.close()
}
}, [open])
function clearCaptureState() {
sessionStorage.removeItem('captureDialogLocator')
sessionStorage.removeItem('captureDialogError')
sessionStorage.removeItem('captureDialogBusy')
sessionStorage.removeItem('captureDialogJobStatus')
sessionStorage.removeItem('captureDialogJobUid')
setLocator('')
setError(null)
setBusy(false)
setJobStatus(null)
setJobUid(null)
}
async function handleSubmit() {
if (!locator.trim()) { setError('Enter a locator.'); return }
setBusy(true)
@ -42,6 +139,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
setJobStatus(null)
try {
const job = await submitCapture(archiveId, locator.trim())
setJobUid(job.job_uid)
setJobStatus('running')
pollRef.current = setInterval(async () => {
try {
@ -51,6 +149,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured })
pollRef.current = null
setBusy(false)
setJobStatus('completed')
clearCaptureState()
dialogRef.current?.close()
onCaptured()
} else if (updated.status === 'failed') {