From fa90f026097266a40458d0ba44233a924f17dcd6 Mon Sep 17 00:00:00 2001 From: cu8code Date: Wed, 16 Oct 2024 12:26:53 +0530 Subject: [PATCH] added support for create folder and file #4 --- src/components/FileExplorer.tsx | 199 ++++++++++++++++++++++++++++---- src/store.ts | 24 ++++ 2 files changed, 203 insertions(+), 20 deletions(-) diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index 637ad69..0237156 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; -import { X } from "lucide-react"; +import React, { useState, useEffect } from "react"; +import { X, Plus } from "lucide-react"; import { useVSCodeStore } from "../store"; import { FileSystemTree } from "@webcontainer/api"; @@ -13,19 +13,57 @@ const FileExplorer: React.FC = ({ handleFileClick }) => { selectedFile, setShowExplorer, getTheme, - }= useVSCodeStore(); + createFile, + createFolder, + } = useVSCodeStore(); const theme = getTheme(); - const [expandedFolders, setExpandedFolders] = useState<{ [key: string]: boolean }>({}); + const [expandedFolders, setExpandedFolders] = useState<{ + [key: string]: boolean; + }>({}); + const [newFileName, setNewFileName] = useState(""); + const [isFolder, setIsFolder] = useState(false); + const [currentPath, setCurrentPath] = useState(""); // Tracks the current path + const [showInput, setShowInput] = useState(false); + const [inputPositionPath, setInputPositionPath] = useState( + null, + ); + const [contextMenu, setContextMenu] = useState<{ + x: number; + y: number; + folderPath: string; + visible: boolean; + }>({ + x: 0, + y: 0, + folderPath: "", + visible: false, + }); + + useEffect(() => { + const handleClickOutside = () => { + setContextMenu({ ...contextMenu, visible: false }); + }; + + window.addEventListener("click", handleClickOutside); + return () => { + window.removeEventListener("click", handleClickOutside); + }; + }, [contextMenu]); const getIcon = (fileName: string) => { const extension = fileName.split(".").pop(); return ( - theme.fileExplorer.body.icons[extension as keyof typeof theme.fileExplorer.body.icons] || - theme.fileExplorer.body.icons["default-file"] + theme.fileExplorer.body.icons[ + extension as keyof typeof theme.fileExplorer.body.icons + ] || theme.fileExplorer.body.icons["default-file"] ); }; - const renderFileTree = (fileSystemTree: FileSystemTree, path = "", depth = 0) => { + const renderFileTree = ( + fileSystemTree: FileSystemTree, + path = "", + depth = 0, + ) => { return Object.keys(fileSystemTree).map((fileName) => { const filePath = `${path}/${fileName}`; const filePathParts = filePath.split("/"); @@ -33,8 +71,13 @@ const FileExplorer: React.FC = ({ handleFileClick }) => { if ("directory" in fileSystemTree[fileName]) { const isExpanded = expandedFolders[filePath]; + const isInputVisible = showInput && inputPositionPath === filePath; + return ( -
+
handleContextMenu(e, filePath)} + >
= ({ handleFileClick }) => { > {theme.fileExplorer.body.icons["default-folder"]} {displayName} - {isExpanded ? ( - - ) : ( - - )} + {isExpanded ? : }
- {isExpanded && renderFileTree(fileSystemTree[fileName].directory, filePath, depth + 1)} + {isExpanded && + renderFileTree( + fileSystemTree[fileName].directory, + filePath, + depth + 1, + )} + {isInputVisible && ( + + )}
); } else { @@ -84,12 +139,74 @@ const FileExplorer: React.FC = ({ handleFileClick }) => { }); }; + const handleCreateFile = async (path: string, fileName: string) => { + await createFile(path, fileName, ""); + setNewFileName(""); + }; + + const handleCreateFolder = async (path: string, folderName: string) => { + await createFolder(path, folderName); + setNewFileName(""); + setExpandedFolders((prev) => ({ + ...prev, + [`${path}/${folderName}`]: true, + })); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setNewFileName(e.target.value); + setIsFolder(!e.target.value.includes(".")); + }; + + const handleKeyPress = async (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (newFileName.trim() === "") { + setShowInput(false); + return; + } + + if (isFolder) { + await handleCreateFolder(currentPath, newFileName); + } else { + await handleCreateFile(currentPath, newFileName); + } + setShowInput(false); + setInputPositionPath(null); // Reset input position + } + }; + + const handleContextMenu = (e: React.MouseEvent, folderPath: string) => { + e.preventDefault(); + setContextMenu({ + x: e.pageX, + y: e.pageY, + folderPath, + visible: true, + }); + }; + + const handleMenuClick = (type: "file" | "folder") => { + setIsFolder(type === "folder"); + setCurrentPath(contextMenu.folderPath); + setShowInput(true); + setInputPositionPath(contextMenu.folderPath); // Show input below the folder + setContextMenu({ ...contextMenu, visible: false }); + }; + + // New: Add button handler for top-level file/folder creation + const handleAddButtonClick = (type: "file" | "folder") => { + setIsFolder(type === "folder"); + setCurrentPath(""); // Root-level path or current folder + setShowInput(true); + setInputPositionPath(null); // Input appears at the root level + }; + if (!files) { return
Loading...
; } return ( -
+
= ({ handleFileClick }) => { }} >

Explorer

- setShowExplorer(false)} - /> +
+ handleAddButtonClick("file")} + /> + setShowExplorer(false)} + /> +
= ({ handleFileClick }) => { backgroundColor: theme.fileExplorer.body.backgroundColor, }} > + {/* Input field for creating new file/folder */} + {showInput && !inputPositionPath && ( +
+ +
+ )} {renderFileTree(files)}
+ + {/* Context menu for folder right-click */} + {contextMenu.visible && ( +
+
handleMenuClick("file")} + > + Add New File +
+
handleMenuClick("folder")} + > + Add New Folder +
+
+ )}
); }; diff --git a/src/store.ts b/src/store.ts index c82818c..d75f422 100644 --- a/src/store.ts +++ b/src/store.ts @@ -29,6 +29,8 @@ interface VSCodeState { updateFile: (fileName: string, content: string) => void; closeFile: (fileName: string) => void; updateFileSystem: () => Promise; + createFile: (filePath: string, filename: string, content: string) => Promise; + createFolder: (filePath: string, filename: string) => Promise; } export const useVSCodeStore = create((set, get) => ({ @@ -126,6 +128,28 @@ export const useVSCodeStore = create((set, get) => ({ const { theme, themes } = get(); return themes[theme]; }, + createFile: async (filePath: string, fileName: string, content: string) => { + const { webcontainerInstance, updateFileSystem } = get(); + if (webcontainerInstance) { + try { + await webcontainerInstance.fs.writeFile(`${filePath}/${fileName}`, content); + } catch (error) { + console.error(`Error creating file ${filePath}/${fileName}:`, error); + } + } + updateFileSystem() + }, + createFolder: async (filePath: string, folderName: string) => { + const { webcontainerInstance, updateFileSystem } = get(); + if (webcontainerInstance) { + try { + await webcontainerInstance.fs.mkdir(`${filePath}/${folderName}`); + } catch (error) { + console.error(`Error creating folder ${filePath}/${folderName}:`, error); + } + } + updateFileSystem() + }, })); const readDir = async (