feat: improve media library layout and loading behavior
All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 24s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 28s

- Added a hidden anchor element to the media library for smooth scrolling to the top of the grid after uploads.
- Introduced a sequence reference to manage concurrent media item fetch requests, ensuring accurate loading states.
- Updated upload summary messaging to include a note about the list being refreshed after uploads.
- Enhanced loading state management to prevent unnecessary updates when fetch requests are out of sync.
This commit is contained in:
Lars 2026-05-07 22:02:18 +02:00
parent cc4e621f95
commit ceef6f09e2
2 changed files with 23 additions and 2 deletions

View File

@ -5563,6 +5563,14 @@ a.analysis-split__nav-item {
color: var(--text2);
flex: 1 1 200px;
}
.media-library__grid-top-anchor {
height: 1px;
margin: 0;
padding: 0;
overflow: hidden;
visibility: hidden;
pointer-events: none;
}
.media-library__upload-icon {
vertical-align: middle;
margin-right: 6px;

View File

@ -262,6 +262,8 @@ export default function MediaLibraryPage() {
const [uploadClubId, setUploadClubId] = useState('')
const [uploadBusy, setUploadBusy] = useState(false)
const [uploadSummary, setUploadSummary] = useState('')
const mediaListFetchSeqRef = useRef(0)
const gridTopAnchorRef = useRef(null)
const loadClubs = useCallback(async () => {
try {
@ -277,6 +279,7 @@ export default function MediaLibraryPage() {
}, [loadClubs])
const loadItems = useCallback(async () => {
const seq = ++mediaListFetchSeqRef.current
setLoading(true)
setError('')
try {
@ -294,6 +297,7 @@ export default function MediaLibraryPage() {
? { uploaded_by: Number(filterUploaderId) }
: {}),
})
if (seq !== mediaListFetchSeqRef.current) return
setItems(res.items || [])
setViewer(res.viewer || null)
if (res.filter_meta?.uploaders?.length) {
@ -301,9 +305,10 @@ export default function MediaLibraryPage() {
}
setSelected(new Set())
} catch (e) {
if (seq !== mediaListFetchSeqRef.current) return
setError(e.message || String(e))
} finally {
setLoading(false)
if (seq === mediaListFetchSeqRef.current) setLoading(false)
}
}, [lifecycle, q, mediaKind, filterClubId, filterUploaderId, isSuperadmin, viewer?.show_uploader_meta])
@ -485,9 +490,15 @@ export default function MediaLibraryPage() {
...(uploadVis === 'club' ? { club_id: Number(uploadClubId) } : {}),
})
setUploadSummary(
`Archiv-Upload: neu ${res.created_count}, bereits vorhanden ${res.duplicate_count}, fehlgeschlagen ${res.failed_count}.`,
`Archiv-Upload: neu ${res.created_count}, bereits vorhanden ${res.duplicate_count}, fehlgeschlagen ${res.failed_count}. Liste aktualisiert.`,
)
if (res.created_count > 0 || res.duplicate_count > 0) {
setFilterUploaderId('')
}
await loadItems()
window.requestAnimationFrame(() => {
gridTopAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
})
} catch (err) {
window.alert(err.message || String(err))
} finally {
@ -682,6 +693,8 @@ export default function MediaLibraryPage() {
</p>
) : null}
<div ref={gridTopAnchorRef} className="media-library__grid-top-anchor" aria-hidden="true" />
{loading && !items.length ? <div className="spinner media-library__spinner" /> : null}
{!loading && !items.length && !error ? (