fix(admin): keep the active inventory filter and surface background-swap failures (#293)

handleBackgrounds re-read /api/admin/items unfiltered and called setItems(all) after every remove-backgrounds or restore-originals call, so an admin who had filtered Inventory to one category and opened an item from that filtered view saw the table silently repopulate with the entire unfiltered catalogue the moment the request resolved. Every other mutation in this file goes through load(), which respects the active filters; this one didn't, for no reason the spec required.

The fix reuses load() instead: it now hands back the rows it fetched (previously discarded after setItems), and handleBackgrounds picks the edited item's fresh row out of that filtered result to refresh the open modal, rather than issuing a second unfiltered fetch. There is no GET /api/admin/items/:id route to fetch a single item directly, and the remove-backgrounds/restore-originals routes return only a summary, not the item, so load()'s own result is what's actually available. A background swap never touches the fields anything filters on, so the edited item stays in the filtered result whenever it was in it before.

Also added a catch to handleBackgrounds, matching the message.error shape every sibling handler (handleDelete, handleDeleteImage, handleStatusChange) already uses — previously a network drop or a malformed JSON body became an unhandled rejection with no toast, silently different from how the rest of the file reports failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 10:44:07 -05:00
co-authored by Claude Opus 5
parent 385d5b89bf
commit dbb63bc3d2
+16 -9
View File
@@ -85,11 +85,15 @@ function Inventory() {
return fetchAdminItems(active)
.then(rows => {
if (seq === latestRequest.current) setItems(rows);
// Handed back so a caller that needs one fresh row (handleBackgrounds)
// can pick it out of this filtered fetch instead of issuing its own
// second, unfiltered one.
return rows;
})
// Without this the table simply keeps showing whatever it had, so a
// failed refetch after a save looks identical to a save that did not
// change anything.
.catch(() => message.error('Could not load items'));
.catch(() => { message.error('Could not load items'); return undefined; });
}, [filters]);
// The item form needs the current category tree and tag list; both change
@@ -213,6 +217,7 @@ function Inventory() {
* showing the previous files and the button looks like it did nothing.
*/
async function handleBackgrounds(itemId: number, action: 'remove-backgrounds' | 'restore-originals') {
const label = action === 'remove-backgrounds' ? 'remove backgrounds' : 'restore originals';
setBusyBackgrounds(true);
try {
const res = await fetch(`/api/admin/items/${itemId}/${action}`, { method: 'POST' });
@@ -231,14 +236,16 @@ function Inventory() {
message.success('Done.');
}
// Re-read the item so the thumbnails match what is now on the server.
const fresh = await fetch('/api/admin/items');
if (fresh.ok) {
const all: Item[] = await fresh.json();
const updated = all.find((candidate) => candidate.id === itemId);
if (updated) setEditingItem(prev => (prev && prev.id === itemId ? updated : prev));
setItems(all);
}
// Re-read the list through load() — same as every other mutation here —
// so a filtered view survives this, then pick this item's fresh images
// back out of it for the open modal. The item's own filtered fields
// (category, tags, status, search) are untouched by a background swap,
// so it stays in the result whenever it was in it before.
const rows = await load();
const updated = rows?.find(candidate => candidate.id === itemId);
if (updated) setEditingItem(prev => (prev && prev.id === itemId ? updated : prev));
} catch (err) {
message.error(`Couldn't ${label}${(err as Error).message}`);
} finally {
setBusyBackgrounds(false);
}