fix(web): surface a toast on failed mutations (#14)
Several client mutations either ignored a non-ok response (silent no-op) or toasted success unconditionally even when the request failed. Sweep them so every mutation gives feedback: - artists: toggle auto-monitor, unfollow → error toast on failure. - discography + wanted: toggle monitor / search-now → success or error toast. - discover suggestion follow/want/dismiss → error toast (was toasting success even on failure). - The Floor: queue request + retry → error toast on failure. - artist-modal + preview follow/want, Last.fm want → error toast on failure (closes the deferred "want() shows no toast on non-ok" item). web 153 tests, tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,8 @@ export function ArtistModal({
|
|||||||
if (res.ok || res.status === 409) {
|
if (res.ok || res.status === 409) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
toast(`Following ${name}`);
|
toast(`Following ${name}`);
|
||||||
|
} else {
|
||||||
|
toast(`Couldn't follow ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,8 @@ export function ArtistModal({
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setWanted((s) => new Set(s).add(r.rgMbid));
|
setWanted((s) => new Set(s).add(r.rgMbid));
|
||||||
toast(`Added ${r.album}`);
|
toast(`Added ${r.album}`);
|
||||||
|
} else {
|
||||||
|
toast(`Couldn't add ${r.album}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { PageHead } from "../../_ui/page-head";
|
import { PageHead } from "../../_ui/page-head";
|
||||||
import { CoverArt } from "../../_ui/cover-art";
|
import { CoverArt } from "../../_ui/cover-art";
|
||||||
import { AlbumModal } from "../../_ui/album-modal";
|
import { AlbumModal } from "../../_ui/album-modal";
|
||||||
|
import { toast } from "../../_ui/toast";
|
||||||
import { categoryOf, TAB_ORDER, type Category } from "../../_ui/categories";
|
import { categoryOf, TAB_ORDER, type Category } from "../../_ui/categories";
|
||||||
|
|
||||||
type Release = {
|
type Release = {
|
||||||
@@ -39,15 +40,17 @@ export function DiscographyClient({ id }: { id: string }) {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
async function toggleMonitor(r: Release) {
|
async function toggleMonitor(r: Release) {
|
||||||
await fetch(`/api/releases/${r.id}`, {
|
const res = await fetch(`/api/releases/${r.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ monitored: !r.monitored }),
|
body: JSON.stringify({ monitored: !r.monitored }),
|
||||||
});
|
}).catch(() => null);
|
||||||
|
if (!res?.ok) toast(`Couldn't update ${r.album}`);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
async function searchNow(r: Release) {
|
async function searchNow(r: Release) {
|
||||||
await fetch(`/api/releases/${r.id}/search`, { method: "POST" });
|
const res = await fetch(`/api/releases/${r.id}/search`, { method: "POST" }).catch(() => null);
|
||||||
|
toast(res?.ok ? `Searching for ${r.album}` : `Couldn't search for ${r.album}`);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,16 +95,18 @@ export function ArtistsClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAuto(a: Artist) {
|
async function toggleAuto(a: Artist) {
|
||||||
await fetch(`/api/artists/${a.id}`, {
|
const res = await fetch(`/api/artists/${a.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ autoMonitorFuture: !a.autoMonitorFuture }),
|
body: JSON.stringify({ autoMonitorFuture: !a.autoMonitorFuture }),
|
||||||
});
|
}).catch(() => null);
|
||||||
|
if (!res?.ok) toast(`Couldn't update ${a.name}`);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unfollow(a: Artist) {
|
async function unfollow(a: Artist) {
|
||||||
await fetch(`/api/artists/${a.id}`, { method: "DELETE" });
|
const res = await fetch(`/api/artists/${a.id}`, { method: "DELETE" }).catch(() => null);
|
||||||
|
if (!res?.ok) toast(`Couldn't unfollow ${a.name}`);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export function PreviewClient({ mbid, initialName }: { mbid: string; initialName
|
|||||||
if (res.ok || res.status === 409) {
|
if (res.ok || res.status === 409) {
|
||||||
setFollowed(true);
|
setFollowed(true);
|
||||||
toast(`Following ${name}`);
|
toast(`Following ${name}`);
|
||||||
|
} else {
|
||||||
|
toast(`Couldn't follow ${name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,8 @@ export function PreviewClient({ mbid, initialName }: { mbid: string; initialName
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setWanted((s) => new Set(s).add(r.rgMbid));
|
setWanted((s) => new Set(s).add(r.rgMbid));
|
||||||
toast(`Added ${r.album}`);
|
toast(`Added ${r.album}`);
|
||||||
|
} else {
|
||||||
|
toast(`Couldn't add ${r.album}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,11 +103,15 @@ export function DiscoverClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function act(id: string, action: "follow" | "want" | "dismiss") {
|
async function act(id: string, action: "follow" | "want" | "dismiss") {
|
||||||
await fetch(`/api/discover/${id}`, {
|
const res = await fetch(`/api/discover/${id}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ action }),
|
body: JSON.stringify({ action }),
|
||||||
});
|
}).catch(() => null);
|
||||||
|
if (!res?.ok) {
|
||||||
|
toast(`Couldn't ${action}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
toast(action === "follow" ? "Following" : action === "want" ? "Added to wanted" : "Dismissed");
|
toast(action === "follow" ? "Following" : action === "want" ? "Added to wanted" : "Dismissed");
|
||||||
loadFeed();
|
loadFeed();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ export function LastfmClient() {
|
|||||||
toast(`Wanted ${match.title}`);
|
toast(`Wanted ${match.title}`);
|
||||||
setWantedSet((s) => new Set(s).add(match.rgMbid));
|
setWantedSet((s) => new Set(s).add(match.rgMbid));
|
||||||
setAlbumTarget(null);
|
setAlbumTarget(null);
|
||||||
|
} else {
|
||||||
|
toast(`Couldn't add ${match.title}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -76,22 +76,28 @@ export function Queue() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function retry(id: string) {
|
async function retry(id: string) {
|
||||||
const res = await fetch(`/api/requests/${id}/retry`, { method: "POST" });
|
const res = await fetch(`/api/requests/${id}/retry`, { method: "POST" }).catch(() => null);
|
||||||
if (res.ok) {
|
if (!res?.ok) {
|
||||||
toast("Retrying…");
|
toast("Couldn't retry");
|
||||||
refresh();
|
return;
|
||||||
}
|
}
|
||||||
|
toast("Retrying…");
|
||||||
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit(e: React.FormEvent) {
|
async function submit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!artist.trim() || !album.trim()) return;
|
if (!artist.trim() || !album.trim()) return;
|
||||||
const label = album.trim();
|
const label = album.trim();
|
||||||
await fetch("/api/requests", {
|
const res = await fetch("/api/requests", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({ artist, album }),
|
body: JSON.stringify({ artist, album }),
|
||||||
});
|
}).catch(() => null);
|
||||||
|
if (!res?.ok) {
|
||||||
|
toast(`Couldn't queue ${label}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
toast(`Queued ${label}`);
|
toast(`Queued ${label}`);
|
||||||
setArtist("");
|
setArtist("");
|
||||||
setAlbum("");
|
setAlbum("");
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export function WantedClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function searchNow(w: Wanted) {
|
async function searchNow(w: Wanted) {
|
||||||
await fetch(`/api/releases/${w.id}/search`, { method: "POST" });
|
const res = await fetch(`/api/releases/${w.id}/search`, { method: "POST" }).catch(() => null);
|
||||||
toast(`Searching for ${w.album}`);
|
toast(res?.ok ? `Searching for ${w.album}` : `Couldn't search for ${w.album}`);
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user