Show request submission progress in a focused follow-up dialog
Magent CI/CD / verify (push) Successful in 1m53s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 55s

This commit is contained in:
2026-09-15 14:02:08 +12:00
parent c194db167a
commit 6a84e68a03
3 changed files with 63 additions and 22 deletions
+33 -20
View File
@@ -1,6 +1,8 @@
'use client'
import PageHeading from '../ui/PageHeading'
import { lockBodyScroll } from '../lib/scrollLock'
import './request-progress.css'
import { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
@@ -96,12 +98,6 @@ const apiError = async (response: Response, fallback: string) => {
return fallback
}
const formatDuration = (milliseconds?: number | null) => {
if (milliseconds == null) return null
if (milliseconds < 1000) return `${Math.round(milliseconds)} ms`
return `${(milliseconds / 1000).toFixed(1)} s`
}
export default function NewRequestClient() {
const router = useRouter()
const searchSectionRef = useRef<HTMLElement | null>(null)
@@ -118,6 +114,16 @@ export default function NewRequestClient() {
const [selectedSeasons, setSelectedSeasons] = useState<number[]>([])
const [acceptOriginalLanguage, setAcceptOriginalLanguage] = useState<boolean | null>(null)
const [submitting, setSubmitting] = useState(false)
const [progressOpen, setProgressOpen] = useState(false)
const progressDialog = useRef<HTMLDialogElement>(null)
useEffect(() => {
if (!progressOpen) return
const previous = document.activeElement as HTMLElement | null
progressDialog.current?.showModal()
const unlock = lockBodyScroll()
return () => { progressDialog.current?.close(); unlock(); previous?.focus() }
}, [progressOpen])
const [operation, setOperation] = useState<OperationProgress | null>(null)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
@@ -269,12 +275,13 @@ export default function NewRequestClient() {
}
const submitRequest = async () => {
if (!selected || !options) return
if (!selected || !options || submitting) return
if (options.media.originalLanguage && acceptOriginalLanguage === null) { setError('Choose an audio language option before requesting.'); return }
if (selected.type === 'tv' && selectedSeasons.length === 0) {
setError('Select at least one season.')
return
}
setProgressOpen(true)
setSubmitting(true)
setError(null)
setSuccess(null)
@@ -309,7 +316,7 @@ export default function NewRequestClient() {
setResults((current) => current.map((item) => item.tmdbId === selected.tmdbId && item.type === selected.type
? { ...item, requestId, statusLabel: payload?.statusLabel ?? item.statusLabel }
: item))
setSuccess(requestId ? `Request #${requestId} has been accepted by Seerr.` : 'Your request has been accepted by Seerr.')
setSuccess('Your request has been received.')
} catch (caught) {
setError(caught instanceof Error ? caught.message : 'The request could not be submitted.')
} finally {
@@ -336,6 +343,22 @@ export default function NewRequestClient() {
return (
<main className="card request-portal-page">
<dialog ref={progressDialog} className="create-request-dialog" aria-labelledby="create-progress-title" onCancel={() => setProgressOpen(false)} onClose={() => setProgressOpen(false)}>
<div className="create-progress-header"><span>Request progress</span><button type="button" className="ghost-button" onClick={() => setProgressOpen(false)} aria-label="Close request progress">Close</button></div>
<div className="create-progress-body" aria-live="polite" aria-atomic="true">
{submitting && <span className="create-progress-spinner" aria-hidden="true" />}
<h2 id="create-progress-title">{submitting ? 'Sending your request' : success ? 'Request received' : 'Your request needs attention'}</h2>
<p className="create-progress-title">{selected?.title}</p>
<p>{submitting ? (operation?.events.some(event => event.service === 'Sonarr' || event.service === 'Radarr') ? 'Setting up your title for collection. Please wait.' : 'Checking your selection and sending it to the request service. Please wait.') : success ? 'Your request is now in the pipeline. Follow it to see approval, download progress and when it is ready to watch.' : error || 'We could not confirm the result. Check My requests before trying again.'}</p>
{submitting && <div className="create-progress-track" role="progressbar" aria-label="Submitting request"><span /></div>}
{success && <div className="create-progress-stage"><span>Current stage</span><strong>{selected?.statusLabel || 'Request received'}</strong></div>}
</div>
<div className="create-progress-actions">
{!submitting && <button type="button" className="create-progress-follow" onClick={() => router.push(selected?.requestId ? `/requests/${selected.requestId}` : '/')} >{success ? 'Follow your request' : 'Check My requests'} <span aria-hidden="true">&rarr;</span></button>}
{!submitting && <button type="button" className="ghost-button" onClick={() => setProgressOpen(false)}>{success ? 'Back to browsing' : 'Back to request'}</button>}
{submitting && <small>You can close this window. Submission will continue while you stay on this page.</small>}
</div>
</dialog>
<PageHeading title="New request" description="Find a movie or TV show and choose what you want to watch." />
<ol className="request-master-stepper" aria-label="New request progress">
@@ -513,20 +536,10 @@ export default function NewRequestClient() {
</div>
) : null}
{operation && (
<div className={`request-submit-progress is-${operation.status}`} aria-live="polite">
<header><div><span>Remote activity</span><strong>{operation.status === 'running' ? 'Sending your request' : operation.status === 'complete' ? 'Request hand-off complete' : 'Request hand-off needs attention'}</strong></div>{formatDuration(operation.duration_ms) && <small>{formatDuration(operation.duration_ms)}</small>}</header>
<div>
{operation.events.map((event) => (
<p key={event.id} className={`is-${event.state}`}><i aria-hidden="true" /><span><strong>{event.service}</strong>{event.message}</span><small>{formatDuration(event.duration_ms)}{event.status_code ? ` · HTTP ${event.status_code}` : ''}</small></p>
))}
{operation.events.length === 0 && <p className="is-active"><i aria-hidden="true" /><span><strong>Magent</strong>Preparing the request</span></p>}
</div>
</div>
)}
{operation && <button type="button" className="ghost-button" onClick={() => setProgressOpen(true)}>View request progress</button>}
{success && selected.requestId && (
<div className="request-complete-actions"><button type="button" onClick={() => router.push(`/requests/${selected.requestId}`)}>Track request #{selected.requestId}</button><button type="button" className="ghost-button" onClick={() => resetAfterType(selected.type)}>Request something else</button></div>
<div className="request-complete-actions"><button type="button" onClick={() => router.push(`/requests/${selected.requestId}`)}>Follow your request</button><button type="button" className="ghost-button" onClick={() => resetAfterType(selected.type)}>Request something else</button></div>
)}
</section>
)}