fix: cap request bodies, link login error, add book-form retry, guard double add
Limit request bodies to 1 MiB (413 JSON), wire aria-describedby on the login form, add Retry and cancellation to the book form load, disable Add entry while a note POST is in flight, and poll pg_isready in README. Claude-Session: https://claude.ai/code/session_01M9MLit5Ko3X4s7rzC5Kv7X
This commit is contained in:
@@ -62,4 +62,15 @@ describe('BookForm', () => {
|
||||
body: { title: 'Dune Messiah', author: 'Frank Herbert', genre_id: 5, total_pages: 380, current_page: 142 },
|
||||
})
|
||||
})
|
||||
|
||||
it('retries a failed load and shows the prefilled form', async () => {
|
||||
let attempts = 0
|
||||
signedIn({
|
||||
'GET /api/books/7': () => (++attempts === 1 ? [500, { error: 'Database unavailable' }] : [200, dune]),
|
||||
})
|
||||
const user = renderApp('/books/7/edit')
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent('Database unavailable')
|
||||
await user.click(screen.getByRole('button', { name: 'Retry' }))
|
||||
expect(await screen.findByLabelText('Title')).toHaveValue('Dune')
|
||||
})
|
||||
})
|
||||
|
||||
+21
-9
@@ -19,11 +19,16 @@ export function BookForm() {
|
||||
const [loadError, setLoadError] = useState<string | null>(null)
|
||||
const [error, setError] = useState<FormError | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [attempt, setAttempt] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing) return
|
||||
let cancelled = false
|
||||
setLoadError(null)
|
||||
setLoading(true)
|
||||
api<Book>('GET', `/books/${id}`).then(
|
||||
(book) => {
|
||||
if (cancelled) return
|
||||
setFields({
|
||||
title: book.title,
|
||||
author: book.author,
|
||||
@@ -34,11 +39,15 @@ export function BookForm() {
|
||||
setLoading(false)
|
||||
},
|
||||
(err) => {
|
||||
if (cancelled) return
|
||||
setLoadError(errorMessage(err))
|
||||
setLoading(false)
|
||||
},
|
||||
)
|
||||
}, [editing, id])
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [editing, id, attempt])
|
||||
|
||||
function set(name: keyof Fields) {
|
||||
return (event: { target: { value: string } }) => setFields((prev) => ({ ...prev, [name]: event.target.value }))
|
||||
@@ -72,13 +81,16 @@ export function BookForm() {
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<div className={card}>
|
||||
<p role="alert" className="text-red-700">
|
||||
{loadError}
|
||||
</p>
|
||||
<Link to="/books" className="mt-2 inline-block text-indigo-700 underline">
|
||||
← Back to library
|
||||
</Link>
|
||||
<div role="alert" className={card}>
|
||||
<p className="text-red-700">{loadError}</p>
|
||||
<div className="mt-2 flex items-center gap-3">
|
||||
<button type="button" className={secondaryButton} onClick={() => setAttempt((n) => n + 1)}>
|
||||
Retry
|
||||
</button>
|
||||
<Link to="/books" className="text-indigo-700 underline">
|
||||
← Back to library
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -106,7 +118,7 @@ export function BookForm() {
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{genresError && <p className="text-sm text-red-700">{`Could not load genres: ${genresError}`}</p>}
|
||||
{genresError && <p role="alert" className="text-sm text-red-700">{`Could not load genres: ${genresError}`}</p>}
|
||||
<div className="flex gap-4">
|
||||
<label className="block">
|
||||
<span className={labelText}>Total pages</span>
|
||||
|
||||
Reference in New Issue
Block a user