From 1e51ffbde07b133287bdbd54404119ede0383487 Mon Sep 17 00:00:00 2001 From: R-Hart80 Date: Mon, 13 Apr 2026 17:08:25 -0300 Subject: [PATCH 1/2] fix: dim labels and reposition note for webhook-driven readonly fields When a project is webhook-driven, the four GitHub integration input fields (repo owner, repo name, branch, file path) were already marked readOnly and visually styled as disabled on the inputs themselves. This commit extends the visual treatment to their labels by applying opacity-60 when isWebhookDriven is true, and moves the explanatory note ("Repository fields are managed by the GitHub integration.") to appear after all four fields so it clearly covers the whole group. Co-Authored-By: Claude Sonnet 4.6 --- app/projects/[id]/settings/page.tsx | 32 ++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) 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 */}
From 89aee90693cbd5bea68cfcfd14ab9bcf9665f723 Mon Sep 17 00:00:00 2001 From: R-Hart80 Date: Mon, 11 May 2026 11:02:56 -0300 Subject: [PATCH 2/2] fix: highlight ancestor nodes whose labels match the search query mergePathsIntoTree now accepts a query string and marks any node isSearchMatch when its label contains the query (case-insensitive), not only the leaf IRIs returned by the backend. Plumbed searchQuery from useTreeSearch through UseFilteredTreeOptions into the merge. Four new tests cover: ancestor highlight, case-insensitivity, empty query no-op, and non-matching ancestor stays unhighlighted. Fixes #209. Co-Authored-By: Claude Sonnet 4.6 --- __tests__/lib/hooks/useFilteredTree.test.ts | 66 +++++++++++++++++++ .../developer/DeveloperEditorLayout.tsx | 1 + .../editor/standard/StandardEditorLayout.tsx | 1 + lib/hooks/useFilteredTree.ts | 14 ++-- 4 files changed, 78 insertions(+), 4 deletions(-) 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/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; } }