feat(playlists): act on the selected rows from the toolbar, and undo either

While rows are selected the toolbar's second row is the band: the count,
the last target as one click, Move to, Remove, Clear selection. Both write
in one request and undo back to the places; a video the target held before
stays there. Delete takes the selection when there is one. A failure puts
the rows back ticked.

A row's own move goes through the same write, so it undoes too, and the
single-row move route and service function are gone. A playlist row drops
its grip below a 24rem table: the row menu keeps the moves.
This commit is contained in:
2026-11-01 22:07:09 +01:00
parent 3b85881af4
commit efbbb23525
24 changed files with 1114 additions and 236 deletions
+49 -7
View File
@@ -77,16 +77,58 @@ export const playlistsApi = {
}),
removeFromPlaylist: (id: number, videoId: string) =>
req(`/api/playlists/${id}/items/${videoId}`, { method: "DELETE" }),
// One request on purpose: add-then-remove from the client leaves the video in BOTH lists when
// the second call fails. The server does it in one transaction.
moveToPlaylist: (
// One request on purpose: add-then-remove from the client leaves a video in BOTH lists when the
// second call fails. The server does it in one transaction, and says which landed in the target
// and which it already held: the Undo needs to tell them apart.
moveManyToPlaylist: (
id: number,
videoId: string,
videoIds: string[],
targetPlaylistId: number,
): Promise<{ ok: true; from_playlist_id: number; to_playlist_id: number; video_id: string }> =>
req(`/api/playlists/${id}/items/${videoId}/move`, {
): Promise<{
ok: true;
from_playlist_id: number;
to_playlist_id: number;
moved: string[];
already_there: string[];
}> =>
req(`/api/playlists/${id}/items/move-bulk`, {
method: "POST",
body: JSON.stringify({ target_playlist_id: targetPlaylistId }),
body: JSON.stringify({ video_ids: videoIds, target_playlist_id: targetPlaylistId }),
}),
// The Undo of a move, back into `id`. `positions` pairs with `moved` + `alreadyThere`.
undoPlaylistMove: (
id: number,
fromPlaylistId: number,
moved: string[],
alreadyThere: string[],
positions: number[],
): Promise<{ ok: true; playlist_id: number; added: number }> =>
req(`/api/playlists/${id}/items/undo-move`, {
method: "POST",
body: JSON.stringify({
from_playlist_id: fromPlaylistId,
moved,
already_there: alreadyThere,
positions,
}),
}),
removeManyFromPlaylist: (
id: number,
videoIds: string[],
): Promise<{ ok: true; playlist_id: number; removed: number }> =>
req(`/api/playlists/${id}/items/remove-bulk`, {
method: "POST",
body: JSON.stringify({ video_ids: videoIds }),
}),
// The Undo of a removal, in one request; an index in the stored order per video.
restoreToPlaylist: (
id: number,
videoIds: string[],
positions: number[],
): Promise<{ ok: true; playlist_id: number; added: number }> =>
req(`/api/playlists/${id}/items/restore`, {
method: "POST",
body: JSON.stringify({ video_ids: videoIds, positions }),
}),
reorderPlaylist: (id: number, videoIds: string[]) =>
req(`/api/playlists/${id}/order`, {