Skip to content
Closed
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
32 changes: 22 additions & 10 deletions app/projects/[id]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2883,7 +2883,10 @@ function RemoteSyncSection({
{/* Repository */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
<label className={cn(
"mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300",
isWebhookDriven && "opacity-60"
)}>
Repository owner
</label>
<input
Expand All @@ -2901,7 +2904,10 @@ function RemoteSyncSection({
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
<label className={cn(
"mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300",
isWebhookDriven && "opacity-60"
)}>
Repository name
</label>
<input
Expand All @@ -2920,12 +2926,6 @@ function RemoteSyncSection({
</div>
</div>

{isWebhookDriven && (
<p className="text-xs text-indigo-500 dark:text-indigo-400">
Repository fields are managed by the GitHub integration.
</p>
)}

{/* Same-repo info when editing form matches GitHub integration */}
{!isWebhookDriven && githubIntegration &&
repoOwner === githubIntegration.repo_owner &&
Expand All @@ -2939,7 +2939,10 @@ function RemoteSyncSection({
{/* Branch + File path */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
<label className={cn(
"mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300",
isWebhookDriven && "opacity-60"
)}>
Branch
</label>
<input
Expand All @@ -2957,7 +2960,10 @@ function RemoteSyncSection({
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
<label className={cn(
"mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300",
isWebhookDriven && "opacity-60"
)}>
File path
</label>
<input
Expand All @@ -2976,6 +2982,12 @@ function RemoteSyncSection({
</div>
</div>

{isWebhookDriven && (
<p className="text-xs text-indigo-500 dark:text-indigo-400">
Repository fields are managed by the GitHub integration.
</p>
)}

{/* Frequency + Update mode */}
<div className="grid grid-cols-2 gap-3">
<div>
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
14 changes: 10 additions & 4 deletions lib/hooks/useFilteredTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
projectId: string;
accessToken?: string;
branch?: string;
searchQuery?: string;
}

interface UseFilteredTreeReturn {
Expand All @@ -29,6 +30,7 @@
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 @@
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 @@
}
}
})();
}, [searchResults, projectId, accessToken, branch]);

Check warning on line 112 in lib/hooks/useFilteredTree.ts

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'searchQuery'. Either include it or remove the dependency array

return { filteredNodes, isBuilding, firstMatchIri, truncated };
}
Expand All @@ -121,8 +123,11 @@
/**
* 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 @@

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 @@
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