diff --git a/app/nodejs-cli/page.tsx b/app/nodejs-cli/page.tsx index ec0e435..887f069 100644 --- a/app/nodejs-cli/page.tsx +++ b/app/nodejs-cli/page.tsx @@ -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 ( diff --git a/app/tutorials/[id]/layout.tsx b/app/tutorials/[id]/layout.tsx index d9aa35f..feab348 100644 --- a/app/tutorials/[id]/layout.tsx +++ b/app/tutorials/[id]/layout.tsx @@ -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) { diff --git a/app/workspace/[id]/page.tsx b/app/workspace/[id]/page.tsx index 093a0cc..6732eac 100644 --- a/app/workspace/[id]/page.tsx +++ b/app/workspace/[id]/page.tsx @@ -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 ( diff --git a/components/file-explorer/context-menu.tsx b/components/file-explorer/context-menu.tsx new file mode 100644 index 0000000..5bbe262 --- /dev/null +++ b/components/file-explorer/context-menu.tsx @@ -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 ( + + {children} + { + e.stopPropagation(); + }} + > + handleClick(IAction.RENAME)} /> + handleClick(IAction.DELETE)} /> + + + ); +} + +export function FolderContextMenu({ + children, + handleClick, +}: IContextMenuProps) { + return ( + + {children} + { + e.stopPropagation(); + }} + > + handleClick(IAction.NEW_FILE)}> + + New File + Ctrl+N + + handleClick(IAction.NEW_FOLDER)}> + + New Folder + Ctrl+Shift+N + + handleClick(IAction.RENAME)} /> + handleClick(IAction.DELETE)} /> + + + ); +} + +interface IMenuItemProps { + onClick: () => void; +} + +const DeleteItem = ({ onClick }: IMenuItemProps) => ( + + + Delete + Del + +); + +const RenameItem = ({ onClick }: IMenuItemProps) => ( + + + Rename + F2 + +); \ No newline at end of file diff --git a/components/file-explorer/delete.tsx b/components/file-explorer/delete.tsx new file mode 100644 index 0000000..abc6e9f --- /dev/null +++ b/components/file-explorer/delete.tsx @@ -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 ( + + + + Delete {type} + + Are you sure you want to delete {path}? This action is not + reversible! + + + + + + + + + ); +} diff --git a/components/file-explorer/file-explorer-context.ts b/components/file-explorer/file-explorer-context.ts new file mode 100644 index 0000000..9af6362 --- /dev/null +++ b/components/file-explorer/file-explorer-context.ts @@ -0,0 +1,14 @@ +"use client"; + +import { createContext, useContext } from "react"; +import { initialState, useFileExplorerReducer } from "./file-explorer-reducer"; + +const FileExplorerContext = createContext< + ReturnType +>([initialState, () => {}]); + +const useFileExplorerContext = () => { + return useContext(FileExplorerContext); +}; + +export { FileExplorerContext, useFileExplorerContext }; diff --git a/components/file-explorer/file-explorer-node-renderer.tsx b/components/file-explorer/file-explorer-node-renderer.tsx new file mode 100644 index 0000000..6f45a04 --- /dev/null +++ b/components/file-explorer/file-explorer-node-renderer.tsx @@ -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; + +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 ? ( + + + + + + {name} + + + ) : ( + + + + + + {name} + + + ); +}; diff --git a/components/file-explorer/file-explorer-reducer.ts b/components/file-explorer/file-explorer-reducer.ts new file mode 100644 index 0000000..64b7cb9 --- /dev/null +++ b/components/file-explorer/file-explorer-reducer.ts @@ -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); +}; diff --git a/components/workspace/file-explorer-top-menu.tsx b/components/file-explorer/file-explorer-top-menu.tsx similarity index 100% rename from components/workspace/file-explorer-top-menu.tsx rename to components/file-explorer/file-explorer-top-menu.tsx diff --git a/components/file-explorer/file-explorer.css b/components/file-explorer/file-explorer.css new file mode 100644 index 0000000..3dabc7c --- /dev/null +++ b/components/file-explorer/file-explorer.css @@ -0,0 +1,43 @@ +.file-tree { + user-select: none; + border-radius: 0.4em; +} + +.file-tree .tree, +.file-tree .tree-node, +.file-tree .tree-node-group { + list-style: none; + margin: 0; + padding: 0; +} + +.file-tree .tree-branch-wrapper, +.file-tree .tree-node__leaf { + outline: none; + outline: none; +} + +.file-tree .tree-node { + cursor: pointer; +} + +.file-tree .tree-node:hover { + background: rgba(255, 255, 255, 0.1); +} + +.file-tree .tree .tree-node--focused { + background: rgba(255, 255, 255, 0.2); +} + +.file-tree .tree .tree-node--selected { + background: rgba(48, 107, 176); +} + +.file-tree .tree-node__branch { + display: block; +} + +.file-tree .icon { + vertical-align: middle; + margin-right: 5px; +} \ No newline at end of file diff --git a/components/workspace/file-icon.tsx b/components/file-explorer/file-icon.tsx similarity index 100% rename from components/workspace/file-icon.tsx rename to components/file-explorer/file-icon.tsx diff --git a/components/file-explorer/folder-icon.tsx b/components/file-explorer/folder-icon.tsx new file mode 100644 index 0000000..4ca3308 --- /dev/null +++ b/components/file-explorer/folder-icon.tsx @@ -0,0 +1,8 @@ +import { FolderOpen, FolderClosed } from "lucide-react"; + +export const FolderIcon = ({ isOpen }: { isOpen: boolean }) => + isOpen ? ( + + ) : ( + + ); diff --git a/components/file-explorer/index.tsx b/components/file-explorer/index.tsx new file mode 100644 index 0000000..44c7d23 --- /dev/null +++ b/components/file-explorer/index.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { db } from "@/data/db"; +import { usePathname } from "next/navigation"; +import useSWR, { mutate } from "swr"; +import { FileExplorerTopMenu } from "./file-explorer-top-menu"; +import TreeView, { flattenTree } from "react-accessible-treeview"; +import "./file-explorer.css"; +import { useSetSearchParams } from "@/lib/set-search-params"; +import Rename from "./rename"; +import Delete from "./delete"; +import { + IFileExplorerActionKind, + useFileExplorerReducer, +} from "./file-explorer-reducer"; +import { NodeRenderer } from "./file-explorer-node-renderer"; +import { FileExplorerContext } from "./file-explorer-context"; + +interface TreeViewElement { + name: string; + children?: TreeViewElement[]; +} + +function convert(data: string[]) { + const map = new Map(); + const root: TreeViewElement = { + name: "", + children: [], + }; + for (const name of data) { + let path = ""; + let parent = root; + for (const label of name.split("/")) { + path += "/" + label; + let node = map.get(path); + if (!node) { + map.set( + path, + (node = { + name: label, + id: path.slice(1), + } as TreeViewElement) + ); + (parent.children ??= []).push(node); + } + parent = node; + } + } + return flattenTree(root); +} + +const FileExplorer = () => { + const pathname = usePathname(); + const setSearchParams = useSetSearchParams(); + const refreshFileExplorer = useRefreshFileExplorer(); + + const [state, dispatch] = useFileExplorerReducer(); + + const { data } = useSWR(`file-explorer-${pathname}`, async () => { + const files = await db.files.filter((file) => + file.path.startsWith(pathname + "/") + ); + const filesArray = await files.toArray(); + return convert( + filesArray.map((i) => + decodeURIComponent(i.path.replace(`${pathname}/`, "")) + ) + ); + }); + + if (!data) return

Loading...

; + + return ( + + +
+ { + const { id } = props.element; + + if (typeof id !== "string") return; + + const { children } = props.element; + + dispatch({ + type: IFileExplorerActionKind.SELECT, + payload: { + type: children.length > 0 ? "folder" : "file", + path: id, + }, + }); + + if (children.length === 0) { + setSearchParams("file", encodeURIComponent(id)); + } + }} + // @ts-expect-error + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === "F2") { + e.stopPropagation(); + + dispatch({ type: IFileExplorerActionKind.RENAME }); + } + + if (e.code === "Delete") { + e.stopPropagation(); + + dispatch({ type: IFileExplorerActionKind.DELETE }); + } + + if (state.type === "folder") { + if (e.ctrlKey && e.key.toUpperCase() === "N") { + if (e.shiftKey) { + dispatch({ + type: IFileExplorerActionKind.ADD, + payload: { + type: "folder", + }, + }); + } else { + dispatch({ + type: IFileExplorerActionKind.ADD, + payload: { + type: "file", + }, + }); + } + } + } + }} + nodeRenderer={({ + element, + isBranch, + isExpanded, + getNodeProps, + level, + }) => ( +
+ + {state.showAdd && state.path === element.id ? ( + { + setTimeout(() => { + ref?.focus(); + }, 200); + }} + className="ml-8" + onBlur={() => + dispatch({ type: IFileExplorerActionKind.CLOSE_MODAL }) + } + onKeyDown={async (e) => { + if (e.key === "Escape") + dispatch({ type: IFileExplorerActionKind.CLOSE_MODAL }); + + if (e.key === "Enter") { + await db.files.add({ + path: `${pathname}/${encodeURIComponent( + `${state.path}/${e.currentTarget.value}${ + state.addType === "folder" ? "/.gitkeep" : "" + }` + )}`, + contents: "", + }); + await refreshFileExplorer(); + dispatch({ type: IFileExplorerActionKind.CLOSE_MODAL }); + } + }} + /> + ) : null} +
+ )} + /> +
+ + dispatch({ type: IFileExplorerActionKind.CLOSE_MODAL }) + } + /> + + dispatch({ type: IFileExplorerActionKind.CLOSE_MODAL }) + } + /> +
+ ); +}; + +/** + * + * @returns Function to refresh the file explorer in the current view + */ +export const useRefreshFileExplorer = () => { + const pathname = usePathname(); + return () => mutate(`file-explorer-${pathname}`); +}; + +export default FileExplorer; diff --git a/components/workspace/new-file-form.tsx b/components/file-explorer/new-file-form.tsx similarity index 97% rename from components/workspace/new-file-form.tsx rename to components/file-explorer/new-file-form.tsx index 402c7de..d7e837e 100644 --- a/components/workspace/new-file-form.tsx +++ b/components/file-explorer/new-file-form.tsx @@ -18,7 +18,7 @@ import { Input } from "@/components/ui/input"; import { usePathname } from "next/navigation"; import { db } from "@/data/db"; import { Loader2 } from "lucide-react"; -import { useRefreshFileExplorer } from "./file-explorer"; +import { useRefreshFileExplorer } from "./"; const FormSchema = z.object({ path: z.string(), diff --git a/components/workspace/new-file.tsx b/components/file-explorer/new-file.tsx similarity index 100% rename from components/workspace/new-file.tsx rename to components/file-explorer/new-file.tsx diff --git a/components/file-explorer/rename-form.tsx b/components/file-explorer/rename-form.tsx new file mode 100644 index 0000000..17bc5c7 --- /dev/null +++ b/components/file-explorer/rename-form.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { usePathname } from "next/navigation"; +import { db } from "@/data/db"; +import { Loader2 } from "lucide-react"; +import { useRefreshFileExplorer } from "./"; + +const FormSchema = z.object({ + path: z.string(), +}); + +export function RenameForm({ + onSubmit, + type, + path, +}: { + onSubmit?: (path: string) => void; + type?: "file" | "folder"; + path?: string; +}) { + const pathname = usePathname(); + const refreshFileExplorer = useRefreshFileExplorer(); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + path, + }, + }); + + async function _onSubmit(data: z.infer) { + form.clearErrors(); + + if (!path) return; + + try { + if (type === "file") { + const currentKey = `${pathname}/${encodeURIComponent(path)}`; + const currentFile = await db.files.get(currentKey); + + const newKey = `${pathname}/${encodeURIComponent(data.path)}`; + await db.files.add({ + path: newKey, + contents: currentFile?.contents || "", + }); + await db.files.delete(currentKey); + } else { + const currentKey = `${pathname}/${encodeURIComponent(path)}`; + const currentFiles = await db.files + .filter((file) => file.path.startsWith(currentKey)) + .toArray(); + + const newKey = `${pathname}/${encodeURIComponent(data.path)}`; + const newFiles = currentFiles.map((i) => ({ + ...i, + path: i.path.replace(currentKey, newKey), + })); + + await db.files.bulkAdd(newFiles); + await db.files.bulkDelete(currentFiles.map((i) => i.path)); + } + + await refreshFileExplorer(); + onSubmit?.(data.path); + } catch (err) { + form.setError("path", { message: "Path already exists." }); + } + } + + return ( +
+ + ( + + Path + + + + + This is the new path of your {type}. + + + + )} + /> + + + + ); +} diff --git a/components/file-explorer/rename.tsx b/components/file-explorer/rename.tsx new file mode 100644 index 0000000..517887d --- /dev/null +++ b/components/file-explorer/rename.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +import { RenameForm } from "./rename-form"; + +export default function Rename({ + type, + path, + isOpen, + setIsOpen, +}: React.PropsWithChildren<{ + type?: "file" | "folder"; + path?: string; + isOpen: boolean; + setIsOpen: (open: boolean) => void; +}>) { + return ( + + + + Rename {type} + + setIsOpen(false)} + /> + + + + + ); +} diff --git a/components/ui/context-menu.tsx b/components/ui/context-menu.tsx new file mode 100644 index 0000000..93ef37b --- /dev/null +++ b/components/ui/context-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/components/workspace/file-explorer.tsx b/components/workspace/file-explorer.tsx deleted file mode 100644 index c0a1689..0000000 --- a/components/workspace/file-explorer.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; - -import { - Tree, - TreeViewElement, - File, - Folder, - CollapseButton, -} from "@/components/extension/tree-view-api"; -import { db } from "@/data/db"; -import Link from "next/link"; -import { usePathname, useSearchParams } from "next/navigation"; -import useSWR, { mutate } from "swr"; -import { FileIcon } from "./file-icon"; -import { FileExplorerTopMenu } from "./file-explorer-top-menu"; -import TreeView, { flattenTree } from "react-accessible-treeview"; - -type TOCProps = { - toc: TreeViewElement[]; - pathname: string; -}; - -const TOC = ({ toc, pathname }: TOCProps) => { - return ( - - {toc.map((element, _) => ( - - ))} - - - ); -}; - -type TreeItemProps = { - elements: TreeViewElement[]; - pathname: string; -}; - -export const TreeItem = ({ elements, pathname }: TreeItemProps) => { - const searchParams = useSearchParams(); - return ( -
    - {elements.map((element) => ( -
  • - {element.children && element.children?.length > 0 ? ( - - - - ) : ( - || undefined} - > - - {element?.name} - - - )} -
  • - ))} -
- ); -}; - -function convert(data: string[]) { - const map = new Map(); - const root: { children: TreeViewElement[] } = { children: [] }; - for (const name of data) { - let path = ""; - let parent = root; - for (const label of name.split("/")) { - path += "/" + label; - let node = map.get(path); - if (!node) { - map.set( - path, - (node = { id: path.slice(1), name: label } as TreeViewElement) - ); - (parent.children ??= []).push(node); - } - parent = node; - } - } - return root.children; -} - -const FileExplorer = () => { - const pathname = usePathname(); - - const { data: toc } = useSWR(`file-explorer-${pathname}`, async () => { - const files = await db.files.filter((file) => - file.path.startsWith(pathname + "/") - ); - const filesArray = await files.toArray(); - return convert( - filesArray.map((i) => - decodeURIComponent(i.path.replace(`${pathname}/`, "")) - ) - ); - }); - - console.log(toc); - - if (!toc) return

Loading...

; - - return ( - <> - - - - ); -}; - -/** - * - * @returns Function to refresh the file explorer in the current view - */ -export const useRefreshFileExplorer = () => { - const pathname = usePathname(); - return () => mutate(`file-explorer-${pathname}`); -}; - -export default FileExplorer; diff --git a/components/workspace/upload-modal.tsx b/components/workspace/upload-modal.tsx index 69b029a..698d7c5 100644 --- a/components/workspace/upload-modal.tsx +++ b/components/workspace/upload-modal.tsx @@ -15,7 +15,7 @@ import { db, FileContent } from "@/data/db"; import { useCallback, useState } from "react"; import { useDropzone, DropzoneOptions } from "react-dropzone"; import { usePathname } from "next/navigation"; -import { useRefreshFileExplorer } from "./file-explorer"; +import { useRefreshFileExplorer } from "@/components/file-explorer"; import { useLoadFiles } from "../webcontainer/use-load-files"; import { Tooltip } from "../tooltip"; diff --git a/package-lock.json b/package-lock.json index 03485b9..1d5b606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@octokit/plugin-throttling": "^9.3.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", @@ -2008,6 +2009,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.1.tgz", + "integrity": "sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-menu": "2.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", diff --git a/package.json b/package.json index efecc51..57b3cb4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@octokit/plugin-throttling": "^9.3.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", + "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0",