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 */}
-
-
+
Repository name
- {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 */}
-
+
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;
}
}