Skip to content
Open
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
66 changes: 66 additions & 0 deletions __tests__/lib/hooks/useFilteredTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,72 @@ describe("mergePathsIntoTree", () => {
expect(tree).toHaveLength(0);
});

it("highlights an ancestor whose label matches the query, even though it is not a backend result", () => {
const paths: AncestorPath[] = [
{
matchIri: "urn:leaf",
matchLabel: "Document Collection Event",
ancestors: [
{ iri: "urn:root", label: "Event", child_count: 1 },
{ iri: "urn:ancestor", label: "Document Collection and Production Events", child_count: 1 },
],
},
];

const tree = mergePathsIntoTree(paths, "document");

// Root ("Event") does not match
expect(tree[0].isSearchMatch).toBeFalsy();
// Ancestor ("Document Collection and Production Events") matches the query
expect(tree[0].children[0].isSearchMatch).toBe(true);
// Leaf ("Document Collection Event") is a backend match AND label matches
expect(tree[0].children[0].children[0].isSearchMatch).toBe(true);
});

it("is case-insensitive when matching ancestor labels against the query", () => {
const paths: AncestorPath[] = [
{
matchIri: "urn:leaf",
matchLabel: "Leaf",
ancestors: [{ iri: "urn:ancestor", label: "Document Archive", child_count: 1 }],
},
];

const tree = mergePathsIntoTree(paths, "DOCUMENT");

expect(tree[0].isSearchMatch).toBe(true);
});

it("does not highlight ancestors when query is empty", () => {
const paths: AncestorPath[] = [
{
matchIri: "urn:leaf",
matchLabel: "Leaf",
ancestors: [{ iri: "urn:ancestor", label: "Some Ancestor", child_count: 1 }],
},
];

const tree = mergePathsIntoTree(paths, "");

expect(tree[0].isSearchMatch).toBeFalsy();
expect(tree[0].children[0].isSearchMatch).toBe(true);
});

it("does not highlight ancestors whose labels do not contain the query", () => {
const paths: AncestorPath[] = [
{
matchIri: "urn:leaf",
matchLabel: "Document Event",
ancestors: [{ iri: "urn:ancestor", label: "Unrelated Category", child_count: 1 }],
},
];

const tree = mergePathsIntoTree(paths, "document");

expect(tree[0].isSearchMatch).toBeFalsy();
expect(tree[0].children[0].isSearchMatch).toBe(true);
});

it("assigns entityType 'class' to all nodes", () => {
const paths: AncestorPath[] = [
{
Expand Down
1 change: 1 addition & 0 deletions components/editor/developer/DeveloperEditorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export function DeveloperEditorLayout(props: DeveloperEditorLayoutProps) {
projectId,
accessToken,
branch: activeBranch,
searchQuery,
});

const handleSearchSelect = (iri: string) => {
Expand Down
1 change: 1 addition & 0 deletions components/editor/standard/StandardEditorLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ export function StandardEditorLayout(props: StandardEditorLayoutProps) {
projectId,
accessToken,
branch: activeBranch,
searchQuery,
});

const handleSearchSelect = (iri: string) => {
Expand Down
16 changes: 11 additions & 5 deletions lib/hooks/useFilteredTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface UseFilteredTreeOptions {
projectId: string;
accessToken?: string;
branch?: string;
searchQuery?: string;
}

interface UseFilteredTreeReturn {
Expand All @@ -29,6 +30,7 @@ export function useFilteredTree({
projectId,
accessToken,
branch,
searchQuery,
}: UseFilteredTreeOptions): UseFilteredTreeReturn {
const [filteredNodes, setFilteredNodes] = useState<EntityTreeNode[] | null>(null);
const [isBuilding, setIsBuilding] = useState(false);
Expand Down Expand Up @@ -92,7 +94,7 @@ export function useFilteredTree({
if (buildId !== buildIdRef.current) return;

// Merge all ancestor paths into a unified tree
const tree = mergePathsIntoTree(ancestorPaths);
const tree = mergePathsIntoTree(ancestorPaths, searchQuery ?? "");

setFilteredNodes(tree);
setFirstMatchIri(limitedResults[0]?.iri ?? null);
Expand All @@ -107,7 +109,7 @@ export function useFilteredTree({
}
}
})();
}, [searchResults, projectId, accessToken, branch]);
}, [searchResults, searchQuery, projectId, accessToken, branch]);

return { filteredNodes, isBuilding, firstMatchIri, truncated };
}
Expand All @@ -121,8 +123,11 @@ export interface AncestorPath {
/**
* Merge multiple ancestor paths into a unified EntityTreeNode tree.
* Matched nodes get `isSearchMatch: true`, all ancestors are `isExpanded: true`.
* If `query` is provided, any node whose label contains the query (case-insensitive)
* is also marked as a match — not just the leaf IRIs the backend returned.
*/
export function mergePathsIntoTree(paths: AncestorPath[]): EntityTreeNode[] {
export function mergePathsIntoTree(paths: AncestorPath[], query = ""): EntityTreeNode[] {
const q = query.trim().toLowerCase();
// nodeMap: iri -> EntityTreeNode
const nodeMap = new Map<string, EntityTreeNode>();
// childrenMap: parentIri -> Set<childIri>
Expand All @@ -143,6 +148,7 @@ export function mergePathsIntoTree(paths: AncestorPath[]): EntityTreeNode[] {

for (let i = 0; i < fullPath.length; i++) {
const item = fullPath[i];
const labelMatches = q.length > 0 && item.label.toLowerCase().includes(q);

if (!nodeMap.has(item.iri)) {
nodeMap.set(item.iri, {
Expand All @@ -153,12 +159,12 @@ export function mergePathsIntoTree(paths: AncestorPath[]): EntityTreeNode[] {
isLoading: false,
hasChildren: item.hasChildren,
entityType: "class",
isSearchMatch: matchIris.has(item.iri),
isSearchMatch: matchIris.has(item.iri) || labelMatches,
});
} else {
const existing = nodeMap.get(item.iri)!;
existing.hasChildren = existing.hasChildren || item.hasChildren;
if (matchIris.has(item.iri)) {
if (matchIris.has(item.iri) || labelMatches) {
existing.isSearchMatch = true;
}
}
Expand Down
Loading