FEAT: Editor is good!!! Just needs lots of debugging.
This commit is contained in:
parent
8215704795
commit
4fbef6e9f8
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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}/>)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
82
frontend/src/components/Editor.jsx
Normal file
82
frontend/src/components/Editor.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user