Skip to content

Commit f3de21b

Browse files
feat(ui): Phase 2 — Project Explorer file tree in left sidebar
- Add FileTreeNode type and getFileTree() API method (GET /api/file-tree) - Add FileSelectionContext to propagate selected file/dir across views - Add ProjectFileTree component with expand/collapse, file icons, node count badges, inline search filter, keyboard navigation (arrow keys), and density progress bar - Wire ProjectFileTree into sidebar, replacing Phase 1 placeholder - Update CodeGraphView to show file-filter badge and filter drill-down nodes by selected file or directory from context - Add 13 Playwright E2E tests covering tree render, expand/collapse, search, keyboard nav, ARIA roles, and graph filter integration Closes RAN-73 Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent 0292d8d commit f3de21b

10 files changed

Lines changed: 1126 additions & 43 deletions

File tree

src/main/frontend/src/components/CodeGraphView.tsx

Lines changed: 83 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import * as d3 from 'd3';
33
import { useApi } from '@/hooks/useApi';
44
import { api } from '@/lib/api';
55
import type { KindEntry, NodeResponse, NodesListResponse } from '@/types/api';
6-
import { ChevronRight, Home } from 'lucide-react';
6+
import { ChevronRight, Home, X, FileCode } from 'lucide-react';
7+
import { useFileSelection } from '@/contexts/FileSelectionContext';
78

89
// Reuse the kind-color mapping from ExplorerView for consistency
910
const KIND_COLORS: Record<string, string> = {
@@ -231,6 +232,8 @@ export default function CodeGraphView() {
231232
const [drillTotal, setDrillTotal] = useState(0);
232233
const [drillLoading, setDrillLoading] = useState(false);
233234

235+
const { selectedPath, selectedType, clearSelection } = useFileSelection();
236+
234237
const { data: kindsData, loading: kindsLoading } = useApi(() => api.getKinds(), []);
235238

236239
// Observe container size
@@ -261,16 +264,34 @@ export default function CodeGraphView() {
261264
setDrillNodes([]);
262265
setDrillTotal(0);
263266
try {
264-
const result: NodesListResponse = await api.getNodesByKind(kind, 200, 0);
265-
setDrillNodes(result.nodes ?? []);
266-
setDrillTotal(result.total ?? result.count ?? (result.nodes ?? []).length);
267+
const result: NodesListResponse = await api.getNodesByKind(kind, 500, 0);
268+
const nodes = result.nodes ?? [];
269+
// If a file/directory is selected, filter nodes to that path
270+
const filtered = selectedPath
271+
? nodes.filter(n => {
272+
if (!n.file_path) return false;
273+
return selectedType === 'directory'
274+
? n.file_path.startsWith(selectedPath)
275+
: n.file_path === selectedPath;
276+
})
277+
: nodes;
278+
setDrillNodes(filtered);
279+
setDrillTotal(filtered.length);
267280
} catch {
268281
setDrillNodes([]);
269282
setDrillTotal(0);
270283
} finally {
271284
setDrillLoading(false);
272285
}
273-
}, []);
286+
}, [selectedPath, selectedType]);
287+
288+
// Re-run drill if file selection changes while already drilled into a kind
289+
useEffect(() => {
290+
if (selectedKind) {
291+
handleDrillDown(selectedKind);
292+
}
293+
// eslint-disable-next-line react-hooks/exhaustive-deps
294+
}, [selectedPath]);
274295

275296
const handleDrillUp = useCallback(() => {
276297
setSelectedKind(null);
@@ -280,52 +301,81 @@ export default function CodeGraphView() {
280301

281302
const kinds: KindEntry[] = kindsData?.kinds ?? [];
282303

304+
// Friendly display name for the selected path
305+
const selectedLabel = selectedPath
306+
? selectedPath.split('/').pop() ?? selectedPath
307+
: null;
308+
283309
return (
284310
<div className="flex flex-col h-full space-y-3">
285311
{/* Header + breadcrumb */}
286-
<div className="flex items-center justify-between">
312+
<div className="flex items-center justify-between flex-wrap gap-2">
287313
<div>
288314
<h1 className="text-2xl font-bold gradient-text">Code Graph</h1>
289315
<p className="text-sm text-surface-400 mt-0.5">
290316
{selectedKind
291-
? drillTotal > drillNodes.length
292-
? `Showing ${drillNodes.length} of ${drillTotal} "${selectedKind}" nodes`
293-
: `${drillNodes.length} nodes of kind "${selectedKind}"`
317+
? drillTotal > 0
318+
? `${drillTotal} "${selectedKind}" nodes${selectedPath ? ` in ${selectedLabel}` : ''}`
319+
: `No "${selectedKind}" nodes${selectedPath ? ` in ${selectedLabel}` : ''}`
294320
: `${kinds.length} node kinds — click a tile to explore`}
295321
</p>
296322
</div>
297323

298-
{/* Breadcrumb */}
299-
<nav className="flex items-center gap-1 text-sm">
300-
<button
301-
onClick={handleDrillUp}
302-
className={`flex items-center gap-1 px-2 py-1 rounded transition-colors ${
303-
selectedKind
304-
? 'text-brand-400 hover:text-brand-300 hover:bg-surface-800/50 cursor-pointer'
305-
: 'text-surface-500 cursor-default'
306-
}`}
307-
disabled={!selectedKind}
308-
>
309-
<Home className="w-3.5 h-3.5" />
310-
<span>All Kinds</span>
311-
</button>
312-
{selectedKind && (
313-
<>
314-
<ChevronRight className="w-3.5 h-3.5 text-surface-600" />
315-
<span
316-
className="px-2 py-1 rounded text-brand-300 font-medium"
317-
style={{ color: getKindColor(selectedKind) }}
318-
>
319-
{selectedKind.toUpperCase()}
324+
<div className="flex items-center gap-2">
325+
{/* Active file filter badge */}
326+
{selectedPath && (
327+
<div
328+
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs
329+
bg-surface-800/70 border border-surface-700/50 text-surface-300"
330+
data-testid="file-filter-badge"
331+
>
332+
<FileCode className="w-3 h-3 text-brand-400 shrink-0" />
333+
<span className="truncate max-w-[200px]" title={selectedPath}>
334+
{selectedLabel}
320335
</span>
321-
</>
336+
<button
337+
onClick={clearSelection}
338+
aria-label="Clear file filter"
339+
className="text-surface-500 hover:text-surface-300 ml-0.5"
340+
>
341+
<X className="w-3 h-3" />
342+
</button>
343+
</div>
322344
)}
323-
</nav>
345+
346+
{/* Breadcrumb */}
347+
<nav className="flex items-center gap-1 text-sm" aria-label="Graph breadcrumb">
348+
<button
349+
onClick={handleDrillUp}
350+
className={`flex items-center gap-1 px-2 py-1 rounded transition-colors ${
351+
selectedKind
352+
? 'text-brand-400 hover:text-brand-300 hover:bg-surface-800/50 cursor-pointer'
353+
: 'text-surface-500 cursor-default'
354+
}`}
355+
disabled={!selectedKind}
356+
>
357+
<Home className="w-3.5 h-3.5" />
358+
<span>All Kinds</span>
359+
</button>
360+
{selectedKind && (
361+
<>
362+
<ChevronRight className="w-3.5 h-3.5 text-surface-600" />
363+
<span
364+
className="px-2 py-1 rounded text-brand-300 font-medium"
365+
style={{ color: getKindColor(selectedKind) }}
366+
>
367+
{selectedKind.toUpperCase()}
368+
</span>
369+
</>
370+
)}
371+
</nav>
372+
</div>
324373
</div>
325374

326375
{/* Treemap container */}
327376
<div
328377
ref={containerRef}
378+
data-testid="graph-container"
329379
className="flex-1 rounded-xl border border-surface-800/50 bg-surface-900/40 overflow-hidden"
330380
>
331381
{(kindsLoading || drillLoading) && (

src/main/frontend/src/components/Layout.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { Button } from '@/components/ui/button';
2626
import { Separator } from '@/components/ui/separator';
2727
import SearchBar from './SearchBar';
2828
import { useTheme } from '@/hooks/useTheme';
29+
import { FileSelectionProvider } from '@/contexts/FileSelectionContext';
30+
import ProjectFileTree from './ProjectFileTree';
2931

3032
/* ------------------------------------------------------------------ */
3133
/* Right-panel context — lets child views open the details panel */
@@ -194,17 +196,12 @@ function Sidebar({ collapsed, mobileOpen, onCollapse, onMobileClose }: SidebarPr
194196
})}
195197
</nav>
196198

197-
{/* File tree placeholder */}
199+
{/* Project file tree */}
198200
{!collapsed && (
199201
<>
200202
<Separator />
201-
<div className="px-3 py-3">
202-
<p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-2">
203-
Project Files
204-
</p>
205-
<div className="text-xs text-muted-foreground/60 italic px-1">
206-
File tree coming in Phase 2
207-
</div>
203+
<div className="flex-1 min-h-0 overflow-hidden">
204+
<ProjectFileTree />
208205
</div>
209206
</>
210207
)}
@@ -342,6 +339,7 @@ export default function Layout() {
342339
};
343340

344341
return (
342+
<FileSelectionProvider>
345343
<RightPanelContext.Provider value={{ openPanel, closePanel, isOpen: rightPanelOpen }}>
346344
<div className="flex h-screen overflow-hidden bg-background text-foreground">
347345

@@ -405,5 +403,6 @@ export default function Layout() {
405403
</div>
406404
</div>
407405
</RightPanelContext.Provider>
406+
</FileSelectionProvider>
408407
);
409408
}

0 commit comments

Comments
 (0)