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
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The customer-facing filter panel does not scale with the taxonomy behind it.
Categories are an antd
Treeinfrontend/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 —selectCategorysetsfilters.categoryIdto 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
Tagchips, 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.
ActiveFilterChipsshows 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.
categoryIdis a single value the whole way down: the storefront filter, the query string, and the recursive CTE inbackend/src/itemFilters.tsthat 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
TreeSelectkeeps the tree and supports both search and multiple, and the admin side already uses a tree select inCategoryTreeSelect.tsx, which is worth looking at first for consistency.Malformed filter params currently return
400rather than being ignored (decision 9), so the parsing for a multi-valued category needs to keep that property.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
tagsandstatusalready use. So?category=1still 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 isIN, 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'sCategoryTreeSelect; tags become a multipleSelectwhose 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 whatURLSearchParamsproduces, and it is howtagshas 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.