From 4efa22cd95fe5d02f3dfd400bb6e042200109614 Mon Sep 17 00:00:00 2001 From: Zadak Date: Wed, 1 Jul 2026 15:22:05 +0200 Subject: [PATCH 1/4] fix(graph-ui): navigate Windows drive breadcrumbs to real paths The index file picker built breadcrumb targets as '/' + segments, so on a Windows drive path (C:/Users/rap) clicking a crumb browsed to '/C:/...', which the backend rejected as 'not a directory'. Only the '.. (up)' button worked. Build drive-aware crumb targets (C:/, C:/Users) and drop the bogus unified '/' root crumb on Windows drive paths; POSIX behavior is unchanged. Add a regression test for Windows breadcrumb navigation and cleanup() for test isolation. Signed-off-by: Zadak --- graph-ui/src/components/StatsTab.test.tsx | 36 ++++++++++++++++++++++- graph-ui/src/components/StatsTab.tsx | 18 ++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/graph-ui/src/components/StatsTab.test.tsx b/graph-ui/src/components/StatsTab.test.tsx index f0cc22d5..494c0a30 100644 --- a/graph-ui/src/components/StatsTab.test.tsx +++ b/graph-ui/src/components/StatsTab.test.tsx @@ -1,6 +1,6 @@ /* @vitest-environment jsdom */ import "@testing-library/jest-dom/vitest"; -import { fireEvent, render, screen, waitFor, act } from "@testing-library/react"; +import { cleanup, fireEvent, render, screen, waitFor, act } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { StatsTab, IndexProgress } from "./StatsTab"; import { messages } from "../lib/i18n"; @@ -43,6 +43,7 @@ function mockProjectsFetch(extra?: (url: string, init?: RequestInit) => Response describe("StatsTab index modal", () => { afterEach(() => { + cleanup(); vi.unstubAllGlobals(); }); @@ -93,6 +94,39 @@ describe("StatsTab index modal", () => { expect(screen.getByRole("button", { name: "Index beta" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Browse D:/" })).toBeInTheDocument(); }); + + it("navigates Windows breadcrumb segments to real drive paths", async () => { + const fetchMock = mockProjectsFetch((url) => { + if (url.startsWith("/api/browse")) { + return new Response(JSON.stringify({ + path: "C:/Users/rap", + parent: "C:/Users", + dirs: ["Documents", "Downloads"], + roots: ["C:/", "D:/"], + }), { status: 200, headers: { "Content-Type": "application/json" } }); + } + return undefined; + }); + + render( {}} />); + fireEvent.click(await screen.findByRole("button", { name: "Index your first repository" })); + + /* No bogus unified "/" root crumb on a Windows drive path. */ + await screen.findByRole("button", { name: "C:" }); + expect(screen.queryByRole("button", { name: "/" })).not.toBeInTheDocument(); + + /* Clicking the drive crumb browses to "C:/", not "/C:". */ + fireEvent.click(screen.getByRole("button", { name: "C:" })); + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith("/api/browse?path=C%3A%2F"); + }); + + /* Clicking a nested crumb browses to "C:/Users", not "/C:/Users". */ + fireEvent.click(screen.getByRole("button", { name: "Users" })); + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith("/api/browse?path=C%3A%2FUsers"); + }); + }); }); describe("IndexProgress", () => { diff --git a/graph-ui/src/components/StatsTab.tsx b/graph-ui/src/components/StatsTab.tsx index 07e15fd5..b210f861 100644 --- a/graph-ui/src/components/StatsTab.tsx +++ b/graph-ui/src/components/StatsTab.tsx @@ -239,6 +239,16 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat /* Breadcrumb segments */ const displayPath = currentPath.replace(/\\/g, "/"); const segments = displayPath.split("/").filter(Boolean); + /* A Windows drive path ("C:/Users/rap") has no unified "/" root — its first + * segment is the drive letter. Build crumb targets accordingly so clicking a + * segment navigates to a real directory instead of a bogus "/C:/..." path + * that the backend rejects as "not a directory". */ + const isWinPath = /^[A-Za-z]:$/.test(segments[0] ?? ""); + const crumbPath = (i: number): string => { + const parts = segments.slice(0, i + 1); + if (isWinPath) return parts.length === 1 ? `${parts[0]}/` : parts.join("/"); + return "/" + parts.join("/"); + }; return (
@@ -297,12 +307,14 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat {/* Breadcrumb */}
- + {!isWinPath && ( + + )} {segments.map((seg, i) => ( - / + {(i > 0 || !isWinPath) && /}