Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions SW.Bitween.Web/ClientApp/src/components/ui/SearchSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -97,29 +107,42 @@ export function SearchSelect({
<ChevronsUpDown className="size-3.5" aria-hidden />
</ComboboxButton>
</div>
{/* 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. */}
<ComboboxOptions
anchor={{ to: "bottom start", gap: 4 }}
className="z-50 max-h-64 w-(--input-width) overflow-auto rounded-lg border border-ink-200 bg-white py-1 shadow-lg empty:hidden"
className="z-50 max-h-64 max-w-[min(28rem,90vw)] min-w-(--input-width) overflow-auto rounded-lg border border-ink-200 bg-white py-1 shadow-lg empty:hidden"
>
{filtered.map((o) => (
<ComboboxOption
key={o.value || "∅"}
value={o.value}
title={o.title}
className="group flex cursor-pointer items-center gap-2 px-3 py-1.5 data-focus:bg-ink-50"
>
{o.render ?? (
<>
<span
className={`min-w-0 flex-1 truncate text-sm ${o.value === "" ? "text-ink-500 italic" : "text-ink-800"}`}
>
{o.label}
<span className="min-w-0 flex-1">
<span
className={`block truncate text-sm ${o.value === "" ? "text-ink-500 italic" : "text-ink-800"}`}
>
{o.label}
</span>
{o.sublabel && (
<span className="block truncate text-xs text-ink-400">{o.sublabel}</span>
)}
</span>
{o.code && (
<code className="shrink-0 rounded bg-ink-100 px-1.5 py-0.5 font-mono text-[11px] text-ink-600">
{o.code}
</code>
)}
{o.hint && <span className="shrink-0 text-xs text-ink-400">{o.hint}</span>}
{/* 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 && (
<span className="max-w-[50%] min-w-0 truncate text-xs text-ink-400">{o.hint}</span>
)}
<Check className="size-3.5 shrink-0 text-crimson-600 opacity-0 group-data-selected:opacity-100" aria-hidden />
</>
)}
Expand Down
28 changes: 22 additions & 6 deletions SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>();
for (const t of infoTypes)
const scoped = query.informationTypeId
? infoTypes.filter((t) => t.id === query.informationTypeId)
: infoTypes;
const owners = new Map<string, { type: string; path: string }[]>();
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) => {
Expand Down
Loading