refactor(web): runMutation helper for generic-toast admin handlers (#375)

Wraps the dominant pattern (try { await fn } catch { pushToast(errMessage,
'error') }) for the 5 admin/+page.svelte handlers that surface errMessage
directly: onApprove, onReject, onResolve, onDeleteFile, onDeleteLidarr.

Tighter scope than originally planned — the 3 handlers in
admin/users/+page.svelte (onToggleAutoApprove, onGenerateInvite,
onRevokeInvite) use errCode + verb prefix ("Generate failed: ${code}")
which is structurally different and would change UX wire output if
forced through the helper. Skipping them keeps behavior identical.

The helper SWALLOWS errors so callers no longer need try/finally for
busy-state — `saving = true; await runMutation(...); saving = false;`
is correct because runMutation can't throw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 15:39:00 -04:00
parent 0eaaf66748
commit 6e0223a9b1
2 changed files with 40 additions and 21 deletions
+28
View File
@@ -0,0 +1,28 @@
// Wraps the dominant admin-handler mutation pattern:
// try { await fn(); pushToast(success?); }
// catch (e) { pushToast(errMessage(e), 'error'); }
//
// Errors are SWALLOWED — caller does not need a try/catch. Any
// `saving = true / saving = false` state flag can sit around the
// `await runMutation(...)` call without try/finally because the
// helper never rethrows.
//
// Use ONLY for handlers that surface errMessage(e) directly.
// Handlers that branch on specific error codes (last_admin,
// password_too_short) or build custom copy with errCode keep
// their inline catch — wrapping those would obscure intent.
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
export async function runMutation(
fn: () => Promise<unknown>,
opts: { successMessage?: string } = {}
): Promise<void> {
try {
await fn();
if (opts.successMessage) pushToast(opts.successMessage);
} catch (e) {
pushToast(errMessage(e), 'error');
}
}
+12 -21
View File
@@ -24,8 +24,9 @@
type ScanScheduleMode
} from '$lib/api/admin';
import { qk } from '$lib/api/queries';
import { errCode, errMessage } from '$lib/api/errors';
import { errCode } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
import { runMutation } from '$lib/util/runMutation';
import type {
AdminQuarantineRow,
LidarrRequest,
@@ -119,20 +120,16 @@
}
async function onApprove(r: LidarrRequest) {
try {
await runMutation(async () => {
await approveRequest(r.id);
await invalidateRequests();
} catch (e) {
pushToast(errMessage(e), 'error');
}
});
}
async function onReject(r: LidarrRequest) {
try {
await runMutation(async () => {
await rejectRequest(r.id);
await invalidateRequests();
} catch (e) {
pushToast(errMessage(e), 'error');
}
});
}
async function invalidateQuarantine() {
@@ -143,12 +140,10 @@
}
async function onResolve(row: AdminQuarantineRow) {
try {
await runMutation(async () => {
await resolveQuarantine(row.track_id);
await invalidateQuarantine();
} catch (e) {
pushToast(errMessage(e), 'error');
}
});
}
async function onDeleteFile(row: AdminQuarantineRow) {
const key = confirmKey(row.track_id, 'delete-file');
@@ -157,12 +152,10 @@
return;
}
clearConfirm();
try {
await runMutation(async () => {
await deleteQuarantineFile(row.track_id);
await invalidateQuarantine();
} catch (e) {
pushToast(errMessage(e), 'error');
}
});
}
async function onDeleteLidarr(row: AdminQuarantineRow) {
const key = confirmKey(row.track_id, 'delete-lidarr');
@@ -171,12 +164,10 @@
return;
}
clearConfirm();
try {
await runMutation(async () => {
await deleteQuarantineViaLidarr(row.track_id);
await invalidateQuarantine();
} catch (e) {
pushToast(errMessage(e), 'error');
}
});
}
// ---- Library scan ----