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
This commit is contained in:
senke 2026-01-15 21:06:09 +01:00
parent b77902b6bd
commit b8460d5a0c
2 changed files with 38 additions and 23 deletions

View file

@ -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

View file

@ -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<void>) | 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) => (
<div
key={track.id}
@ -690,16 +699,12 @@ export default function LibraryPagePremium() {
</Card>
)}
{/* Pagination */}
{tracksData?.pagination && tracksData.pagination.total_pages > 1 && (
<Pagination
currentPage={page}
totalPages={tracksData.pagination.total_pages}
onPageChange={setPage}
totalItems={tracksData.pagination.total}
itemsPerPage={limit}
showItemsInfo={true}
/>
{/* Pagination removed - using infinite scroll instead */}
{/* Show loading indicator at bottom when fetching more */}
{isFetchingNextPage && (
<div className="flex justify-center items-center py-4">
<div className="text-kodo-secondary text-sm">Chargement...</div>
</div>
)}
<UploadModal open={isUploadModalOpen} onClose={handleCloseUpload} />