fix(web): sort nested file tree folders before files (#562)
This commit is contained in:
@@ -120,9 +120,12 @@ function TreeNodeChildren({ props, depth }: { props: TreeNodeItemProps; depth: n
|
||||
onCancel={onCancelCreate}
|
||||
/>
|
||||
)}
|
||||
{node.children?.map((child) => (
|
||||
<TreeNodeItem key={child.path} {...props} node={child} depth={depth + 1} />
|
||||
))}
|
||||
{node.children &&
|
||||
[...node.children]
|
||||
.sort(compareTreeNodes)
|
||||
.map((child) => (
|
||||
<TreeNodeItem key={child.path} {...props} node={child} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -142,6 +145,9 @@ export function TreeNodeItem(props: TreeNodeItemProps) {
|
||||
|
||||
const rowContent = (
|
||||
<div
|
||||
data-testid="file-tree-node"
|
||||
data-path={node.path}
|
||||
data-is-dir={node.is_dir ? "true" : "false"}
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-1 px-2 py-0.5 text-left text-sm cursor-pointer",
|
||||
"hover:bg-muted",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import type { SeedData } from "../../fixtures/test-base";
|
||||
import type { ApiClient } from "../../helpers/api-client";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
async function seedSimpleTask(
|
||||
testPage: Page,
|
||||
apiClient: ApiClient,
|
||||
seedData: SeedData,
|
||||
title: string,
|
||||
): Promise<SessionPage> {
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
title,
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
test.describe("File tree sorting", () => {
|
||||
test.describe.configure({ retries: 1 });
|
||||
|
||||
test("directories appear before files inside nested folders, including dotfiles", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
}) => {
|
||||
// Seed a nested folder containing a mix of dirs (incl. a dot-dir) and files
|
||||
// (incl. dotfiles). If the sort is wrong, dotfiles will appear above .vscode.
|
||||
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
|
||||
const subDir = path.join(repoDir, "sub");
|
||||
|
||||
fs.mkdirSync(path.join(subDir, "adir"), { recursive: true });
|
||||
fs.writeFileSync(path.join(subDir, "adir", "keep.txt"), "x\n");
|
||||
fs.mkdirSync(path.join(subDir, ".vscode"), { recursive: true });
|
||||
fs.writeFileSync(path.join(subDir, ".vscode", "settings.json"), "{}\n");
|
||||
fs.writeFileSync(path.join(subDir, ".gitignore"), "node_modules\n");
|
||||
fs.writeFileSync(path.join(subDir, ".golangci.yaml"), "linters:\n");
|
||||
fs.writeFileSync(path.join(subDir, "zfile.txt"), "z\n");
|
||||
|
||||
const session = await seedSimpleTask(testPage, apiClient, seedData, "File Tree Sorting");
|
||||
|
||||
await session.clickTab("Files");
|
||||
await expect(session.files).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Expand the sub folder
|
||||
const subRow = session.files.locator('[data-testid="file-tree-node"][data-path="sub"]');
|
||||
await expect(subRow).toBeVisible({ timeout: 10_000 });
|
||||
await subRow.click();
|
||||
|
||||
// Wait until all expected children are rendered
|
||||
const childSelector = (p: string) =>
|
||||
session.files.locator(`[data-testid="file-tree-node"][data-path="${p}"]`);
|
||||
|
||||
await expect(childSelector("sub/adir")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(childSelector("sub/.vscode")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(childSelector("sub/.gitignore")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(childSelector("sub/.golangci.yaml")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(childSelector("sub/zfile.txt")).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Read the rendered order of sub's direct children
|
||||
const childPaths = await session.files
|
||||
.locator('[data-testid="file-tree-node"]')
|
||||
.evaluateAll((els) =>
|
||||
els
|
||||
.map((el) => ({
|
||||
path: el.getAttribute("data-path") ?? "",
|
||||
isDir: el.getAttribute("data-is-dir") === "true",
|
||||
}))
|
||||
.filter((n) => n.path.startsWith("sub/") && !n.path.slice("sub/".length).includes("/")),
|
||||
);
|
||||
|
||||
// Directories must come before files. Within each group, order follows
|
||||
// the comparator's localeCompare (dot-prefixed names sort before letters).
|
||||
const dirs = childPaths.filter((n) => n.isDir).map((n) => n.path);
|
||||
const files = childPaths.filter((n) => !n.isDir).map((n) => n.path);
|
||||
|
||||
expect(dirs).toEqual(["sub/.vscode", "sub/adir"]);
|
||||
expect(files).toEqual(["sub/.gitignore", "sub/.golangci.yaml", "sub/zfile.txt"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user