fix(api): a write that hit a connection failure is still reported, and every failure is reported once
Deep-review round 4, the "one report per failure" contract round 3 introduced. - `req` marked gateway (502/503/504) and network failures "already reported" because they raise the connection notice. But that notice is about connectivity and removes itself on the very next answer — which every caller's own resync refetch fetches at once — so a delete that hit a keepalive reset flashed the notice and was then reported nowhere, the list still there. Only the durable reports (the error dialog, the 401 sign-out) count now; the caller says the action failed. - A connection notice the user closed swallowed every later one for the rest of the visit; it is raised again. - `req` and `uploadReq` share one failure policy (`reportFailure`). The upload copy had drifted: a 502 opened the blocking "Server error (502)" dialog and cleared a notice that was still true. - A taken Plex collection name is a structured 409, localized, and the create/rename calls are quiet: it was an English dialog plus a Hungarian toast. `notifyCollectionEditError` is gone. - The mutation onErrors that toasted unconditionally after a non-quiet request (invite approve / deny / add, demo whitelist, audit clear, scheduler, share, full history, edit, config tests, tag attach) go through `notifyActionError`; the playlist delete's hand-rolled copy of it too (it takes a level now). The user role / suspend / delete actions had no onError at all: a user deleted in another tab failed silently and stayed listed — now said, and the list refetched.
This commit is contained in:
@@ -38,6 +38,7 @@ import { useConfirm } from "./ConfirmProvider";
|
||||
import Tabs, { usePersistedTab } from "./ui/Tabs";
|
||||
import { PageToolbar } from "./ui/PageShell";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
|
||||
// Admin-only user & access management: roles, access requests (the Invite whitelist), and the
|
||||
// demo whitelist + reset. Each section is its own tab (persisted across reloads); the Access tab
|
||||
@@ -120,6 +121,13 @@ function UsersRoles({ me }: { me: Me }) {
|
||||
n.delete(id);
|
||||
return n;
|
||||
});
|
||||
// A failed user action: said once (notifyActionError skips what `req` already put in its dialog),
|
||||
// and the list refetched — a user deleted in another tab would otherwise stay listed, every click
|
||||
// on the row doing nothing.
|
||||
const onGoneOrFailed = (e: unknown, fallback: string) => {
|
||||
notifyActionError(e, fallback);
|
||||
qc.invalidateQueries({ queryKey: qk.adminUsers() });
|
||||
};
|
||||
const setRole = useMutation({
|
||||
mutationFn: ({ id, role }: { id: number; role: "user" | "admin"; email: string }) =>
|
||||
api.setUserRole(id, role),
|
||||
@@ -129,7 +137,9 @@ function UsersRoles({ me }: { me: Me }) {
|
||||
notify({ level: "success", message: t("users.roles.updated", { email: vars.email }) });
|
||||
},
|
||||
onSettled: (_d, _e, vars) => endPending(vars.id),
|
||||
// Errors (e.g. last-admin / self) surface via the global error dialog with the server's detail.
|
||||
// Guards (last admin / self) surface via the global error dialog with the server's detail; what
|
||||
// it does not show (a user deleted meanwhile, a 404) is said here — see onGoneOrFailed.
|
||||
onError: (e) => onGoneOrFailed(e, t("users.roles.failed")),
|
||||
});
|
||||
|
||||
const suspend = useMutation({
|
||||
@@ -146,7 +156,8 @@ function UsersRoles({ me }: { me: Me }) {
|
||||
});
|
||||
},
|
||||
onSettled: (_d, _e, vars) => endPending(vars.id),
|
||||
// Guards (demo / self / last admin) surface via the global error dialog.
|
||||
// Guards (demo / self / last admin) surface via the global error dialog; the rest, as above.
|
||||
onError: (e) => onGoneOrFailed(e, t("users.suspend.failed")),
|
||||
});
|
||||
const del = useMutation({
|
||||
mutationFn: ({ id }: { id: number; email: string }) => api.adminDeleteUser(id),
|
||||
@@ -156,6 +167,7 @@ function UsersRoles({ me }: { me: Me }) {
|
||||
notify({ level: "success", message: t("users.delete.done", { email: vars.email }) });
|
||||
},
|
||||
onSettled: (_d, _e, vars) => endPending(vars.id),
|
||||
onError: (e) => onGoneOrFailed(e, t("users.delete.failed")),
|
||||
});
|
||||
|
||||
const onToggle = async (u: AdminUserRow) => {
|
||||
@@ -518,7 +530,7 @@ function AdminInvites() {
|
||||
refresh();
|
||||
notify({ level: "success", message: t("settings.invites.approved", { email: vars.email }) });
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.invites.approveFailed") }),
|
||||
onError: (e) => notifyActionError(e, t("settings.invites.approveFailed")),
|
||||
});
|
||||
const deny = useMutation({
|
||||
mutationFn: (id: number) => api.denyInvite(id),
|
||||
@@ -526,7 +538,7 @@ function AdminInvites() {
|
||||
refresh();
|
||||
notify({ level: "success", message: t("settings.invites.denied") });
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.invites.denyFailed") }),
|
||||
onError: (e) => notifyActionError(e, t("settings.invites.denyFailed")),
|
||||
});
|
||||
const onDeny = async (i: { id: number; email: string }) => {
|
||||
const ok = await confirm({
|
||||
@@ -630,7 +642,7 @@ function AdminInvites() {
|
||||
refresh();
|
||||
notify({ level: "success", message: t("settings.invites.addedToWhitelist", { email }) });
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.invites.addFailed") }),
|
||||
onError: (e) => notifyActionError(e, t("settings.invites.addFailed")),
|
||||
});
|
||||
|
||||
const list = invites.data ?? [];
|
||||
@@ -780,7 +792,7 @@ function AdminDemo() {
|
||||
qc.invalidateQueries({ queryKey: qk.demoWhitelist() });
|
||||
notify({ level: "success", message: t("settings.demo.added", { email }) });
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.demo.addFailed") }),
|
||||
onError: (e) => notifyActionError(e, t("settings.demo.addFailed")),
|
||||
});
|
||||
const remove = useMutation({
|
||||
mutationFn: (id: number) => api.removeDemoWhitelist(id),
|
||||
@@ -788,7 +800,7 @@ function AdminDemo() {
|
||||
qc.invalidateQueries({ queryKey: qk.demoWhitelist() });
|
||||
notify({ level: "success", message: t("settings.demo.removed") });
|
||||
},
|
||||
onError: () => notify({ level: "error", message: t("settings.demo.removeFailed") }),
|
||||
onError: (e) => notifyActionError(e, t("settings.demo.removeFailed")),
|
||||
});
|
||||
const onRemove = async (r: { id: number; email: string }) => {
|
||||
const ok = await confirm({
|
||||
@@ -806,7 +818,7 @@ function AdminDemo() {
|
||||
level: "success",
|
||||
message: t("settings.demo.resetDone", { count: r.playlists_seeded }),
|
||||
}),
|
||||
onError: () => notify({ level: "error", message: t("settings.demo.resetFailed") }),
|
||||
onError: (e) => notifyActionError(e, t("settings.demo.resetFailed")),
|
||||
});
|
||||
|
||||
const rows = list.data ?? [];
|
||||
|
||||
Reference in New Issue
Block a user