feat(ui): storefront filters and admin category/tag management (#23)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m49s
Tests / backend-unit (pull_request) Successful in 37s
Tests / backend-integration (pull_request) Failing after 3h2m23s
Tests / frontend-e2e (pull_request) Failing after 3m54s

Storefront gains a Filters drawer holding the category tree, colour-coded
tag pills, and a price range, with applied filters shown as removable
chips. Filter state lives in the URL query string, so a filtered view is
shareable and the back button works. Item cards now show their category
and tags.

Admin gains Categories and Tags tabs, and the item form gains a category
TreeSelect plus a tags Select that creates new tags on the fly.

The admin category tree tracks expansion in state rather than using
defaultExpandAll: that prop is evaluated once at mount, so a branch added
afterwards rendered collapsed and its children were unreachable. Creating
or moving a node now expands its parent. Caught by the new admin e2e spec.

The chip row is marked as a named group so its "Clear all" stays
distinguishable from the drawer's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 09:34:37 -05:00
co-authored by Claude Opus 5
parent 9222e97deb
commit d28fb5634a
13 changed files with 1374 additions and 22 deletions
@@ -169,9 +169,12 @@ described in `.claude/project-context.md` with no nginx change.
- `GET|POST /api/admin/categories`, `PUT|DELETE /api/admin/categories/:id`
- `GET|POST /api/admin/tags`, `PUT|DELETE /api/admin/tags/:id`
`PUT /api/admin/categories/:id` accepts `name`, `parent_id`, and `sort_order`. Reparenting is
validated against cycles server-side — a node may not become its own descendant — returning `400`.
`DELETE` responds with the affected counts so the UI can confirm before committing.
`GET /api/admin/categories` returns each node with an `item_count`, which is what lets the admin UI
state the blast radius of a delete before calling it — the tree and the counts are already on the
client, so no extra preview endpoint is needed. `PUT /api/admin/categories/:id` accepts `name`,
`parent_id`, and `sort_order`; reparenting is validated against cycles server-side — a node may not
become its own descendant — returning `400`. `DELETE` reports what it actually did, as
`{ deleted_categories, uncategorized_items }`.
`POST`/`PUT /api/admin/items` gain two multipart fields:
@@ -209,6 +212,9 @@ own line on mobile. The drawer enters from the right at every screen size (`plac
near-full-width below the `md` breakpoint), with a sticky footer holding "Clear all" and
"Show N items".
The chip row is marked `role="group" aria-label="Active filters"`, which keeps its "Clear all"
distinguishable from the identically-labelled control in the drawer.
The tag section is labelled **"Tags — must have all"** so that selecting a second tag and watching
the grid shrink reads as intentional rather than broken.
@@ -219,7 +225,9 @@ the grid shrink reads as intentional rather than broken.
Two tabs added beside Inventory, Customers, and Settings:
- **Categories** — antd `Tree` with drag-to-reparent, inline add/rename/delete, delete confirm
naming the affected subcategory and item counts
naming the affected subcategory and item counts. Expansion is controlled state rather than
`defaultExpandAll`: that prop is evaluated once at mount, so a branch created afterwards would
render collapsed and its children be unreachable. Creating or moving a node expands its parent.
- **Tags** — list with rename, colour override from a palette, and delete showing the item count
The item modal gains a category `TreeSelect` (with an explicit Uncategorized option) and a tags
@@ -249,8 +257,19 @@ user rule. Existing files keep their barrel imports — churning them is out of
**E2E** (`frontend/tests/e2e/filters.spec.ts`)
- Open the drawer, filter by category, tags, and price; assert the grid narrows
- Remove a chip and assert the grid widens
- A nested category is selectable, not just the roots
- Remove a chip and assert the grid widens; "Clear all" resets everything
- Reload a filtered URL and assert the filters are restored
- Tags render on the item card
**E2E** (`frontend/tests/e2e/admin-taxonomy.spec.ts`)
- Create a category and a nested child, asserting the child stays visible
- Create a tag and confirm a colour was assigned
- The item form exposes category and tag fields
Both e2e specs run against a database that is never reset between runs, so fixture names carry a
per-run suffix, and `filters.spec.ts` treats re-entry into its `beforeAll` as a no-op — a worker can
be handed the same spec file in more than one batch, and seeding twice would duplicate every item.
## Out of scope