diff --git a/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx b/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx index 9e2b595f..e71d14c7 100644 --- a/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx +++ b/SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx @@ -9,6 +9,13 @@ export interface SearchSelectOption { code?: string; /** Muted right-aligned text on the option row. */ hint?: string; + /** + * Muted second line under the label, for context too long to sit beside it. Search matches + * it as well, since it is text the reader can see on the row. + */ + sublabel?: string; + /** Native tooltip on the option row, for detail that would crowd it (a path, an id…). */ + title?: string; /** Fully custom option row; label/code/hint rendering is skipped. */ render?: ReactNode; } @@ -64,7 +71,10 @@ export function SearchSelect({ const needle = query.trim().toLowerCase(); if (!needle) return all; return all.filter( - (o) => o.label.toLowerCase().includes(needle) || o.code?.toLowerCase().includes(needle), + (o) => + o.label.toLowerCase().includes(needle) || + o.code?.toLowerCase().includes(needle) || + o.sublabel?.toLowerCase().includes(needle), ); }, [all, query]); @@ -97,29 +107,42 @@ export function SearchSelect({ + {/* The panel starts at the input's width but is free to outgrow it: several of these sit + in narrow filter bars, and an option is worth nothing if its name is the part that + gets truncated away. */} {filtered.map((o) => ( {o.render ?? ( <> - - {o.label} + + + {o.label} + + {o.sublabel && ( + {o.sublabel} + )} {o.code && ( {o.code} )} - {o.hint && {o.hint}} + {/* Shrinkable, unlike the label: whatever a hint adds, it is never the thing + worth reading if only one of the two can fit. */} + {o.hint && ( + {o.hint} + )} )} diff --git a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx index 7d6f76fe..c4bd7390 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx @@ -69,20 +69,36 @@ export function ExchangesPage() { * Every promoted key any information type declares, with the types that declare it. * Read off the list already fetched for the information-type filter, so offering the * keys costs nothing — and picking from real names beats remembering how one was spelled. + * + * Narrowed to the picked information type when there is one: a short list of its own keys + * beats the whole catalogue with a disambiguating line on every row. */ const propertyKeyOptions = useMemo(() => { - const owners = new Map(); - for (const t of infoTypes) + const scoped = query.informationTypeId + ? infoTypes.filter((t) => t.id === query.informationTypeId) + : infoTypes; + const owners = new Map(); + for (const t of scoped) for (const p of t.promotedProperties ?? []) { const carriers = owners.get(p.key) ?? []; - const name = t.code ?? t.name; - if (!carriers.includes(name)) carriers.push(name); + const type = t.code ?? t.name; + if (!carriers.some((c) => c.type === type)) carriers.push({ type, path: p.path }); owners.set(p.key, carriers); } return [...owners.entries()] .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, carriers]) => ({ value: key, label: key, hint: carriers.join(", ") })); - }, [infoTypes]); + .map(([key, carriers]) => ({ + value: key, + label: key, + // A second line rather than text beside the name: these run long enough + // ("InventoryTransactionPosted") that sharing one line left the name with no room at + // all. Dropped once a type is picked above — naming it again on every row says nothing. + sublabel: query.informationTypeId ? undefined : carriers.map((c) => c.type).join(", "), + // What the key actually reads out of the payload. The thing you want when a filter + // comes back empty and you can't tell whether the key or the value is wrong. + title: carriers.map((c) => `${c.type}: ${c.path}`).join("\n"), + })); + }, [infoTypes, query.informationTypeId]); /** Set (or drop) one URL param; changing any filter resets paging. */ const setParam = (key: string, value: string | null, resetOffset = true) => {