The storefront filter panel needs searchable multi-select for categories and tags #139

Closed
opened 2026-08-23 08:53:54 -05:00 by bermudalamb · 1 comment
Owner

The customer-facing filter panel does not scale with the taxonomy behind it.

Categories are an antd Tree in frontend/src/components/FilterDrawer.tsx, rendering the whole tree at whatever depth it has grown to, with no way to search it. Finding a category means expanding branches until you see it. The tree is also single-select — selectCategory sets filters.categoryId to one id, and picking the already-selected node clears it — so a customer cannot ask for two categories at once.

Tags are a flat wall of clickable Tag chips, every tag in the system rendered at once with no search. That is fine at a dozen tags and unusable at a hundred, and it grows every time an item is tagged with something new.

Neither control tells you what you have chosen without reading it back off the control itself. ActiveFilterChips shows the active filters elsewhere, but inside the panel the selection is only visible as tree highlighting and chip colour.

What this needs

A search input over the categories and one over the tags, so a customer types instead of scans.

Both as multi-select dropdowns that list the selections that have been made, so what is currently chosen is readable in the control rather than inferred from highlighting.

Things to work out when this is picked up

Multi-select categories is a backend change as well as a frontend one. categoryId is a single value the whole way down: the storefront filter, the query string, and the recursive CTE in backend/src/itemFilters.ts that matches a node and all its descendants. Selecting several categories has to decide whether the descendant expansion applies per selected node and whether multiple categories combine as OR — almost certainly OR, since a customer picking two categories wants both — which is deliberately different from tag filtering, where the existing rule is AND ("must have all", decision 8 in the project notes). Two adjacent controls with opposite combining rules needs to be obvious in the UI rather than a surprise.

Whether the category control keeps its hierarchy matters too. A flat searchable multi-select loses the parent/child relationship a customer uses to browse; antd's TreeSelect keeps the tree and supports both search and multiple, and the admin side already uses a tree select in CategoryTreeSelect.tsx, which is worth looking at first for consistency.

Malformed filter params currently return 400 rather than being ignored (decision 9), so the parsing for a multi-valued category needs to keep that property.

The customer-facing filter panel does not scale with the taxonomy behind it. Categories are an antd `Tree` in `frontend/src/components/FilterDrawer.tsx`, rendering the whole tree at whatever depth it has grown to, with no way to search it. Finding a category means expanding branches until you see it. The tree is also single-select — `selectCategory` sets `filters.categoryId` to one id, and picking the already-selected node clears it — so a customer cannot ask for two categories at once. Tags are a flat wall of clickable `Tag` chips, every tag in the system rendered at once with no search. That is fine at a dozen tags and unusable at a hundred, and it grows every time an item is tagged with something new. Neither control tells you what you have chosen without reading it back off the control itself. `ActiveFilterChips` shows the active filters elsewhere, but inside the panel the selection is only visible as tree highlighting and chip colour. ### What this needs A search input over the categories and one over the tags, so a customer types instead of scans. Both as multi-select dropdowns that list the selections that have been made, so what is currently chosen is readable in the control rather than inferred from highlighting. ### Things to work out when this is picked up Multi-select categories is a backend change as well as a frontend one. `categoryId` is a single value the whole way down: the storefront filter, the query string, and the recursive CTE in `backend/src/itemFilters.ts` that matches a node and all its descendants. Selecting several categories has to decide whether the descendant expansion applies per selected node and whether multiple categories combine as OR — almost certainly OR, since a customer picking two categories wants both — which is deliberately different from tag filtering, where the existing rule is AND ("must have all", decision 8 in the project notes). Two adjacent controls with opposite combining rules needs to be obvious in the UI rather than a surprise. Whether the category control keeps its hierarchy matters too. A flat searchable multi-select loses the parent/child relationship a customer uses to browse; antd's `TreeSelect` keeps the tree and supports both search and multiple, and the admin side already uses a tree select in `CategoryTreeSelect.tsx`, which is worth looking at first for consistency. Malformed filter params currently return `400` rather than being ignored (decision 9), so the parsing for a multi-valued category needs to keep that property.
bermudalamb added this to the Customer and Admin UI review findings project 2026-08-23 09:06:15 -05:00
bermudalamb self-assigned this 2026-08-23 09:06:28 -05:00
Author
Owner

Done on feature/139-searchable-multi-select-filters.

Decisions taken, since the issue left them open:

Categories combine as OR, tags stay AND. A customer picking Furniture and Decor wants both, not the empty intersection. Each selected id still expands to its descendants, so the answer is the union of the subtrees. Both headings now state their rule — "Categories — any of these" and "Tags — must have all of these" — rather than leaving two adjacent controls with opposite behaviour to be discovered.

The query parameter keeps its singular name and becomes comma-separated, which is the shape tags and status already use. So ?category=1 still parses, as a list of one. No alias, and no way to give the filter twice with two meanings.

The recursive CTE is seeded with = ANY($n::int[]) rather than one id. That walks every selected root in a single recursion and gives the OR for free, and because the outer match is IN, overlapping subtrees stay a set test — an item filed under two selected branches appears once.

Malformed stays 400 (decision 9). A list with one unreadable entry is refused rather than honoured in part: filtering on the readable half would answer a narrower question than the one asked and look indistinguishable from a filter that worked.

Categories keep their hierarchy in a TreeSelect, matching the admin's CategoryTreeSelect; tags become a multiple Select whose selected pills keep their colours, which is the only place a tag's colour was load-bearing.

Two things worth knowing:

The URL carries the separator percent-encoded — ?category=1%2C2 — because that is what URLSearchParams produces, and it is how tags has always looked. Both spellings parse.

The e2e page object types a category or tag name before clicking it. Not for realism: the option lists are virtualized, so against a database holding hundreds of categories the wanted row never renders until a search narrows to it.

Follow-on filed as #169 — the admin's inventory filters were left on the old row of controls, which now differs from the customer-facing panel.

Done on `feature/139-searchable-multi-select-filters`. Decisions taken, since the issue left them open: **Categories combine as OR, tags stay AND.** A customer picking Furniture and Decor wants both, not the empty intersection. Each selected id still expands to its descendants, so the answer is the union of the subtrees. Both headings now state their rule — "Categories — any of these" and "Tags — must have all of these" — rather than leaving two adjacent controls with opposite behaviour to be discovered. **The query parameter keeps its singular name and becomes comma-separated**, which is the shape `tags` and `status` already use. So `?category=1` still parses, as a list of one. No alias, and no way to give the filter twice with two meanings. **The recursive CTE is seeded with `= ANY($n::int[])`** rather than one id. That walks every selected root in a single recursion and gives the OR for free, and because the outer match is `IN`, overlapping subtrees stay a set test — an item filed under two selected branches appears once. **Malformed stays 400** (decision 9). A list with one unreadable entry is refused rather than honoured in part: filtering on the readable half would answer a narrower question than the one asked and look indistinguishable from a filter that worked. Categories keep their hierarchy in a `TreeSelect`, matching the admin's `CategoryTreeSelect`; tags become a multiple `Select` whose selected pills keep their colours, which is the only place a tag's colour was load-bearing. Two things worth knowing: The URL carries the separator percent-encoded — `?category=1%2C2` — because that is what `URLSearchParams` produces, and it is how `tags` has always looked. Both spellings parse. The e2e page object types a category or tag name before clicking it. Not for realism: the option lists are virtualized, so against a database holding hundreds of categories the wanted row never renders until a search narrows to it. Follow-on filed as #169 — the admin's inventory filters were left on the old row of controls, which now differs from the customer-facing panel.
bermudalamb added reference feature/139-searchable-multi-select-filters 2026-08-24 16:45:32 -05:00
bermudalamb moved this to Review in Customer and Admin UI review findings on 2026-08-24 16:48:04 -05:00
bermudalamb moved this to Ready for Release in Customer and Admin UI review findings on 2026-09-09 13:33:39 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#139