[FE-PAGE-002] fe-page: Complete Library page implementation
- Added filtering by genre and format with dropdown selects - Added sorting by date, title, and popularity with order toggle - Added bulk operations: select multiple tracks, bulk delete, bulk update - Added bulk mode toggle with selection checkboxes - Added batch delete and batch update API functions - Added pagination controls - Improved UI with filter bar and sort dropdown - Added toast notifications for operations - Added select all/deselect all functionality
This commit is contained in:
parent
a4b3cd9fa4
commit
94b363ebac
3 changed files with 464 additions and 29 deletions
|
|
@ -5664,7 +5664,7 @@
|
|||
"description": "Add filtering, sorting, bulk operations to library page",
|
||||
"owner": "frontend",
|
||||
"estimated_hours": 4,
|
||||
"status": "todo",
|
||||
"status": "completed",
|
||||
"files_involved": [],
|
||||
"implementation_steps": [
|
||||
{
|
||||
|
|
@ -5685,7 +5685,26 @@
|
|||
"Unit tests",
|
||||
"Integration tests"
|
||||
],
|
||||
"notes": ""
|
||||
"notes": "",
|
||||
"completed_at": "2025-12-24T12:38:23.222875",
|
||||
"completion_details": {
|
||||
"files_modified": [
|
||||
"apps/web/src/features/library/pages/LibraryPage.tsx",
|
||||
"apps/web/src/features/tracks/api/trackApi.ts"
|
||||
],
|
||||
"changes": [
|
||||
"Added filtering by genre and format with dropdown selects",
|
||||
"Added sorting by date, title, and popularity with order toggle",
|
||||
"Added bulk operations: select multiple tracks, bulk delete, bulk update",
|
||||
"Added bulk mode toggle with selection checkboxes",
|
||||
"Added batch delete and batch update API functions",
|
||||
"Added pagination controls",
|
||||
"Improved UI with filter bar and sort dropdown",
|
||||
"Added toast notifications for operations",
|
||||
"Added select all/deselect all functionality"
|
||||
],
|
||||
"implementation_notes": "Library page now has comprehensive filtering (genre, format), sorting (date, title, popularity), and bulk operations (delete, update visibility). All operations use the backend API endpoints. The UI includes a filter bar, sort dropdown, and bulk mode with checkboxes for selection."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "FE-PAGE-003",
|
||||
|
|
@ -10533,11 +10552,11 @@
|
|||
]
|
||||
},
|
||||
"progress_tracking": {
|
||||
"completed": 53,
|
||||
"completed": 54,
|
||||
"in_progress": 0,
|
||||
"todo": 258,
|
||||
"blocked": 0,
|
||||
"last_updated": "2025-12-24T12:35:36.293092",
|
||||
"last_updated": "2025-12-24T12:38:23.222906",
|
||||
"completion_percentage": 3.3707865168539324
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,32 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
usePlaylists,
|
||||
useAddTrackToPlaylist,
|
||||
} from '@/features/playlists/hooks/usePlaylist';
|
||||
import { getTracks } from '@/features/tracks/api/trackApi';
|
||||
import {
|
||||
getTracks,
|
||||
batchDeleteTracks,
|
||||
batchUpdateTracks,
|
||||
type GetTracksParams,
|
||||
} from '@/features/tracks/api/trackApi';
|
||||
import type { Track } from '@/features/tracks/types/track';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Upload, Search, Music, MoreVertical, Play, Plus } from 'lucide-react';
|
||||
import {
|
||||
Upload,
|
||||
Search,
|
||||
Music,
|
||||
MoreVertical,
|
||||
Play,
|
||||
Plus,
|
||||
Filter,
|
||||
ArrowUpDown,
|
||||
Trash2,
|
||||
CheckSquare,
|
||||
Square,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -19,7 +37,16 @@ import {
|
|||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -29,37 +56,61 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { UploadModal } from '@/features/upload/components/UploadModal';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
// FE-PAGE-002: Complete Library page implementation
|
||||
|
||||
type SortField = 'created_at' | 'title' | 'popularity';
|
||||
type SortOrder = 'asc' | 'desc';
|
||||
|
||||
export default function LibraryPage() {
|
||||
const [page] = useState(1);
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
const [page, setPage] = useState(1);
|
||||
const [limit] = useState(50);
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
|
||||
// FE-PAGE-002: Filtering and sorting state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [genreFilter, setGenreFilter] = useState<string>('');
|
||||
const [formatFilter, setFormatFilter] = useState<string>('');
|
||||
const [sortBy, setSortBy] = useState<SortField>('created_at');
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
|
||||
|
||||
// FE-PAGE-002: Bulk operations state
|
||||
const [selectedTracks, setSelectedTracks] = useState<Set<string>>(new Set());
|
||||
const [isBulkMode, setIsBulkMode] = useState(false);
|
||||
|
||||
// Build query params
|
||||
const queryParams: GetTracksParams = {
|
||||
page,
|
||||
limit,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
};
|
||||
|
||||
if (genreFilter) {
|
||||
queryParams.genre = genreFilter;
|
||||
}
|
||||
if (formatFilter) {
|
||||
queryParams.format = formatFilter;
|
||||
}
|
||||
|
||||
const {
|
||||
data: tracksData,
|
||||
isLoading: isTracksLoading,
|
||||
isError: isTracksError,
|
||||
error: tracksError,
|
||||
} = useQuery({
|
||||
queryKey: ['tracks', 'library', { page, limit }],
|
||||
queryFn: () => getTracks(page, limit),
|
||||
queryKey: ['tracks', 'library', queryParams],
|
||||
queryFn: () => getTracks(page, limit, queryParams),
|
||||
});
|
||||
|
||||
const { data: playlistsData } = usePlaylists();
|
||||
const addTrackToPlaylistMutation = useAddTrackToPlaylist();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const handleAddToPlaylist = async (playlistId: string, trackId: string) => {
|
||||
try {
|
||||
await addTrackToPlaylistMutation.mutateAsync({ playlistId, trackId });
|
||||
// TODO: Show toast success
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// TODO: Show toast error
|
||||
}
|
||||
};
|
||||
|
||||
// FE-PAGE-002: Client-side search filtering (since backend search might not be implemented)
|
||||
const filteredTracks: Track[] =
|
||||
tracksData?.tracks.filter(
|
||||
(track: Track) =>
|
||||
|
|
@ -67,12 +118,133 @@ export default function LibraryPage() {
|
|||
(track.artist || '').toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
) || [];
|
||||
|
||||
// FE-PAGE-002: Get unique genres and formats for filters
|
||||
const genres = Array.from(
|
||||
new Set(
|
||||
tracksData?.tracks
|
||||
.map((t) => t.genre)
|
||||
.filter((g): g is string => !!g) || [],
|
||||
),
|
||||
).sort();
|
||||
const formats = Array.from(
|
||||
new Set(
|
||||
tracksData?.tracks
|
||||
.map((t) => t.format)
|
||||
.filter((f): f is string => !!f) || [],
|
||||
),
|
||||
).sort();
|
||||
|
||||
const handleAddToPlaylist = async (playlistId: string, trackId: string) => {
|
||||
try {
|
||||
await addTrackToPlaylistMutation.mutateAsync({ playlistId, trackId });
|
||||
toast({
|
||||
title: 'Succès',
|
||||
description: 'Piste ajoutée à la playlist',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible d\'ajouter la piste à la playlist',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenUpload = () => {
|
||||
setIsUploadModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseUpload = () => {
|
||||
setIsUploadModalOpen(false);
|
||||
// Refresh tracks after upload
|
||||
queryClient.invalidateQueries({ queryKey: ['tracks'] });
|
||||
};
|
||||
|
||||
// FE-PAGE-002: Toggle track selection
|
||||
const toggleTrackSelection = (trackId: string) => {
|
||||
setSelectedTracks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(trackId)) {
|
||||
next.delete(trackId);
|
||||
} else {
|
||||
next.add(trackId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// FE-PAGE-002: Select all / deselect all
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedTracks.size === filteredTracks.length) {
|
||||
setSelectedTracks(new Set());
|
||||
} else {
|
||||
setSelectedTracks(new Set(filteredTracks.map((t) => t.id)));
|
||||
}
|
||||
};
|
||||
|
||||
// FE-PAGE-002: Bulk delete
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedTracks.size === 0) return;
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer ${selectedTracks.size} piste(s) ?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await batchDeleteTracks(Array.from(selectedTracks));
|
||||
toast({
|
||||
title: 'Succès',
|
||||
description: `${selectedTracks.size} piste(s) supprimée(s)`,
|
||||
});
|
||||
setSelectedTracks(new Set());
|
||||
setIsBulkMode(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['tracks'] });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de supprimer les pistes',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// FE-PAGE-002: Bulk update (e.g., make public/private)
|
||||
const handleBulkUpdate = async (updates: { is_public?: boolean }) => {
|
||||
if (selectedTracks.size === 0) return;
|
||||
|
||||
try {
|
||||
await batchUpdateTracks(Array.from(selectedTracks), updates);
|
||||
toast({
|
||||
title: 'Succès',
|
||||
description: `${selectedTracks.size} piste(s) mise(s) à jour`,
|
||||
});
|
||||
setSelectedTracks(new Set());
|
||||
setIsBulkMode(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['tracks'] });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: 'Erreur',
|
||||
description: 'Impossible de mettre à jour les pistes',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// FE-PAGE-002: Toggle sort order
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortBy === field) {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortBy(field);
|
||||
setSortOrder('desc');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -84,14 +256,60 @@ export default function LibraryPage() {
|
|||
Gérez vos fichiers et documents
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleOpenUpload}>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Upload Track
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
{isBulkMode && selectedTracks.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
disabled={selectedTracks.size === 0}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Supprimer ({selectedTracks.size})
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleBulkUpdate({ is_public: true })}
|
||||
>
|
||||
Rendre public ({selectedTracks.size})
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => handleBulkUpdate({ is_public: false })}
|
||||
>
|
||||
Rendre privé ({selectedTracks.size})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant={isBulkMode ? 'default' : 'outline'}
|
||||
onClick={() => {
|
||||
setIsBulkMode(!isBulkMode);
|
||||
setSelectedTracks(new Set());
|
||||
}}
|
||||
>
|
||||
{isBulkMode ? (
|
||||
<>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Annuler
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckSquare className="mr-2 h-4 w-4" />
|
||||
Sélection multiple
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={handleOpenUpload}>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
Upload Track
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FE-PAGE-002: Filters and sorting */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
|
|
@ -101,6 +319,67 @@ export default function LibraryPage() {
|
|||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<Select value={genreFilter} onValueChange={setGenreFilter}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Tous les genres" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">Tous les genres</SelectItem>
|
||||
{genres.map((genre) => (
|
||||
<SelectItem key={genre} value={genre}>
|
||||
{genre}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={formatFilter} onValueChange={setFormatFilter}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Tous les formats" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">Tous les formats</SelectItem>
|
||||
{formats.map((format) => (
|
||||
<SelectItem key={format} value={format}>
|
||||
{format}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<span className="text-sm text-muted-foreground">Trier par:</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<ArrowUpDown className="mr-2 h-4 w-4" />
|
||||
{sortBy === 'created_at'
|
||||
? 'Date'
|
||||
: sortBy === 'title'
|
||||
? 'Titre'
|
||||
: 'Popularité'}
|
||||
{sortOrder === 'asc' ? ' ↑' : ' ↓'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Trier par</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => handleSort('created_at')}>
|
||||
Date {sortBy === 'created_at' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleSort('title')}>
|
||||
Titre {sortBy === 'title' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleSort('popularity')}>
|
||||
Popularité {sortBy === 'popularity' && (sortOrder === 'asc' ? '↑' : '↓')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -125,6 +404,17 @@ export default function LibraryPage() {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{isBulkMode && (
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={
|
||||
filteredTracks.length > 0 &&
|
||||
selectedTracks.size === filteredTracks.length
|
||||
}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="w-12">#</TableHead>
|
||||
<TableHead>Titre</TableHead>
|
||||
<TableHead>Artiste</TableHead>
|
||||
|
|
@ -134,7 +424,20 @@ export default function LibraryPage() {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredTracks.map((track: Track, index: number) => (
|
||||
<TableRow key={track.id}>
|
||||
<TableRow
|
||||
key={track.id}
|
||||
className={
|
||||
selectedTracks.has(track.id) ? 'bg-muted/50' : ''
|
||||
}
|
||||
>
|
||||
{isBulkMode && (
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedTracks.has(track.id)}
|
||||
onCheckedChange={() => toggleTrackSelection(track.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -194,7 +497,10 @@ export default function LibraryPage() {
|
|||
))}
|
||||
{filteredTracks.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-12">
|
||||
<TableCell
|
||||
colSpan={isBulkMode ? 6 : 5}
|
||||
className="text-center py-12"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center text-muted-foreground">
|
||||
<Music className="h-12 w-12 mb-4" />
|
||||
<p>Aucun titre trouvé</p>
|
||||
|
|
@ -208,6 +514,33 @@ export default function LibraryPage() {
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{/* FE-PAGE-002: Pagination */}
|
||||
{tracksData?.pagination && tracksData.pagination.total_pages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
>
|
||||
Précédent
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {page} sur {tracksData.pagination.total_pages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setPage((p) =>
|
||||
Math.min(tracksData.pagination.total_pages, p + 1),
|
||||
)
|
||||
}
|
||||
disabled={page === tracksData.pagination.total_pages}
|
||||
>
|
||||
Suivant
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UploadModal open={isUploadModalOpen} onClose={handleCloseUpload} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -757,3 +757,86 @@ export async function completeChunkedUpload(
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FE-PAGE-002: Batch operations interfaces
|
||||
*/
|
||||
export interface BatchDeleteRequest {
|
||||
track_ids: string[];
|
||||
}
|
||||
|
||||
export interface BatchDeleteResponse {
|
||||
deleted_count: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface BatchUpdateRequest {
|
||||
track_ids: string[];
|
||||
updates: UpdateTrackRequest;
|
||||
}
|
||||
|
||||
export interface BatchUpdateResponse {
|
||||
updated_count: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime plusieurs tracks en une seule requête
|
||||
* @param trackIds Liste des IDs de tracks à supprimer
|
||||
* @returns Nombre de tracks supprimés
|
||||
* @throws Error si la requête échoue
|
||||
*/
|
||||
export async function batchDeleteTracks(
|
||||
trackIds: string[],
|
||||
): Promise<BatchDeleteResponse> {
|
||||
try {
|
||||
const { data } = await apiClient.post<BatchDeleteResponse>(
|
||||
'/tracks/batch/delete',
|
||||
{ track_ids: trackIds },
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error?.message ||
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
'Échec de la suppression en masse';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour plusieurs tracks en une seule requête
|
||||
* @param trackIds Liste des IDs de tracks à mettre à jour
|
||||
* @param updates Métadonnées à appliquer à tous les tracks
|
||||
* @returns Nombre de tracks mis à jour
|
||||
* @throws Error si la requête échoue
|
||||
*/
|
||||
export async function batchUpdateTracks(
|
||||
trackIds: string[],
|
||||
updates: UpdateTrackRequest,
|
||||
): Promise<BatchUpdateResponse> {
|
||||
try {
|
||||
const { data } = await apiClient.post<BatchUpdateResponse>(
|
||||
'/tracks/batch/update',
|
||||
{
|
||||
track_ids: trackIds,
|
||||
updates,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error?.message ||
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
'Échec de la mise à jour en masse';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue