Deep-review round 5, over round 4's own fixes.
- `notifyActionError` preferred the server's `detail` over the caller's translated fallback, and
`reportFailure` deliberately leaves 403/404/429 to the caller — so for exactly those statuses the
dozen call sites round 4 converted started printing whatever English a route hand-wrote into an
otherwise Hungarian UI ("No user with that email.", "Not subscribed to this channel", "Unknown
job"). A STRUCTURED detail is still preferred: `req` has already run it through `localizeDetail`,
so it is both localized and more specific. A plain string is not.
- The other half of the same defect, on the backend: round 4's `get_or_404(what="channel")` sweep
reached exactly ONE of the five identical sites in channels.py and shipped the HU/EN string with
it, so the Block button said it in Hungarian and the four controls beside it in English. All five
are converted, and the subscription / share-recipient / scheduler-job 404s with them (two new
reasons in both locales).
- `PlexCollectionEditor`'s add/remove, take-over and delete were try/finally with no catch — in the
very file round 4 edited to give `createNew` one. 403 and 404 are the caller's to report, so the
tick simply never appeared and a CONFIRMED "delete collection" did nothing, silently.
- `UploadDialog` is the only `uploadReq` caller and kept an empty catch whose comment still claimed
the global error modal had it. It had not since `reportFailure` became shared: a proxy 502 raised
only the self-clearing connection notice and a 413/403/404 nothing at all, so the dialog snapped
back to its pre-upload state with no error named. The user's own Cancel stays silent — and since
a cancel and a network drop both carry status 0, that difference is now a named `isAborted`
rather than a string literal at each end.
Regression tests for each (5 vitest, 2 pytest, plus two existing assertions corrected), all
mutation-checked: 6 mutations, each red on the assertion it is named after. vitest 1007,
pytest 1127.
29 lines
1.7 KiB
TypeScript
29 lines
1.7 KiB
TypeScript
// From the core, not the `./api` barrel: these two are pure (no request), and a test that mocks the
|
|
// barrel wholesale to stub its requests must not take the error path's own plumbing with it.
|
|
import { alreadyReported, HttpError } from "./api/core";
|
|
import { notify } from "./notifications";
|
|
|
|
/**
|
|
* Surface a failed write action as an error toast instead of the click silently doing nothing.
|
|
* Prefers the server's own reason over the caller's already-translated generic fallback — but only
|
|
* when the server sent a STRUCTURED detail (`{code, reason}`), which `req` has already run through
|
|
* `localizeDetail`. A plain-string `detail` is whatever English the route hand-wrote, and
|
|
* `reportFailure` deliberately leaves 403/404/429 to the caller, so exactly those statuses used to
|
|
* put the route's English sentence into an otherwise Hungarian UI — "No user with that email.",
|
|
* "Not subscribed to this channel" — where the caller's own translated fallback was right there
|
|
* (deep-review round 5). Domain-agnostic: used by the Plex watched-toggles and the saved-view ops,
|
|
* and delegated to by `notifyYouTubeActionError` for its non-403 case.
|
|
*/
|
|
export function notifyActionError(
|
|
err: unknown,
|
|
fallbackMessage: string,
|
|
level: "error" | "warning" = "error",
|
|
): void {
|
|
// Already on screen (`req` opened the error dialog, or the tab is being signed out): a toast on
|
|
// top reported one fault twice. Playlists filtered this by status by hand; the watched toggles,
|
|
// the saved views and a playlist delete did not.
|
|
if (alreadyReported(err)) return;
|
|
const localized = err instanceof HttpError && err.detailData != null ? err.detail : null;
|
|
notify({ level, message: localized || fallbackMessage });
|
|
}
|