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) => {