Problem
Searching the Class tree highlights only the leaf-level matches that the backend's search endpoint returned. If an ancestor also contains the query string in its label, that ancestor is not highlighted — even though it's just as much a valid match from the user's perspective.
Example reported by a user searching for document:
> Event
> Conflict
> Unarmed Conflict
> Legal Dispute Events
> Litigation Events
> Discovery Events
> Document Collection and Production Events ← contains "Document", NOT highlighted
* Document Collection Event ← contains "Document", highlighted
Both nodes contain "document" in their label, but only the leaf is rendered with the <mark> highlight + tree-search-match class. The user has to mentally re-scan every ancestor row to find any other matches — the whole point of search highlighting is to avoid that.
Where the bug is
lib/hooks/useFilteredTree.ts, function mergePathsIntoTree at line 125-164.
for (const path of paths) {
matchIris.add(path.matchIri); // only the leaf "matchIri" goes into matchIris
...
for (let i = 0; i < fullPath.length; i++) {
const item = fullPath[i];
if (!nodeMap.has(item.iri)) {
nodeMap.set(item.iri, {
...
isSearchMatch: matchIris.has(item.iri), // only true for leaves
});
} else {
const existing = nodeMap.get(item.iri)!;
...
if (matchIris.has(item.iri)) {
existing.isSearchMatch = true;
}
}
...
}
}
matchIris is populated only from path.matchIri (the deepest match for each search-result path). The ancestor IRIs in fullPath[0 .. n-1] are added to the tree as expanded context but never checked against the search query — even when their labels would match.
The downstream rendering in EntityTreeNode.tsx:233-235 correctly defers to node.isSearchMatch, so the fix is upstream in the merge.
Proposed fix
Pass the active search query (or its lowercase form) into mergePathsIntoTree, and mark isSearchMatch: true for any node whose label contains the query — not just the leaf-level matches the backend returned.
export function mergePathsIntoTree(
paths: AncestorPath[],
query: string, // ← new param, e.g. "document"
): EntityTreeNode[] {
const q = query.trim().toLowerCase();
...
// When inserting / updating each node:
const labelMatches = q.length > 0 && item.label.toLowerCase().includes(q);
isSearchMatch: matchIris.has(item.iri) || labelMatches,
...
}
The caller (useFilteredTree) already has the query in scope via searchResults — though it's not exposed by name there. Either:
- (A) Plumb the query down from
useTreeSearch.searchQuery through to useFilteredTree, or
- (B) Derive the query from
searchResults (each EntitySearchResult was returned by a query that the hook can hold onto when it triggered the search). Cleaner to just pass it in explicitly.
I'd go with (A): add searchQuery: string | null to UseFilteredTreeOptions, pass it to mergePathsIntoTree, and let the merge highlight any label containing it.
Done criteria
Out of scope
- Expanding the backend's
searchEntities endpoint to return all transitive matches. The frontend fix is more robust because it stays in sync with whatever ranking / limit the backend applies, and works retroactively without an API change.
- Highlighting matches in node labels that aren't in the rendered tree (e.g., siblings of an ancestor that didn't make the search results). Those nodes aren't visible anyway; nothing to highlight.
- Fuzzy / regex / multi-token search semantics. The bug is just about case-insensitive substring containment, which is the existing behavior in
highlightMatch.
Related
Other v0.5.0 polish items: #204, #205, #206, #207, #208. This one is purely in useFilteredTree and a small caller plumb-through; could ship in any of the chrome-polish PRs or its own.
Problem
Searching the Class tree highlights only the leaf-level matches that the backend's search endpoint returned. If an ancestor also contains the query string in its label, that ancestor is not highlighted — even though it's just as much a valid match from the user's perspective.
Example reported by a user searching for
document:Both nodes contain "document" in their label, but only the leaf is rendered with the
<mark>highlight +tree-search-matchclass. The user has to mentally re-scan every ancestor row to find any other matches — the whole point of search highlighting is to avoid that.Where the bug is
lib/hooks/useFilteredTree.ts, functionmergePathsIntoTreeat line 125-164.matchIrisis populated only frompath.matchIri(the deepest match for each search-result path). The ancestor IRIs infullPath[0 .. n-1]are added to the tree as expanded context but never checked against the search query — even when their labels would match.The downstream rendering in
EntityTreeNode.tsx:233-235correctly defers tonode.isSearchMatch, so the fix is upstream in the merge.Proposed fix
Pass the active search query (or its lowercase form) into
mergePathsIntoTree, and markisSearchMatch: truefor any node whose label contains the query — not just the leaf-level matches the backend returned.The caller (
useFilteredTree) already has the query in scope viasearchResults— though it's not exposed by name there. Either:useTreeSearch.searchQuerythrough touseFilteredTree, orsearchResults(eachEntitySearchResultwas returned by a query that the hook can hold onto when it triggered the search). Cleaner to just pass it in explicitly.I'd go with (A): add
searchQuery: string | nulltoUseFilteredTreeOptions, pass it tomergePathsIntoTree, and let the merge highlight any label containing it.Done criteria
documentin a tree where bothDocument Collection Event(leaf) andDocument Collection and Production Events(ancestor) exist highlights both nodes' labels.highlightMatchinEntityTreeNode.tsx:34-45).useFilteredTree(if any) is extended; new test asserts a multi-level match path highlights every matching ancestor.Out of scope
searchEntitiesendpoint to return all transitive matches. The frontend fix is more robust because it stays in sync with whatever ranking / limit the backend applies, and works retroactively without an API change.highlightMatch.Related
Other v0.5.0 polish items: #204, #205, #206, #207, #208. This one is purely in
useFilteredTreeand a small caller plumb-through; could ship in any of the chrome-polish PRs or its own.