fix(web): hide filesystem directory sentinels

This commit is contained in:
Bryan Helmkamp 2026-05-10 10:41:02 -04:00
parent f607068a99
commit d0f3676d88
No known key found for this signature in database
2 changed files with 17 additions and 22 deletions

View file

@ -205,14 +205,15 @@ describe("binary and decoding helpers", () => {
});
describe("buildTreeInputs", () => {
test("emits placeholders for directories so they appear in the tree", () => {
test("emits native directory paths without user-visible placeholders", () => {
const entries: SandboxFileEntry[] = [
{ name: "src", is_dir: true },
{ name: "package.json", is_dir: false, size: 200 },
];
const inputs = buildTreeInputs(entries);
expect(inputs.paths).toContain("src/");
expect(inputs.paths).toContain("package.json");
expect(inputs.paths.some((path) => path.startsWith("src/"))).toBe(true);
expect(inputs.paths).not.toContain("src/__fabro_dir__");
expect(inputs.directories.has("src")).toBe(true);
expect(inputs.fileEntries.get("package.json")?.size).toBe(200);
});
@ -225,9 +226,8 @@ describe("classifySelection", () => {
];
const inputs = buildTreeInputs(entries);
test("recognizes a placeholder selection as a directory", () => {
const placeholder = inputs.paths.find((path) => path.startsWith("src/"))!;
expect(classifySelection(placeholder, inputs.fileEntries, inputs.directories)).toEqual({
test("recognizes a native directory path selection as a directory", () => {
expect(classifySelection("src/", inputs.fileEntries, inputs.directories)).toEqual({
kind: "dir",
relativePath: "src",
});
@ -363,11 +363,11 @@ describe("FilesystemPanel render", () => {
};
renderPanel();
expect(lastFilesArgs?.path).toBe("/");
const placeholder = filesystemPanelModule.buildTreeInputs(
const directoryPath = filesystemPanelModule.buildTreeInputs(
filesState.data!.data,
).paths.find((path) => path.startsWith("src/"))!;
).paths.find((path) => path === "src/")!;
act(() => {
lastTreeOptions?.onSelectionChange?.([placeholder]);
lastTreeOptions?.onSelectionChange?.([directoryPath]);
});
expect(lastFilesArgs?.path).toBe("/src");
});

View file

@ -142,9 +142,8 @@ interface BuiltTreeInputs {
// The list endpoint returns flat names like `foo` (depth=1) or `foo/bar.ts`
// (depth=2). We feed those names to @pierre/trees as relative paths. Every
// directory gets a synthetic placeholder child so empty directories still
// appear; the placeholders are filtered out before selection.
const DIRECTORY_PLACEHOLDER = "__fabro_dir__";
// directories are represented with the tree library's canonical trailing-slash
// form so empty directories appear without synthetic child rows.
export function buildTreeInputs(entries: readonly SandboxFileEntry[]): BuiltTreeInputs {
const paths: string[] = [];
@ -154,7 +153,7 @@ export function buildTreeInputs(entries: readonly SandboxFileEntry[]): BuiltTree
for (const entry of entries) {
if (entry.is_dir) {
directories.add(entry.name);
paths.push(`${entry.name}/${DIRECTORY_PLACEHOLDER}`);
paths.push(`${entry.name}/`);
} else {
fileEntries.set(entry.name, entry);
paths.push(entry.name);
@ -164,8 +163,8 @@ export function buildTreeInputs(entries: readonly SandboxFileEntry[]): BuiltTree
return { paths, fileEntries, directories };
}
function isPlaceholderPath(path: string): boolean {
return path.endsWith(`/${DIRECTORY_PLACEHOLDER}`) || path === DIRECTORY_PLACEHOLDER;
function normalizeDirectorySelection(path: string): string {
return path.replace(/\/+$/, "");
}
export function classifySelection(
@ -173,19 +172,15 @@ export function classifySelection(
fileEntries: Map<string, SandboxFileEntry>,
directories: Set<string>,
): { kind: "file"; entry: SandboxFileEntry } | { kind: "dir"; relativePath: string } | null {
if (isPlaceholderPath(selectedPath)) {
const relPath = selectedPath.substring(0, selectedPath.length - DIRECTORY_PLACEHOLDER.length - 1);
if (relPath) return { kind: "dir", relativePath: relPath };
return null;
}
const fileEntry = fileEntries.get(selectedPath);
if (fileEntry) return { kind: "file", entry: fileEntry };
if (directories.has(selectedPath)) {
return { kind: "dir", relativePath: selectedPath };
const directoryPath = normalizeDirectorySelection(selectedPath);
if (directories.has(directoryPath)) {
return { kind: "dir", relativePath: directoryPath };
}
// Intermediate directory implied by a nested path (depth>1). Treat selection
// as navigation into that subdir.
return { kind: "dir", relativePath: selectedPath };
return { kind: "dir", relativePath: directoryPath };
}
interface PreviewState {