From d126a9fe7cf568e7d6afe9c002204757e741059f Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 3 Sep 2025 03:11:04 -0600 Subject: [PATCH] Added Script Editor with Syntax Highlighting --- Data/Server/WebUI/package.json | 1 + .../WebUI/src/Scripting/Script_Editor.jsx | 73 ++++++++++--------- Data/Server/server.py | 23 +++--- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/Data/Server/WebUI/package.json b/Data/Server/WebUI/package.json index 1e39ff9..eed5c20 100644 --- a/Data/Server/WebUI/package.json +++ b/Data/Server/WebUI/package.json @@ -15,6 +15,7 @@ "@mui/x-tree-view": "8.10.0", "normalize.css": "8.0.1", "prismjs": "1.30.0", + "react-simple-code-editor": "0.13.1", "react": "19.1.0", "react-color": "2.19.3", "react-dom": "19.1.0", diff --git a/Data/Server/WebUI/src/Scripting/Script_Editor.jsx b/Data/Server/WebUI/src/Scripting/Script_Editor.jsx index 64147b4..b870bd0 100644 --- a/Data/Server/WebUI/src/Scripting/Script_Editor.jsx +++ b/Data/Server/WebUI/src/Scripting/Script_Editor.jsx @@ -30,6 +30,7 @@ import "prismjs/components/prism-bash"; import "prismjs/components/prism-powershell"; import "prismjs/components/prism-batch"; import "prismjs/themes/prism-okaidia.css"; +import Editor from "react-simple-code-editor"; // ---------- helpers ---------- const TYPE_OPTIONS = [ @@ -52,9 +53,11 @@ function typeFromFilename(name = "") { } function ensureExt(baseName, typeKey) { + if (!baseName) return baseName; + // If user already provided any extension, keep it. + if (/\.[^./\\]+$/i.test(baseName)) return baseName; const t = TYPES[typeKey] || TYPES.ansible; - const bn = baseName.replace(/\.(yml|ps1|bat|sh)$/i, ""); - return bn + t.ext; + return baseName + t.ext; } function buildTree(scripts, folders) { @@ -121,12 +124,12 @@ function highlightedHtml(code, prismLang) { const grammar = Prism.languages[prismLang] || Prism.languages.markup; return Prism.highlight(code ?? "", grammar, prismLang); } catch { - return (code ?? "").replace(/[&<>]/g, (c) => ({"&":"&","<":"<",">":">"}[c])); + return (code ?? "").replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c])); } } // Local dialog: rename script file -function RenameScriptDialog({ open, value, onChange, onCancel, onSave }) { +function RenameScriptDialog({ open, value, onChange, onCancel, onSave, onBlur }) { return ( ( @@ -155,6 +158,7 @@ function RenameScriptDialog({ open, value, onChange, onCancel, onSave }) { variant="outlined" value={value} onChange={(e) => onChange(e.target.value)} + onBlur={() => onBlur && onBlur()} sx={{ "& .MuiOutlinedInput-root": { backgroundColor: "#2a2a2a", @@ -177,7 +181,7 @@ function RenameScriptDialog({ open, value, onChange, onCancel, onSave }) { } // Local dialog: new script (name + type) -function NewScriptDialog({ open, name, type, onChangeName, onChangeType, onCancel, onCreate }) { +function NewScriptDialog({ open, name, type, onChangeName, onChangeType, onCancel, onCreate, onBlurName }) { return (
} sx={{ display: open ? "block" : "none" }}>
onChangeName(e.target.value)} + onBlur={() => onBlurName && onBlurName()} sx={{ "& .MuiOutlinedInput-root": { backgroundColor: "#2a2a2a", @@ -344,9 +349,11 @@ export default function ScriptEditor() { setNewScriptOpen(true); return; } + // Ensure filename extension aligns with selected type when saving new file by name + const normalizedName = currentPath ? undefined : ensureExt(fileName, type); const payload = { path: currentPath || undefined, - name: currentPath ? undefined : fileName, + name: normalizedName, content: code, type }; @@ -400,10 +407,11 @@ export default function ScriptEditor() { const saveRenameFile = async () => { try { + const finalName = ensureExt(renameValue, type); const res = await fetch("/api/scripts/rename_file", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ path: selectedNode.path, new_name: renameValue, type }) + body: JSON.stringify({ path: selectedNode.path, new_name: finalName, type }) }); const data = await res.json(); if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`); @@ -491,7 +499,7 @@ export default function ScriptEditor() { }; const rootChildIds = tree[0]?.children?.map((c) => c.id) || []; - const highlighted = useMemo(() => highlightedHtml(code, prismLang), [code, prismLang]); + // live highlighting handled by react-simple-code-editor const renderItems = (nodes) => (nodes || []).map((n) => ( @@ -523,7 +531,7 @@ export default function ScriptEditor() { return ( {/* Left: Tree */} - + Scripts @@ -575,7 +583,7 @@ export default function ScriptEditor() { {/* Right: Editor */} - + Type @@ -622,34 +630,25 @@ export default function ScriptEditor() { - {/* Editor input */} - setCode(e.target.value)} - sx={{ - flex: 0, - mb: 2, - "& .MuiOutlinedInput-root": { - backgroundColor: "#121212", + {/* Single-pane live-highlight editor */} + + highlightedHtml(src, prismLang)} + padding={12} + placeholder={currentPath ? `Editing: ${currentPath}` : "New Script..."} + style={{ + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontSize: 14, color: "#e6edf3", - fontFamily: "monospace", - fontSize: "0.9rem", + background: "#121212", + outline: "none", + minHeight: 300, lineHeight: 1.4, - "& fieldset": { borderColor: "#444" }, - "&:hover fieldset": { borderColor: "#666" } - } - }} - /> - - {/* Highlighted preview */} - - Preview (syntax highlighted) -
-            
-          
+ caretColor: "#58a6ff" + }} + />
@@ -660,6 +659,7 @@ export default function ScriptEditor() { onChange={setRenameValue} onCancel={() => setRenameOpen(false)} onSave={saveRenameFile} + onBlur={() => setRenameValue((v) => ensureExt(v, type))} /> setNewScriptOpen(false)} onCreate={createNewScript} + onBlurName={() => setNewScriptName((v) => ensureExt(v, newScriptType))} />