diff --git a/__tests__/lib/hooks/useFilteredTree.test.ts b/__tests__/lib/hooks/useFilteredTree.test.ts index afc55306..5cebdc4c 100644 --- a/__tests__/lib/hooks/useFilteredTree.test.ts +++ b/__tests__/lib/hooks/useFilteredTree.test.ts @@ -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[] = [ { diff --git a/app/projects/[id]/settings/page.tsx b/app/projects/[id]/settings/page.tsx index e420d8d4..0c846e41 100644 --- a/app/projects/[id]/settings/page.tsx +++ b/app/projects/[id]/settings/page.tsx @@ -2883,7 +2883,10 @@ function RemoteSyncSection({ {/* Repository */}
-
-
- {isWebhookDriven && ( -

- Repository fields are managed by the GitHub integration. -

- )} - {/* Same-repo info when editing form matches GitHub integration */} {!isWebhookDriven && githubIntegration && repoOwner === githubIntegration.repo_owner && @@ -2939,7 +2939,10 @@ function RemoteSyncSection({ {/* Branch + File path */}
-
-
+ {isWebhookDriven && ( +

+ Repository fields are managed by the GitHub integration. +

+ )} + {/* Frequency + Update mode */}
diff --git a/components/editor/developer/DeveloperEditorLayout.tsx b/components/editor/developer/DeveloperEditorLayout.tsx index d6638d7b..fce7b083 100644 --- a/components/editor/developer/DeveloperEditorLayout.tsx +++ b/components/editor/developer/DeveloperEditorLayout.tsx @@ -331,6 +331,7 @@ export function DeveloperEditorLayout(props: DeveloperEditorLayoutProps) { projectId, accessToken, branch: activeBranch, + searchQuery, }); const handleSearchSelect = (iri: string) => { diff --git a/components/editor/standard/StandardEditorLayout.tsx b/components/editor/standard/StandardEditorLayout.tsx index 7bdeca78..ed932f32 100644 --- a/components/editor/standard/StandardEditorLayout.tsx +++ b/components/editor/standard/StandardEditorLayout.tsx @@ -296,6 +296,7 @@ export function StandardEditorLayout(props: StandardEditorLayoutProps) { projectId, accessToken, branch: activeBranch, + searchQuery, }); const handleSearchSelect = (iri: string) => { diff --git a/lib/hooks/useFilteredTree.ts b/lib/hooks/useFilteredTree.ts index 25e02af8..49258428 100644 --- a/lib/hooks/useFilteredTree.ts +++ b/lib/hooks/useFilteredTree.ts @@ -11,6 +11,7 @@ interface UseFilteredTreeOptions { projectId: string; accessToken?: string; branch?: string; + searchQuery?: string; } interface UseFilteredTreeReturn { @@ -29,6 +30,7 @@ export function useFilteredTree({ projectId, accessToken, branch, + searchQuery, }: UseFilteredTreeOptions): UseFilteredTreeReturn { const [filteredNodes, setFilteredNodes] = useState(null); const [isBuilding, setIsBuilding] = useState(false); @@ -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); @@ -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(); // childrenMap: parentIri -> Set @@ -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, { @@ -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; } }