From 4fbef6e9f86c4acef566edfcf26241c5feb6802c Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Tue, 4 Mar 2025 23:23:19 -0700 Subject: [PATCH] FEAT: Editor is good!!! Just needs lots of debugging. --- backend/src/server.ts | 50 +++++++++++++- frontend/src/components/Directory.jsx | 3 +- frontend/src/components/DirectoryList.jsx | 4 +- frontend/src/components/Editor.jsx | 82 +++++++++++++++++++++++ frontend/src/components/Error.jsx | 2 +- frontend/src/pages/Dashboard.jsx | 66 +++++++++++++++++- 6 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/Editor.jsx diff --git a/backend/src/server.ts b/backend/src/server.ts index 6acbbd7..c37d4f9 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -16,6 +16,11 @@ const PORT = 5000; const APP: Express = express(); const ROOT: string = "/home/azpect"; +/** + * Invalid file extentions for the file editor. + */ +const INVALID_EXTS: string[] = ["exe", "dll", "obj", "lib", "bin", "dat", "pdf", "jpg", "jpeg", "png", "gif", "webm", "webp", "bmp", "mp3", "wav", "mp4", "avi", "zip", "rar", "7z", "iso", "dmg", "class", "pyc", "o", "a", "woff", "woff2", "ttf", "otf", "db", "sqlite", "mdb", "accdb", "psd", "ai", "indd", "blend", "fbx", "unitypackage", "pak", "sav", "msi", ".doc", ".docx", ".dot", ".dotx", ".docm", ".dotm", ".rtf", ".txt", ".xls", ".xlsx", ".xlsm", ".xltx", ".xltm", ".csv", ".ppt", ".pptx", ".pptm", ".potx", ".potm", ".ppsx", ".ppsm", ".mdb", ".accdb", ".accde", ".accdt", ".pst", ".ost", ".msg", ".one", ".onetoc2", ".pub", ".vsd", ".vsdx", ".vssx", ".vstx", ".odc", ".oft", ".pki"]; + /** * Configure cors * TODO: Update hosts for production @@ -90,7 +95,7 @@ v1.get("/children", (req: Request, res: Response): void => { }); /** - * Down a group of files provided in the body + * Down a group of files provided in the body. */ v1.post("/download", (req: Request, res: Response): void => { // Get the files from the body @@ -137,6 +142,49 @@ v1.post("/download", (req: Request, res: Response): void => { archive.finalize(); }); +/** + * Retrieve the content of a file provided in the path query. + */ +v1.get("/content", (req: Request, res: Response): void => { + // Get the path, if it was not provided, return no content + const tgtPath: string = (req.query.path || "") as string; + if (!tgtPath) { + res.status(204); + return; + } + + // Ensure the file isn't something we can't edit + const ext: string = path.extname(tgtPath).slice(1); + if (INVALID_EXTS.includes(ext)) { + res.status(400).json({error: `Cannot edit files of type *.${ext}`, code: 400}); + return; + } + + try { + // Read the file and return it + const content = fs.readFileSync(tgtPath, {encoding: "utf-8", flag: "r"}) + res.status(200).json({content, code: 200}); + } catch (err) { + res.status(500).json({error: `An error occurred on the server. ${err}`, code: 500}); + } +}); + +/** + * Update a file's content, path and content should be provided. + * On success, nothing should be sent back, 204. + */ +v1.post("/update", (req: Request, res: Response): void => { + // Get path and content from the request + const {path, content} = req.body; + + try { + fs.writeFileSync(path, content); + console.log(fs.readFileSync(path).toString()); + res.status(204); + } catch (error) { + res.status(500).json({code: 500, error}) + } +}); /** * Apply the routes to the server */ diff --git a/frontend/src/components/Directory.jsx b/frontend/src/components/Directory.jsx index c545511..b2cf67c 100644 --- a/frontend/src/components/Directory.jsx +++ b/frontend/src/components/Directory.jsx @@ -28,7 +28,7 @@ function DirectoryIcon() { * @returns {{}} * @constructor */ -export default function Directory({entry, showHidden, appendPath, toggleSelected}) { +export default function Directory({entry, showHidden, appendPath, toggleSelected, toggleEditing}) { const [selected, setSelected] = useState(false); const handleClick = () => { @@ -36,6 +36,7 @@ export default function Directory({entry, showHidden, appendPath, toggleSelected appendPath(entry.name); } else { console.log(`OPENING FILE: ${entry.path}`) + toggleEditing(entry.path); } }; diff --git a/frontend/src/components/DirectoryList.jsx b/frontend/src/components/DirectoryList.jsx index e2b6ba8..390426d 100644 --- a/frontend/src/components/DirectoryList.jsx +++ b/frontend/src/components/DirectoryList.jsx @@ -8,11 +8,11 @@ import Directory from "./Directory.jsx"; * @param toggleSelected {function(string)} Function to toggle selection status. * @constructor */ -export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected}) { +export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected, toggleEditing}) { return ( <> {dirs.map((dir, idx) => )} + toggleSelected={toggleSelected} toggleEditing={toggleEditing}/>)} ) diff --git a/frontend/src/components/Editor.jsx b/frontend/src/components/Editor.jsx new file mode 100644 index 0000000..b611afb --- /dev/null +++ b/frontend/src/components/Editor.jsx @@ -0,0 +1,82 @@ +import {useEffect, useRef, useState} from "react"; + +export default function Editor({content, path, exit, saveExit}) { + const [text, setText] = useState(""); + /** + * Store a reference to the text area object + * @type {React.RefObject} + */ + const textareaRef = useRef(null); + + const updateText = (event) => { + setText(event.target.value); + }; + + useEffect(() => { + setText(content); + }, [content]) + + const handleKeyPress = (event) => { + // Override tab changing focus + // Uses 2 space indents + // TODO: Allow toggle for two and four space indents + if (event.key === "Tab") { + event.preventDefault(); + + const textarea = textareaRef.current; + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const value = textarea.value; + + // Insert tab character + // Update textarea value and cursor position + textarea.value = value.substring(0, start) + " " + value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 2; // Move the cursor after the tab + + // Trigger a change event so React knows the value changed. + textarea.dispatchEvent(new Event('input', {bubbles: true})); + } + }; + + /** + * Call the parent function with the new content which is + * stored in the text state. + */ + const saveAndExit = () => { + saveExit(text); + }; + + return ( +
+
+
+

Editing File: {path}

+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Error.jsx b/frontend/src/components/Error.jsx index 710b6b5..b7f828e 100644 --- a/frontend/src/components/Error.jsx +++ b/frontend/src/components/Error.jsx @@ -11,7 +11,7 @@ export default function Error({error, clear}) { return (
-
+

An Error Occurred!

{error}

diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index fcb0893..8b6c3fb 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -4,6 +4,7 @@ import DirectoryList from "../components/DirectoryList.jsx"; import PathDisplay from "../components/PathDisplay.jsx"; import Navbar from "../components/Navbar.jsx"; import Error from "../components/Error.jsx"; +import Editor from "../components/Editor.jsx"; export default function Dashboard() { @@ -13,6 +14,8 @@ export default function Dashboard() { const [selected, setSelected] = useState([]); const [files, setFiles] = useState([]); const [error, setError] = useState(null); + const [editing, setEditing] = useState(""); + const navigate = useNavigate(); useEffect(() => { @@ -147,17 +150,78 @@ export default function Dashboard() { setError(null); } + const toggleEditing = (path) => { + setEditing(path); + }; + + const exitFile = () => { + setEditing(""); + }; + + const exitAndSaveFile = (newContent) => { + const updateContent = async (path, content) => { + const resp = await fetch("http://localhost:5000/v1/update", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({path, content}), + }) + if (!resp.ok) { + setError("An error occurred when saving the file. Please try again."); + } + return await resp.json(); + }; + + // Send request to server to update the file. This will return nothing + // so no need for any promise handling. + updateContent(editing, newContent); + + // Set editing back to nothing to hide the modal + setEditing(""); + }; + + + /** + * Handle the state for the content being modified in the text editor. + */ + const [editingFileContent, setEditingFileContent] = useState(""); + useEffect(() => { + const fetchContent = async (path) => { + const resp = await fetch(`http://localhost:5000/v1/content?path=${path}`); + if (!resp.ok) { + // TODO: Add this back in, its broken right now. + setError("Something went wrong! Failed to get file content.") + } + return await resp.json(); + }; + + // Fetch the data and handle errors accordingly + fetchContent(editing).then((data) => { + if (data.code === 200) { + setEditingFileContent(data.content); + } else { + // An error occurred, do not open the editor + setEditing(""); + setError(data.error); + } + }); + + }, [editing]); + return (
{error && } + {(editing !== "" && !error) && + }
+ toggleSelected={toggleSelected} toggleEditing={toggleEditing}/>