Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/file explorer v2 #37

Merged
merged 13 commits into from
Sep 2, 2024
2 changes: 1 addition & 1 deletion app/nodejs-cli/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "@/components/ui/resizable";
import NodeTerminal from "@/components/webcontainer/node-terminal";
import Editor from "@/components/workspace/editor";
import FileExplorer from "@/components/workspace/file-explorer";
import FileExplorer from "@/components/file-explorer";

export default function Page() {
return (
Expand Down
2 changes: 1 addition & 1 deletion app/tutorials/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@/components/ui/resizable";
import Cli from "@/components/workspace/cli";
import Editor from "@/components/workspace/editor";
import FileExplorer from "@/components/workspace/file-explorer";
import FileExplorer from "@/components/file-explorer";
import { PropsWithChildren } from "react";

export default function Layout({ children }: PropsWithChildren) {
Expand Down
2 changes: 1 addition & 1 deletion app/workspace/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "@/components/ui/resizable";
import Cli from "@/components/workspace/cli";
import Editor from "@/components/workspace/editor";
import FileExplorer from "@/components/workspace/file-explorer";
import FileExplorer from "@/components/file-explorer";

export default function Page() {
return (
Expand Down
87 changes: 87 additions & 0 deletions components/file-explorer/context-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuShortcut,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import { Pencil, Delete, FilePlus, FolderPlus } from "lucide-react";
import React from "react";

export enum IAction {
RENAME,
DELETE,
NEW_FILE,
NEW_FOLDER,
}

interface IContextMenuProps extends React.PropsWithChildren {
handleClick: (action: IAction) => void;
}

export function FileContextMenu({ children, handleClick }: IContextMenuProps) {
return (
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent
className="w-64"
onClick={(e) => {
e.stopPropagation();
}}
>
<RenameItem onClick={() => handleClick(IAction.RENAME)} />
<DeleteItem onClick={() => handleClick(IAction.DELETE)} />
</ContextMenuContent>
</ContextMenu>
);
}

export function FolderContextMenu({
children,
handleClick,
}: IContextMenuProps) {
return (
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent
className="w-64"
onClick={(e) => {
e.stopPropagation();
}}
>
<ContextMenuItem onClick={() => handleClick(IAction.NEW_FILE)}>
<FilePlus className="w-4 h-4 mr-2" />
<span>New File</span>
<ContextMenuShortcut>Ctrl+N</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem onClick={() => handleClick(IAction.NEW_FOLDER)}>
<FolderPlus className="w-4 h-4 mr-2" />
<span>New Folder</span>
<ContextMenuShortcut>Ctrl+Shift+N</ContextMenuShortcut>
</ContextMenuItem>
<RenameItem onClick={() => handleClick(IAction.RENAME)} />
<DeleteItem onClick={() => handleClick(IAction.DELETE)} />
</ContextMenuContent>
</ContextMenu>
);
}

interface IMenuItemProps {
onClick: () => void;
}

const DeleteItem = ({ onClick }: IMenuItemProps) => (
<ContextMenuItem onClick={onClick}>
<Delete className="w-4 h-4 mr-2" />
<span>Delete</span>
<ContextMenuShortcut>Del</ContextMenuShortcut>
</ContextMenuItem>
);

const RenameItem = ({ onClick }: IMenuItemProps) => (
<ContextMenuItem onClick={onClick}>
<Pencil className="w-4 h-4 mr-2" />
<span>Rename</span>
<ContextMenuShortcut>F2</ContextMenuShortcut>
</ContextMenuItem>
);
74 changes: 74 additions & 0 deletions components/file-explorer/delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "../ui/button";
import { db } from "@/data/db";
import { usePathname } from "next/navigation";
import { useRefreshFileExplorer } from "./";

export default function Delete({
type,
path,
isOpen,
setIsOpen,
}: React.PropsWithChildren<{
type?: "file" | "folder";
path?: string;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}>) {
const pathname = usePathname();
const refreshFileExplorer = useRefreshFileExplorer();

if (!path) return null;

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete {type}</DialogTitle>
<DialogDescription>
Are you sure you want to delete {path}? This action is not
reversible!
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="destructive"
onClick={async () => {
if (type === "file") {
await db.files.delete(
`${pathname}/${encodeURIComponent(path)}`
);
} else {
const all = (
await db.files
.filter((file) =>
file.path.startsWith(
`${pathname}/${encodeURIComponent(path + "/")}`
)
)
.toArray()
).map((i) => i.path);
await db.files.bulkDelete(all);
}

await refreshFileExplorer();
setIsOpen(false);
}}
>
Confirm
</Button>
<Button onClick={() => setIsOpen(false)}>Cancel</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
14 changes: 14 additions & 0 deletions components/file-explorer/file-explorer-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";

import { createContext, useContext } from "react";
import { initialState, useFileExplorerReducer } from "./file-explorer-reducer";

const FileExplorerContext = createContext<
ReturnType<typeof useFileExplorerReducer>
>([initialState, () => {}]);

const useFileExplorerContext = () => {
return useContext(FileExplorerContext);
};

export { FileExplorerContext, useFileExplorerContext };
87 changes: 87 additions & 0 deletions components/file-explorer/file-explorer-node-renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import { INode } from "react-accessible-treeview";
import { FileIcon } from "./file-icon";
import { FileContextMenu, FolderContextMenu, IAction } from "./context-menu";
import { IFlatMetadata } from "react-accessible-treeview/dist/TreeView/utils";
import { FolderIcon } from "./folder-icon";
import { IFileExplorerActionKind } from "./file-explorer-reducer";
import { useFileExplorerContext } from "./file-explorer-context";

type Element = INode<IFlatMetadata>;

export const NodeRenderer = ({
isBranch,
isExpanded,
element,
}: {
isBranch: boolean;
isExpanded: boolean;
element: Element;
}) => {
const [_, dispatch] = useFileExplorerContext();
const { name } = element;

const handleClick = (action: IAction) => {
switch (action) {
case IAction.DELETE:
dispatch({
type: IFileExplorerActionKind.DELETE,
payload: {
type: isBranch ? "folder" : "file",
path: element.id as string,
},
});
break;
case IAction.RENAME:
dispatch({
type: IFileExplorerActionKind.RENAME,
payload: {
type: isBranch ? "folder" : "file",
path: element.id as string,
},
});
break;
case IAction.NEW_FILE:
dispatch({
type: IFileExplorerActionKind.ADD,
payload: {
type: "file",
path: element.id as string,
},
});
break;
case IAction.NEW_FOLDER:
dispatch({
type: IFileExplorerActionKind.ADD,
payload: {
type: "folder",
path: element.id as string,
},
});
break;
}
};

if (name.startsWith(".")) return null;

return isBranch ? (
<FolderContextMenu handleClick={handleClick}>
<span className="flex px-2">
<span className="my-1">
<FolderIcon isOpen={isExpanded} />
</span>
<span className="ml-2 line-clamp-1">{name}</span>
</span>
</FolderContextMenu>
) : (
<FileContextMenu handleClick={handleClick}>
<span className="flex px-2">
<span className="my-1">
<FileIcon filename={name} />
</span>
<span className="ml-2 line-clamp-1">{name}</span>
</span>
</FileContextMenu>
);
};
80 changes: 80 additions & 0 deletions components/file-explorer/file-explorer-reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { useReducer } from "react";

// An enum with all the types of actions to use in our reducer
export enum IFileExplorerActionKind {
RENAME,
DELETE,
SELECT,
CLOSE_MODAL,
ADD,
}

type FileOrFolder = "file" | "folder";

// An interface for our actions
interface IFileExplorerAction {
type: IFileExplorerActionKind;
payload?: { path?: string; type?: FileOrFolder };
}

// An interface for our state
interface IFileExplorerState {
showRename: boolean;
showDelete: boolean;
path?: string;
type?: FileOrFolder;
focusedId?: string;
showAdd: boolean;
addType?: FileOrFolder;
}

export const initialState: IFileExplorerState = {
showRename: false,
showDelete: false,
showAdd: false,
};

// Our reducer function that uses a switch statement to handle our actions
function reducer(state: IFileExplorerState, action: IFileExplorerAction) {
const { type, payload } = action;
switch (type) {
case IFileExplorerActionKind.RENAME:
return {
...state,
...(payload || {}),
showRename: true,
};
case IFileExplorerActionKind.DELETE:
return {
...state,
...(payload || {}),
showDelete: true,
};
case IFileExplorerActionKind.SELECT:
return {
...state,
...(payload || {}),
};
case IFileExplorerActionKind.CLOSE_MODAL:
return {
...state,
showRename: false,
showDelete: false,
showAdd: false,
};
case IFileExplorerActionKind.ADD:
return {
...state,
addType: payload?.type,
showAdd: true,
};
default:
return state;
}
}

export const useFileExplorerReducer = () => {
return useReducer(reducer, initialState);
};
Loading