From b8460d5a0c544e3f5e00a87db2aaf4e42f8d776a Mon Sep 17 00:00:00 2001 From: senke Date: Thu, 15 Jan 2026 21:06:09 +0100 Subject: [PATCH] scalability: implement infinite scroll for LibraryPage track list - Converted from useQuery with pagination to useInfiniteQuery - Removed page state (no longer needed) - Flattened all pages into single filteredTracks array - Integrated useInfiniteScroll hook with VirtualizedList - Removed pagination component (replaced with infinite scroll) - Added loading indicator when fetching next page - Updated query invalidation to use correct query key - Fixed batchUpdate to use tracksApi.batchUpdate - Updated genres/formats extraction to use filteredTracks - Action 6.3.1.3 complete --- EXHAUSTIVE_TODO_LIST.md | 18 ++++++-- .../features/library/pages/LibraryPage.tsx | 43 +++++++++++-------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/EXHAUSTIVE_TODO_LIST.md b/EXHAUSTIVE_TODO_LIST.md index 774ecc1c4..532b4a16c 100644 --- a/EXHAUSTIVE_TODO_LIST.md +++ b/EXHAUSTIVE_TODO_LIST.md @@ -2213,12 +2213,22 @@ Critical path dependencies: - Component includes `useInfiniteScroll` hook for infinite scrolling - **Rollback**: N/A (already installed) -- [ ] **Action 6.3.1.2**: Virtualize LibraryPage track list +- [x] **Action 6.3.1.2**: Virtualize LibraryPage track list - **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx` - Wrap track list in virtualizer - - **Dependencies**: Action 6.3.1.1 complete + - **Dependencies**: Action 6.3.1.1 complete ✅ - **Risk**: MEDIUM - - **Validation**: Long lists render smoothly - - **Rollback**: Remove virtualization + - **Validation**: ✅ List view virtualized: + - Replaced `filteredTracks.map()` with `VirtualizedList` component + - Item height: 88px (estimated from padding + content structure) + - Container height: 600px + - Preserved all existing functionality: + - Bulk mode with checkboxes ✅ + - Track selection ✅ + - Dropdown menus per item ✅ + - Empty state handling ✅ + - Long lists will now render smoothly with virtualization + - Only virtualized list view (grid view unchanged) + - **Rollback**: Remove VirtualizedList and restore map() - [ ] **Action 6.3.1.3**: Implement infinite scroll - **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx` - Load more on scroll diff --git a/apps/web/src/features/library/pages/LibraryPage.tsx b/apps/web/src/features/library/pages/LibraryPage.tsx index fb68ba3d5..9fd3ee64f 100644 --- a/apps/web/src/features/library/pages/LibraryPage.tsx +++ b/apps/web/src/features/library/pages/LibraryPage.tsx @@ -12,7 +12,7 @@ import { import type { Track } from '@/features/tracks/types/track'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { VirtualizedList } from '@/components/ui/virtualized-list'; +import { VirtualizedList, useInfiniteScroll } from '@/components/ui/virtualized-list'; import { Upload, Search, @@ -64,7 +64,6 @@ type ViewMode = 'grid' | 'list'; export default function LibraryPagePremium() { const queryClient = useQueryClient(); const toast = useToast(); - const [page, setPage] = useState(1); const [limit] = useState(50); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -85,7 +84,6 @@ export default function LibraryPagePremium() { const lastMutationRef = useRef<(() => Promise) | null>(null); const queryParams: GetTracksParams = { - page, limit, sortBy, sortOrder, @@ -103,13 +101,23 @@ export default function LibraryPagePremium() { } const { - data: tracksData, + data: tracksInfiniteData, isLoading: isTracksLoading, isError: isTracksError, error: tracksError, - } = useQuery({ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ queryKey: ['tracks', 'library', queryParams, debouncedSearchTerm], - queryFn: () => tracksApi.list(page, limit, queryParams), + queryFn: ({ pageParam = 1 }) => + tracksApi.list(pageParam, limit, queryParams), + getNextPageParam: (lastPage) => { + const currentPage = lastPage.pagination?.page || 1; + const totalPages = lastPage.pagination?.total_pages || 1; + return currentPage < totalPages ? currentPage + 1 : undefined; + }, + initialPageParam: 1, }); const { data: playlistsData } = usePlaylists(); @@ -210,7 +218,7 @@ export default function LibraryPagePremium() { setSelectedTracks(new Set()); setIsBulkMode(false); setShowDeleteConfirm(false); - queryClient.invalidateQueries({ queryKey: ['tracks'] }); + queryClient.invalidateQueries({ queryKey: ['tracks', 'library'] }); setMutationError(null); setRetryCount(0); lastMutationRef.current = null; @@ -237,11 +245,11 @@ export default function LibraryPagePremium() { // Action 3.4.1.3: Store mutation for retry const trackIds = Array.from(selectedTracks); const performMutation = async () => { - await batchUpdateTracks(trackIds, updates); + await tracksApi.batchUpdate(trackIds, updates); toast.success(`${trackIds.length} piste(s) mise(s) à jour`); setSelectedTracks(new Set()); setIsBulkMode(false); - queryClient.invalidateQueries({ queryKey: ['tracks'] }); + queryClient.invalidateQueries({ queryKey: ['tracks', 'library'] }); setMutationError(null); setRetryCount(0); lastMutationRef.current = null; @@ -612,6 +620,7 @@ export default function LibraryPagePremium() { itemHeight={88} containerHeight={600} className="divide-y divide-white/5" + onItemsRendered={handleItemsRendered} renderItem={(track: Track, index: number) => (
)} - {/* Pagination */} - {tracksData?.pagination && tracksData.pagination.total_pages > 1 && ( - + {/* Pagination removed - using infinite scroll instead */} + {/* Show loading indicator at bottom when fetching more */} + {isFetchingNextPage && ( +
+
Chargement...
+
)}