fix(admin): confirm writes succeeded and allow inline category creation (#23)
Reported from testing: the item form said "Item added" for a save that never happened. saveItem and the other admin calls returned res.json() without checking res.ok, so a 4xx/5xx resolved normally and every caller reported success for a write the server had rejected. That is worse than failing outright, because nothing prompts the user to look for the missing row. All admin calls now throw on a non-OK response, and the handlers report the error, keep the form open so entered values survive, and only claim success once the server has accepted the write. Mark sold/available previously did nothing visible on failure at all. Categories can now be created from the item form, as tags already could. Previously a category that did not exist yet meant abandoning a half-filled form for the Categories tab. New categories are created at the top level; nesting stays in the Categories tab. The control lives in its own component: inline, every keystroke re-rendered the whole Inventory component and rebuilt the category tree, which visibly jittered the open popup. It sits above the tree rather than below it, where a long list both hid it and made its position depend on the list's measured height. The tree no longer expands everything on open, which does not scale past a screenful; it has search instead. The app now honours prefers-reduced-motion by disabling antd transitions, and the e2e suite runs with that preference set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-6
@@ -67,32 +67,54 @@ export async function fetchFilterOptions(): Promise<FilterOptions> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Every admin call goes through this. Without the res.ok check a 4xx/5xx still
|
||||
// resolves — the caller then reports success for a write that never happened,
|
||||
// which is worse than failing outright because nothing prompts the user to look
|
||||
// for the missing row.
|
||||
async function expectOk(res: Response, action: string): Promise<Response> {
|
||||
if (res.ok) return res;
|
||||
const detail = await res.json().catch(() => null);
|
||||
throw new Error(detail?.error ? `${action}: ${detail.error}` : action);
|
||||
}
|
||||
|
||||
export async function fetchAdminItems(): Promise<Item[]> {
|
||||
const res = await fetch('/api/admin/items');
|
||||
const res = await expectOk(await fetch('/api/admin/items'), 'failed to load items');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function saveItem(id: number | null, formData: FormData): Promise<Item> {
|
||||
const url = id ? `/api/admin/items/${id}` : '/api/admin/items';
|
||||
const res = await fetch(url, { method: id ? 'PUT' : 'POST', body: formData });
|
||||
const res = await expectOk(
|
||||
await fetch(url, { method: id ? 'PUT' : 'POST', body: formData }),
|
||||
'failed to save item'
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteItem(id: number): Promise<void> {
|
||||
await fetch(`/api/admin/items/${id}`, { method: 'DELETE' });
|
||||
await expectOk(await fetch(`/api/admin/items/${id}`, { method: 'DELETE' }), 'failed to delete item');
|
||||
}
|
||||
|
||||
export async function deleteItemImage(itemId: number, imageId: number): Promise<void> {
|
||||
await fetch(`/api/admin/items/${itemId}/images/${imageId}`, { method: 'DELETE' });
|
||||
await expectOk(
|
||||
await fetch(`/api/admin/items/${itemId}/images/${imageId}`, { method: 'DELETE' }),
|
||||
'failed to remove image'
|
||||
);
|
||||
}
|
||||
|
||||
export async function markSold(id: number): Promise<Item> {
|
||||
const res = await fetch(`/api/admin/items/${id}/mark-sold`, { method: 'POST' });
|
||||
const res = await expectOk(
|
||||
await fetch(`/api/admin/items/${id}/mark-sold`, { method: 'POST' }),
|
||||
'failed to mark sold'
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function markAvailable(id: number): Promise<Item> {
|
||||
const res = await fetch(`/api/admin/items/${id}/mark-available`, { method: 'POST' });
|
||||
const res = await expectOk(
|
||||
await fetch(`/api/admin/items/${id}/mark-available`, { method: 'POST' }),
|
||||
'failed to mark available'
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user