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:
parent
b77902b6bd
commit
b8460d5a0c
2 changed files with 38 additions and 23 deletions
|
|
@ -2213,12 +2213,22 @@ Critical path dependencies:
|
||||||
- Component includes `useInfiniteScroll` hook for infinite scrolling
|
- Component includes `useInfiniteScroll` hook for infinite scrolling
|
||||||
- **Rollback**: N/A (already installed)
|
- **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
|
- **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
|
- **Risk**: MEDIUM
|
||||||
- **Validation**: Long lists render smoothly
|
- **Validation**: ✅ List view virtualized:
|
||||||
- **Rollback**: Remove virtualization
|
- 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
|
- [ ] **Action 6.3.1.3**: Implement infinite scroll
|
||||||
- **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx` - Load more on scroll
|
- **Scope**: `apps/web/src/features/library/pages/LibraryPage.tsx` - Load more on scroll
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import type { Track } from '@/features/tracks/types/track';
|
import type { Track } from '@/features/tracks/types/track';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { VirtualizedList } from '@/components/ui/virtualized-list';
|
import { VirtualizedList, useInfiniteScroll } from '@/components/ui/virtualized-list';
|
||||||
import {
|
import {
|
||||||
Upload,
|
Upload,
|
||||||
Search,
|
Search,
|
||||||
|
|
@ -64,7 +64,6 @@ type ViewMode = 'grid' | 'list';
|
||||||
export default function LibraryPagePremium() {
|
export default function LibraryPagePremium() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [limit] = useState(50);
|
const [limit] = useState(50);
|
||||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
@ -85,7 +84,6 @@ export default function LibraryPagePremium() {
|
||||||
const lastMutationRef = useRef<(() => Promise<void>) | null>(null);
|
const lastMutationRef = useRef<(() => Promise<void>) | null>(null);
|
||||||
|
|
||||||
const queryParams: GetTracksParams = {
|
const queryParams: GetTracksParams = {
|
||||||
page,
|
|
||||||
limit,
|
limit,
|
||||||
sortBy,
|
sortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
|
|
@ -103,13 +101,23 @@ export default function LibraryPagePremium() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: tracksData,
|
data: tracksInfiniteData,
|
||||||
isLoading: isTracksLoading,
|
isLoading: isTracksLoading,
|
||||||
isError: isTracksError,
|
isError: isTracksError,
|
||||||
error: tracksError,
|
error: tracksError,
|
||||||
} = useQuery({
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
} = useInfiniteQuery({
|
||||||
queryKey: ['tracks', 'library', queryParams, debouncedSearchTerm],
|
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();
|
const { data: playlistsData } = usePlaylists();
|
||||||
|
|
@ -210,7 +218,7 @@ export default function LibraryPagePremium() {
|
||||||
setSelectedTracks(new Set());
|
setSelectedTracks(new Set());
|
||||||
setIsBulkMode(false);
|
setIsBulkMode(false);
|
||||||
setShowDeleteConfirm(false);
|
setShowDeleteConfirm(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ['tracks'] });
|
queryClient.invalidateQueries({ queryKey: ['tracks', 'library'] });
|
||||||
setMutationError(null);
|
setMutationError(null);
|
||||||
setRetryCount(0);
|
setRetryCount(0);
|
||||||
lastMutationRef.current = null;
|
lastMutationRef.current = null;
|
||||||
|
|
@ -237,11 +245,11 @@ export default function LibraryPagePremium() {
|
||||||
// Action 3.4.1.3: Store mutation for retry
|
// Action 3.4.1.3: Store mutation for retry
|
||||||
const trackIds = Array.from(selectedTracks);
|
const trackIds = Array.from(selectedTracks);
|
||||||
const performMutation = async () => {
|
const performMutation = async () => {
|
||||||
await batchUpdateTracks(trackIds, updates);
|
await tracksApi.batchUpdate(trackIds, updates);
|
||||||
toast.success(`${trackIds.length} piste(s) mise(s) à jour`);
|
toast.success(`${trackIds.length} piste(s) mise(s) à jour`);
|
||||||
setSelectedTracks(new Set());
|
setSelectedTracks(new Set());
|
||||||
setIsBulkMode(false);
|
setIsBulkMode(false);
|
||||||
queryClient.invalidateQueries({ queryKey: ['tracks'] });
|
queryClient.invalidateQueries({ queryKey: ['tracks', 'library'] });
|
||||||
setMutationError(null);
|
setMutationError(null);
|
||||||
setRetryCount(0);
|
setRetryCount(0);
|
||||||
lastMutationRef.current = null;
|
lastMutationRef.current = null;
|
||||||
|
|
@ -612,6 +620,7 @@ export default function LibraryPagePremium() {
|
||||||
itemHeight={88}
|
itemHeight={88}
|
||||||
containerHeight={600}
|
containerHeight={600}
|
||||||
className="divide-y divide-white/5"
|
className="divide-y divide-white/5"
|
||||||
|
onItemsRendered={handleItemsRendered}
|
||||||
renderItem={(track: Track, index: number) => (
|
renderItem={(track: Track, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={track.id}
|
key={track.id}
|
||||||
|
|
@ -690,16 +699,12 @@ export default function LibraryPagePremium() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination removed - using infinite scroll instead */}
|
||||||
{tracksData?.pagination && tracksData.pagination.total_pages > 1 && (
|
{/* Show loading indicator at bottom when fetching more */}
|
||||||
<Pagination
|
{isFetchingNextPage && (
|
||||||
currentPage={page}
|
<div className="flex justify-center items-center py-4">
|
||||||
totalPages={tracksData.pagination.total_pages}
|
<div className="text-kodo-secondary text-sm">Chargement...</div>
|
||||||
onPageChange={setPage}
|
</div>
|
||||||
totalItems={tracksData.pagination.total}
|
|
||||||
itemsPerPage={limit}
|
|
||||||
showItemsInfo={true}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<UploadModal open={isUploadModalOpen} onClose={handleCloseUpload} />
|
<UploadModal open={isUploadModalOpen} onClose={handleCloseUpload} />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue