fix(playlists): an Undo puts a row back after its old neighbour, not at a stale index

A raw index recorded at removal drifted once anything changed before the Undo.
Both lists now read the list fresh and re-insert each row after the row that
preceded it (lib/reinsert). A failed watched toggle in a Plex playlist also
puts the row's state back: its local copy was never re-seeded.
This commit is contained in:
2026-09-27 14:02:15 +02:00
parent b456232d00
commit de62035889
9 changed files with 225 additions and 32 deletions
+21 -6
View File
@@ -24,6 +24,7 @@ import { notify } from "../lib/notifications";
import { notifyActionError } from "../lib/actionErrors";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { playlistName } from "../lib/playlistName";
import { reinsertAt } from "../lib/reinsert";
import { useUndoable } from "../lib/useUndoable";
import { cycleSort, type SortState } from "../lib/columnSort";
import { formatRuntime } from "../lib/format";
@@ -476,9 +477,10 @@ export default function Playlists() {
// so the guard reset the order: the removed row came back and the undo history the rebase had
// kept went with it. (A failure resyncs below, which puts the row back where it belongs.)
const before = qc.getQueryData<PlaylistDetail>(qk.playlist(listId));
// Where it stood in the STORED order — what the Undo puts it back at (the server's list, so a
// view sort on screen does not decide it).
const at = before?.items.findIndex((v) => v.id === video.id) ?? -1;
// The STORED order it stood in — the Undo puts it back after its neighbour there (the server's
// list, so a view sort on screen does not decide it; `reinsertAt`, so a change in between does
// not shift it off by the rows added or moved meanwhile).
const storedBefore = before?.items.map((v) => v.id) ?? [];
qc.setQueryData<PlaylistDetail>(qk.playlist(listId), (old) =>
old ? { ...old, items: old.items.filter((v) => v.id !== video.id) } : old,
);
@@ -503,10 +505,23 @@ export default function Playlists() {
action: {
label: t("common.undo"),
// ONE request that puts it back at its index (the server renumbers around it): an append
// plus a whole-list reorder would land it at the end whenever the second call failed.
// plus a whole-list reorder would land it at the end whenever the second call failed. The
// index is worked out against the list as it is NOW, fetched fresh.
onClick: () =>
void api
.addToPlaylist(listId, video.id, at < 0 ? undefined : at)
void qc
.fetchQuery({
queryKey: qk.playlist(listId),
queryFn: () => api.playlist(listId),
staleTime: 0,
})
.then((now) => {
const back = reinsertAt(
storedBefore,
[video.id],
now.items.map((v) => v.id),
);
if (back.keys.length) return api.addToPlaylist(listId, video.id, back.positions[0]);
})
.catch((e: unknown) => notifyActionError(e, t("playlists.undoRemoveFailed")))
.finally(() => {
refreshAll(listId);