fix(playlists): a refused edit is taken back on screen, and a gone list or tag is said, not swallowed
Deep-review round 4, the playlist surfaces. - A failed move or reorder relied on the resync to put the rows back — but the server's list is unchanged, so the refetch comes back deep-equal, react-query keeps the same `data` object, and the re-seed effect keyed on it never ran: the moved row stayed gone, the rejected order stayed on screen. Both restore from the cached detail now. - A failed removal puts its row back itself: when the server is unreachable the resync fails too, and round 3's "keep the list on a failed refetch" guard then kept showing it as removed. - The Plex playlist page (remove / rename / delete / reorder), the add-to-playlist popover and the tag manager had no failure handling at all: a list or tag deleted elsewhere (a 404, now a structured `not_found` the request layer leaves to the caller) failed in silence and an optimistic removal stayed removed. Each says it and resyncs; deleting a Plex playlist that is already gone just leaves the page. - Playlists' own loading/error branches use the shared `LoadingState` / `StateMessage` (the loading line had no live region). - The "player stays mounted" tests count mounts: a remount ended with the same flag as staying up.
This commit is contained in:
@@ -31,7 +31,8 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api, type PlexCard } from "../lib/api";
|
||||
import { api, HttpError, type PlexCard } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { formatRuntime } from "../lib/format";
|
||||
import { getAccountRaw, LS, setAccountRaw } from "../lib/storage";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
@@ -401,6 +402,17 @@ export default function PlexPlaylistView({
|
||||
qc.invalidateQueries({ queryKey: qk.plexPlaylists() });
|
||||
};
|
||||
|
||||
// Every edit here is optimistic on the local copy, and none of them used to handle a failure: a
|
||||
// removal that failed stayed removed on screen (the resync never ran), and a 404 — the playlist
|
||||
// deleted on another device, now a structured `not_found` — was an unhandled rejection that said
|
||||
// nothing at all (deep-review round 4; the YouTube side was fixed in round 3). A failure is said,
|
||||
// and the rows are put back from what the server last sent — explicitly, because a resync that
|
||||
// returns the same list keeps the same `q.data` and the re-sync effect above never runs.
|
||||
const putBack = () => {
|
||||
if (q.data) setItems(q.data.items);
|
||||
invalidate();
|
||||
};
|
||||
|
||||
function commit(next: PlexCard[]) {
|
||||
setItems(next);
|
||||
api
|
||||
@@ -408,7 +420,11 @@ export default function PlexPlaylistView({
|
||||
playlistId,
|
||||
next.map((i) => i.id),
|
||||
)
|
||||
.then(() => qc.invalidateQueries({ queryKey: qk.plexPlaylists() }));
|
||||
.then(() => qc.invalidateQueries({ queryKey: qk.plexPlaylists() }))
|
||||
.catch((e: unknown) => {
|
||||
notifyActionError(e, t("plex.playlist.reorderFailed"));
|
||||
putBack();
|
||||
});
|
||||
}
|
||||
|
||||
async function onRemove(keys: string[]) {
|
||||
@@ -420,6 +436,9 @@ export default function PlexPlaylistView({
|
||||
if (keys.length === 1) await api.plexPlaylistRemoveItem(playlistId, keys[0]!);
|
||||
else await api.plexPlaylistRemoveBulk(playlistId, keys);
|
||||
invalidate();
|
||||
} catch (e) {
|
||||
notifyActionError(e, t("plex.playlist.removeFailed"));
|
||||
putBack();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -428,7 +447,12 @@ export default function PlexPlaylistView({
|
||||
async function rename() {
|
||||
const title = name.trim();
|
||||
if (!title) return;
|
||||
await api.plexRenamePlaylist(playlistId, title);
|
||||
try {
|
||||
await api.plexRenamePlaylist(playlistId, title);
|
||||
} catch (e) {
|
||||
notifyActionError(e, t("plex.playlist.renameFailed")); // the field stays open to try again
|
||||
return;
|
||||
}
|
||||
setRenaming(false);
|
||||
invalidate();
|
||||
}
|
||||
@@ -440,7 +464,16 @@ export default function PlexPlaylistView({
|
||||
}))
|
||||
)
|
||||
return;
|
||||
await api.plexDeletePlaylist(playlistId);
|
||||
try {
|
||||
await api.plexDeletePlaylist(playlistId);
|
||||
} catch (e) {
|
||||
// Gone already (deleted on another device): what was asked for is true — leave the dead page.
|
||||
// Anything else is a real failure, and the page stays.
|
||||
if (!(e instanceof HttpError && e.status === 404)) {
|
||||
notifyActionError(e, t("plex.playlist.deleteFailed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
qc.invalidateQueries({ queryKey: qk.plexPlaylists() });
|
||||
onBack();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user