FEAT: Editor is good!!! Just needs lots of debugging.

This commit is contained in:
Hayden Hargreaves 2025-03-04 23:23:19 -07:00
parent 8215704795
commit 4fbef6e9f8
6 changed files with 201 additions and 6 deletions

View File

@ -16,6 +16,11 @@ const PORT = 5000;
const APP: Express = express(); const APP: Express = express();
const ROOT: string = "/home/azpect"; 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 * Configure cors
* TODO: Update hosts for production * 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 => { v1.post("/download", (req: Request, res: Response): void => {
// Get the files from the body // Get the files from the body
@ -137,6 +142,49 @@ v1.post("/download", (req: Request, res: Response): void => {
archive.finalize(); 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 * Apply the routes to the server
*/ */

View File

@ -28,7 +28,7 @@ function DirectoryIcon() {
* @returns {{}} * @returns {{}}
* @constructor * @constructor
*/ */
export default function Directory({entry, showHidden, appendPath, toggleSelected}) { export default function Directory({entry, showHidden, appendPath, toggleSelected, toggleEditing}) {
const [selected, setSelected] = useState(false); const [selected, setSelected] = useState(false);
const handleClick = () => { const handleClick = () => {
@ -36,6 +36,7 @@ export default function Directory({entry, showHidden, appendPath, toggleSelected
appendPath(entry.name); appendPath(entry.name);
} else { } else {
console.log(`OPENING FILE: ${entry.path}`) console.log(`OPENING FILE: ${entry.path}`)
toggleEditing(entry.path);
} }
}; };

View File

@ -8,11 +8,11 @@ import Directory from "./Directory.jsx";
* @param toggleSelected {function(string)} Function to toggle selection status. * @param toggleSelected {function(string)} Function to toggle selection status.
* @constructor * @constructor
*/ */
export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected}) { export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected, toggleEditing}) {
return ( return (
<> <>
{dirs.map((dir, idx) => <Directory key={idx} entry={dir} showHidden={showHidden} appendPath={appendPath} {dirs.map((dir, idx) => <Directory key={idx} entry={dir} showHidden={showHidden} appendPath={appendPath}
toggleSelected={toggleSelected}/>)} toggleSelected={toggleSelected} toggleEditing={toggleEditing}/>)}
</> </>
) )

View File

@ -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<null>}
*/
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 (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black opacity-50 blur-lg"></div>
<div className="relative z-10 bg-white p-8 rounded-lg shadow-lg w-3/4 h-5/6 border-1 border-gray-400">
<h2 className="text-lg font-semibold mb-4 text-blue-400">Editing File: <span
className="font-mono text-black bg-gray-300 p-1 rounded-md">{path}</span></h2>
<textarea
onKeyDown={handleKeyPress}
tabIndex={-1}
ref={textareaRef}
onInput={updateText}
value={text}
className="border-1 border-gray-300 rounded-md w-full h-9/10 p-1 resize-none text-sm font-mono">
</textarea>
<div className="flex justify-end">
<button
title="Exit without saving"
onClick={exit}
className="bg-red-500 hover:bg-red-600 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer mx-2">
Exit
</button>
<button
title="Save changes and exit"
onClick={saveAndExit}
className="bg-blue-400 hover:bg-blue-500 text-white text-sm font-semibold py-1.5 px-3 rounded hover:cursor-pointer">
Save & Exit
</button>
</div>
</div>
</div>
);
}

View File

@ -11,7 +11,7 @@ export default function Error({error, clear}) {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black opacity-50 blur-lg"></div> <div className="fixed inset-0 bg-black opacity-50 blur-lg"></div>
<div className="relative z-10 bg-white p-8 rounded-lg shadow-lg w-96 border-1 border-gray-400"> <div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-96 border-1 border-gray-400">
<h2 className="text-2xl font-semibold mb-4 text-red-600">An Error Occurred!</h2> <h2 className="text-2xl font-semibold mb-4 text-red-600">An Error Occurred!</h2>
<p className="mb-4">{error}</p> <p className="mb-4">{error}</p>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@ -4,6 +4,7 @@ import DirectoryList from "../components/DirectoryList.jsx";
import PathDisplay from "../components/PathDisplay.jsx"; import PathDisplay from "../components/PathDisplay.jsx";
import Navbar from "../components/Navbar.jsx"; import Navbar from "../components/Navbar.jsx";
import Error from "../components/Error.jsx"; import Error from "../components/Error.jsx";
import Editor from "../components/Editor.jsx";
export default function Dashboard() { export default function Dashboard() {
@ -13,6 +14,8 @@ export default function Dashboard() {
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [editing, setEditing] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@ -147,17 +150,78 @@ export default function Dashboard() {
setError(null); 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 ( return (
<div className="w-full min-h-screen h-screen pb-8"> <div className="w-full min-h-screen h-screen pb-8">
<Navbar downloadFiles={downloadFiles}/> <Navbar downloadFiles={downloadFiles}/>
<div className="h-full w-full flex flex-col items-center justify-center pb-8"> <div className="h-full w-full flex flex-col items-center justify-center pb-8">
{error && <Error error={error} clear={clearError}/>} {error && <Error error={error} clear={clearError}/>}
{(editing !== "" && !error) &&
<Editor content={editingFileContent} path={editing} exit={exitFile} saveExit={exitAndSaveFile}/>}
<PathDisplay path={path} updatePath={updatePath} backHome={backHome} backArrow={backArrow}/> <PathDisplay path={path} updatePath={updatePath} backHome={backHome} backArrow={backArrow}/>
<div className="w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300"> <div className="w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300">
<DirectoryList dirs={files} showHidden={showHidden} appendPath={appendPath} <DirectoryList dirs={files} showHidden={showHidden} appendPath={appendPath}
toggleSelected={toggleSelected}/> toggleSelected={toggleSelected} toggleEditing={toggleEditing}/>
</div> </div>
</div> </div>
</div> </div>