Skip to content

Commit

Permalink
Merge branch 'main' into update-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
xingyaoww authored May 18, 2024
2 parents e552f52 + d4c136a commit 805900f
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 372 deletions.
2 changes: 2 additions & 0 deletions .github/.codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ coverage:
threshold: 5% # allow project coverage to drop at most 5%

comment: false
github_checks:
annotations: false
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
interval: "daily"
- package-ecosystem: "npm" # See documentation for possible values
directory: "/frontend" # Location of package manifests
schedule:
interval: "weekly"
interval: "daily"
1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"jose": "^5.2.3",
"monaco-editor": "^0.47.0",
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
Expand Down
18 changes: 10 additions & 8 deletions frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function CodeEditor(): JSX.Element {

const dispatch = useDispatch();
const code = useSelector((state: RootState) => state.code.code);
const activeFilepath = useSelector((state: RootState) => state.code.path);

const handleEditorDidMount = (
editor: editor.IStandaloneCodeEditor,
Expand All @@ -45,20 +46,21 @@ function CodeEditor(): JSX.Element {
monaco.editor.setTheme("my-theme");
};

const onSelectFile = async (absolutePath: string) => {
const paths = absolutePath.split("/");
const rootlessPath = paths.slice(1).join("/");

setSelectedFileAbsolutePath(absolutePath);

const newCode = await selectFile(rootlessPath);
const updateCode = async () => {
const newCode = await selectFile(activeFilepath);
setSelectedFileAbsolutePath(activeFilepath);
dispatch(setCode(newCode));
};

React.useEffect(() => {
// FIXME: we can probably move this out of the component and into state/service
if (activeFilepath) updateCode();
}, [activeFilepath]);

Check warning on line 58 in frontend/src/components/CodeEditor.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

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

return (
<div className="flex h-full w-full bg-neutral-900 transition-all duration-500 ease-in-out">
<CodeEditorContext.Provider value={codeEditorContext}>
<FileExplorer onFileClick={onSelectFile} />
<FileExplorer />
<div className="flex flex-col min-h-0 w-full">
<Tabs
disableCursorAnimation
Expand Down
31 changes: 9 additions & 22 deletions frontend/src/components/file-explorer/ExplorerTree.test.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
import React from "react";
import { render } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import ExplorerTree from "./ExplorerTree";
import { WorkspaceFile } from "#/services/fileService";

const NODE: WorkspaceFile = {
name: "root-folder-1",
children: [
{ name: "file-1-1.ts" },
{ name: "folder-1-2", children: [{ name: "file-1-2.ts" }] },
],
};

const onFileClick = vi.fn();
const FILES = ["file-1-1.ts", "folder-1-2"];

describe("ExplorerTree", () => {
afterEach(() => {
vi.resetAllMocks();
});

it("should render the explorer", () => {
const { getByText, queryByText } = render(
<ExplorerTree root={NODE} onFileClick={onFileClick} defaultOpen />,
const { getByText } = renderWithProviders(
<ExplorerTree files={FILES} defaultOpen />,
);

expect(getByText("root-folder-1")).toBeInTheDocument();
expect(getByText("file-1-1.ts")).toBeInTheDocument();
expect(getByText("folder-1-2")).toBeInTheDocument();
expect(queryByText("file-1-2.ts")).not.toBeInTheDocument();
// TODO: make sure children render
});

it("should render the explorer given the defaultExpanded prop", () => {
const { getByText, queryByText } = render(
<ExplorerTree root={NODE} onFileClick={onFileClick} />,
);
const { queryByText } = renderWithProviders(<ExplorerTree files={FILES} />);

expect(getByText("root-folder-1")).toBeInTheDocument();
expect(queryByText("file-1-1.ts")).not.toBeInTheDocument();
expect(queryByText("folder-1-2")).not.toBeInTheDocument();
expect(queryByText("file-1-2.ts")).not.toBeInTheDocument();
expect(queryByText("file-1-1.ts")).toBeInTheDocument();
expect(queryByText("folder-1-2")).toBeInTheDocument();
// TODO: make sure children don't render
});

it.todo("should render all children as collapsed when defaultOpen is false");
Expand Down
19 changes: 5 additions & 14 deletions frontend/src/components/file-explorer/ExplorerTree.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import React from "react";
import TreeNode from "./TreeNode";
import { WorkspaceFile } from "#/services/fileService";

interface ExplorerTreeProps {
root: WorkspaceFile;
onFileClick: (path: string) => void;
files: string[];
defaultOpen?: boolean;
}

function ExplorerTree({
root,
onFileClick,
defaultOpen = false,
}: ExplorerTreeProps) {
function ExplorerTree({ files, defaultOpen = false }: ExplorerTreeProps) {
return (
<div className="w-full overflow-x-auto h-full pt-[4px]">
<TreeNode
node={root}
path={root.name}
onFileClick={onFileClick}
defaultOpen={defaultOpen}
/>
{files.map((file) => (
<TreeNode key={file} path={file} defaultOpen={defaultOpen} />
))}
</div>
);
}
Expand Down
74 changes: 30 additions & 44 deletions frontend/src/components/file-explorer/FileExplorer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import React from "react";
import { render, waitFor, screen } from "@testing-library/react";
import { waitFor, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from "react-dom/test-utils";
import { renderWithProviders } from "test-utils";
import { describe, it, expect, vi, Mock } from "vitest";
import FileExplorer from "./FileExplorer";
import { getWorkspace, uploadFiles } from "#/services/fileService";
import { uploadFiles, listFiles } from "#/services/fileService";
import toast from "#/utils/toast";

const toastSpy = vi.spyOn(toast, "stickyError");

vi.mock("../../services/fileService", async () => ({
getWorkspace: vi.fn(async () => ({
name: "root",
children: [
{ name: "file1.ts" },
{ name: "folder1", children: [{ name: "file2.ts" }] },
],
})),
listFiles: vi.fn(async (path: string = "/") => {
if (path === "/") {
return Promise.resolve(["folder1/", "file1.ts"]);
}
if (path === "/folder1/" || path === "folder1/") {
return Promise.resolve(["file2.ts"]);
}
return Promise.resolve([]);
}),

uploadFiles: vi.fn(),
}));
Expand All @@ -27,71 +30,54 @@ describe("FileExplorer", () => {
});

it("should get the workspace directory", async () => {
const { getByText } = render(<FileExplorer onFileClick={vi.fn} />);
const { getByText } = renderWithProviders(<FileExplorer />);

expect(getWorkspace).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(getByText("root")).toBeInTheDocument();
expect(getByText("folder1")).toBeInTheDocument();
expect(getByText("file2.ts")).toBeInTheDocument();
});
expect(listFiles).toHaveBeenCalledTimes(2); // once for root, once for folder1
});

it.todo("should render an empty workspace");

it("calls the onFileClick function when a file is clicked", async () => {
const onFileClickMock = vi.fn();
const { getByText } = render(
<FileExplorer onFileClick={onFileClickMock} />,
);

it.only("should refetch the workspace when clicking the refresh button", async () => {
const { getByText } = renderWithProviders(<FileExplorer />);
await waitFor(() => {
expect(getByText("folder1")).toBeInTheDocument();
expect(getByText("file2.ts")).toBeInTheDocument();
});

act(() => {
userEvent.click(getByText("folder1"));
});

act(() => {
userEvent.click(getByText("file2.ts"));
});

const absPath = "root/folder1/file2.ts";
expect(onFileClickMock).toHaveBeenCalledWith(absPath);
});

it("should refetch the workspace when clicking the refresh button", async () => {
const onFileClickMock = vi.fn();
render(<FileExplorer onFileClick={onFileClickMock} />);
expect(listFiles).toHaveBeenCalledTimes(2); // once for root, once for folder 1

// The 'await' keyword is required here to avoid a warning during test runs
await act(() => {
userEvent.click(screen.getByTestId("refresh"));
});

expect(getWorkspace).toHaveBeenCalledTimes(2); // 1 from initial render, 1 from refresh button
expect(listFiles).toHaveBeenCalledTimes(4); // 2 from initial render, 2 from refresh button
});

it("should toggle the explorer visibility when clicking the close button", async () => {
const { getByTestId, getByText, queryByText } = render(
<FileExplorer onFileClick={vi.fn} />,
const { getByTestId, getByText, queryByText } = renderWithProviders(
<FileExplorer />,
);

await waitFor(() => {
expect(getByText("root")).toBeInTheDocument();
expect(getByText("folder1")).toBeInTheDocument();
});

act(() => {
userEvent.click(getByTestId("toggle"));
});

// it should be hidden rather than removed from the DOM
expect(queryByText("root")).toBeInTheDocument();
expect(queryByText("root")).not.toBeVisible();
expect(queryByText("folder1")).toBeInTheDocument();
expect(queryByText("folder1")).not.toBeVisible();
});

it("should upload files", async () => {
// TODO: Improve this test by passing expected argument to `uploadFiles`
const { getByTestId } = render(<FileExplorer onFileClick={vi.fn} />);
const { getByTestId } = renderWithProviders(<FileExplorer />);
const file = new File([""], "file-name");
const file2 = new File([""], "file-name-2");

Expand All @@ -103,7 +89,7 @@ describe("FileExplorer", () => {
});

expect(uploadFiles).toHaveBeenCalledOnce();
expect(getWorkspace).toHaveBeenCalled();
expect(listFiles).toHaveBeenCalled();

const uploadDirInput = getByTestId("file-input");

Expand All @@ -113,7 +99,7 @@ describe("FileExplorer", () => {
});

expect(uploadFiles).toHaveBeenCalledTimes(2);
expect(getWorkspace).toHaveBeenCalled();
expect(listFiles).toHaveBeenCalled();
});

it.skip("should upload files when dragging them to the explorer", () => {
Expand All @@ -127,7 +113,7 @@ describe("FileExplorer", () => {
it.todo("should display an error toast if file upload fails", async () => {
(uploadFiles as Mock).mockRejectedValue(new Error());

const { getByTestId } = render(<FileExplorer onFileClick={vi.fn} />);
const { getByTestId } = renderWithProviders(<FileExplorer />);

const uploadFileInput = getByTestId("file-input");
const file = new File([""], "test");
Expand Down
48 changes: 17 additions & 31 deletions frontend/src/components/file-explorer/FileExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@ import {
IoIosRefresh,
IoIosCloudUpload,
} from "react-icons/io";
import { useDispatch } from "react-redux";
import { IoFileTray } from "react-icons/io5";
import { twMerge } from "tailwind-merge";
import {
WorkspaceFile,
getWorkspace,
uploadFiles,
} from "#/services/fileService";
import { setRefreshID } from "#/state/codeSlice";
import { listFiles, uploadFiles } from "#/services/fileService";
import IconButton from "../IconButton";
import ExplorerTree from "./ExplorerTree";
import { removeEmptyNodes } from "./utils";
import toast from "#/utils/toast";

interface ExplorerActionsProps {
Expand Down Expand Up @@ -86,39 +83,34 @@ function ExplorerActions({
);
}

interface FileExplorerProps {
onFileClick: (path: string) => void;
}

function FileExplorer({ onFileClick }: FileExplorerProps) {
const [workspace, setWorkspace] = React.useState<WorkspaceFile>();
function FileExplorer() {
const [isHidden, setIsHidden] = React.useState(false);
const [isDragging, setIsDragging] = React.useState(false);

const [files, setFiles] = React.useState<string[]>([]);
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
const dispatch = useDispatch();

const getWorkspaceData = async () => {
const wsFile = await getWorkspace();
setWorkspace(removeEmptyNodes(wsFile));
const selectFileInput = () => {
fileInputRef.current?.click(); // Trigger the file browser
};

const selectFileInput = async () => {
// Trigger the file browser
fileInputRef.current?.click();
const refreshWorkspace = async () => {
dispatch(setRefreshID(Math.random()));
setFiles(await listFiles("/"));
};

const uploadFileData = async (files: FileList) => {
const uploadFileData = async (toAdd: FileList) => {
try {
await uploadFiles(files);
await getWorkspaceData(); // Refresh the workspace to show the new file
await uploadFiles(toAdd);
await refreshWorkspace();
} catch (error) {
toast.stickyError("ws", "Error uploading file");
}
};

React.useEffect(() => {
(async () => {
await getWorkspaceData();
await refreshWorkspace();
})();

const enableDragging = () => {
Expand Down Expand Up @@ -162,19 +154,13 @@ function FileExplorer({ onFileClick }: FileExplorerProps) {
>
<div className="flex p-2 items-center justify-between relative">
<div style={{ display: isHidden ? "none" : "block" }}>
{workspace && (
<ExplorerTree
root={workspace}
onFileClick={onFileClick}
defaultOpen
/>
)}
<ExplorerTree files={files} defaultOpen />
</div>

<ExplorerActions
isHidden={isHidden}
toggleHidden={() => setIsHidden((prev) => !prev)}
onRefresh={getWorkspaceData}
onRefresh={refreshWorkspace}
onUpload={selectFileInput}
/>
</div>
Expand Down
Loading

0 comments on commit 805900f

Please sign in to comment.