Skip to content

Commit

Permalink
Merge pull request #37 from AElfProject/feature/file-explorer-v2
Browse files Browse the repository at this point in the history
Feature/file explorer v2
  • Loading branch information
yongenaelf authored Sep 2, 2024
2 parents 5e79bd4 + 8df2162 commit d5d2654
Show file tree
Hide file tree
Showing 22 changed files with 998 additions and 148 deletions.
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

0 comments on commit d5d2654

Please sign in to comment.