improved: error handling, form state, paste fetching and monaco editor config

This commit is contained in:
Jose
2026-03-07 21:51:47 +01:00
parent 4d0c4d3f26
commit 69140e6da1
3 changed files with 98 additions and 88 deletions

View File

@@ -1,16 +1,29 @@
import Editor from "@monaco-editor/react";
import { useTheme } from "@/hooks/useTheme";
import { useRef } from "react";
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";
import * as monaco from "monaco-editor";
const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
const CodeEditor = ({ className = "", syntax, readOnly, onChange, value, editorErrors = [] }) => {
const { theme } = useTheme();
const editorRef = useRef(null);
const onMount = (editor) => {
editorRef.current = editor;
editor.focus();
}
useEffect(() => {
if (!editorRef.current) return;
const model = editorRef.current.getModel();
if (!model) return;
monaco.editor.setModelMarkers(model, "owner", editorErrors.map(err => ({
startLineNumber: err.lineNumber,
startColumn: 1,
endLineNumber: err.lineNumber,
endColumn: model.getLineLength(err.lineNumber) + 1,
message: err.message,
severity: monaco.MarkerSeverity.Error
})));
}, [editorErrors]);
const onMount = (editor) => { editorRef.current = editor; editor.focus(); }
return (
<div className={`code-editor ${className}`}>
@@ -18,7 +31,7 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
language={syntax || "plaintext"}
value={value || ""}
theme={theme === "dark" ? "vs-dark" : "vs-light"}
onChange={(value) => onChange?.(value)}
onChange={onChange}
onMount={onMount}
options={{
minimap: { enabled: false },
@@ -30,10 +43,6 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
scrollbar: { verticalScrollbarSize: 0 },
wordWrap: "on",
formatOnPaste: true,
suggest: {
showFields: true,
showFunctions: true,
},
readOnly: readOnly || false,
}}
/>
@@ -47,6 +56,7 @@ CodeEditor.propTypes = {
readOnly: PropTypes.bool,
onChange: PropTypes.func,
value: PropTypes.string,
editorErrors: PropTypes.array,
};
export default CodeEditor;
export default CodeEditor;

View File

@@ -14,78 +14,76 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
const { pasteKey } = useParams();
const navigate = useNavigate();
const { getData } = useDataContext();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [syntax, setSyntax] = useState("");
const [burnAfter, setBurnAfter] = useState(false);
const [isPrivate, setIsPrivate] = useState(false);
const [password, setPassword] = useState("");
const [formData, setFormData] = useState({
title: "",
content: "",
syntax: "",
burnAfter: false,
isPrivate: false,
password: ""
});
const [selectedPaste, setSelectedPaste] = useState(null);
const [error, setError] = useState(null);
const [editorErrors, setEditorErrors] = useState([]);
const [fieldErrors, setFieldErrors] = useState({});
const [showPasswordModal, setShowPasswordModal] = useState(false);
const handleSubmit = (e) => {
const handleSubmit = async (e) => {
e.preventDefault();
const paste = {
title,
content,
syntax,
burnAfter: burnAfter,
isPrivate: isPrivate,
password: password || null,
};
if (onSubmit) onSubmit(paste);
};
setFieldErrors({});
setEditorErrors([]);
const handleSelectPaste = async (key) => {
navigate(`/${key}`);
};
const fetchPaste = async (key, pwd = "") => {
const url = import.meta.env.MODE === 'production' ?
`https://api.miarma.net/v2/mpaste/pastes/${key}` :
`http://localhost:8081/v2/mpaste/pastes/${key}`;
const { data, error } = await getData(url, {}, false, {
'X-Paste-Password': pwd
});
if (error) {
if (error?.status === 401) {
setShowPasswordModal(true);
return;
try {
if (onSubmit) await onSubmit(formData);
} catch (error) {
if (error.status === 422 && error.errors) {
const newFieldErrors = {};
Object.entries(error.errors).forEach(([field, msg]) => {
if (field === "content") {
setEditorErrors([{ lineNumber: 1, message: msg }]);
} else {
newFieldErrors[field] = msg;
}
});
setFieldErrors(newFieldErrors);
} else {
setError(error);
setSelectedPaste(null);
return;
showError(error);
}
}
setError(null);
setSelectedPaste(data);
setTitle(data.title);
setContent(data.content);
setSyntax(data.syntax || "plaintext");
};
useEffect(() => {
if (pasteKey) fetchPaste(pasteKey);
}, [pasteKey]);
const handleSelectPaste = async (key) => navigate(`/${key}`);
const fetchPaste = async (key, pwd = "") => {
const url = import.meta.env.MODE === 'production'
? `https://api.miarma.net/v2/mpaste/pastes/${key}`
: `http://localhost:8081/v2/mpaste/pastes/${key}`;
const data = await getData(url, { password: pwd }, false);
if (!data) return;
setSelectedPaste(data);
setFormData({
title: data.title ?? "",
content: data.content ?? "",
syntax: data.syntax || "plaintext",
burnAfter: data.burnAfter || false,
isPrivate: data.isPrivate || false,
password: ""
});
};
useEffect(() => { if (pasteKey) fetchPaste(pasteKey); }, [pasteKey]);
const handleChange = (key, value) => {
setFormData(prev => ({ ...prev, [key]: value }));
};
return (
<>
<div className="paste-panel border-0 flex-fill d-flex flex-column min-h-0 p-3">
{error &&
<Alert variant="danger" onClose={() => setError(null)} dismissible>
<strong>
<span className="text-danger">{
error.status == 404 ? "404: Paste no encontrada." :
"Ha ocurrido un error al cargar la paste."
}</span>
</strong>
</Alert>
}
<Form onSubmit={handleSubmit} className="flex-fill d-flex flex-column min-h-0">
<Row className="g-3 flex-fill min-h-0">
<Col xs={12} lg={2} className="order-last order-lg-first d-flex flex-column flex-fill min-h-0 overflow-hidden">
@@ -111,10 +109,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
<Col xs={12} lg={7} className="d-flex flex-column flex-fill min-h-0 overflow-hidden">
<CodeEditor
className="flex-fill custom-border rounded-4 overflow-hidden pt-4 pe-4"
syntax={syntax}
syntax={formData.syntax}
readOnly={!!selectedPaste}
onChange={selectedPaste ? undefined : setContent}
value={content}
onChange={(val) => handleChange("content", val)}
value={formData.content ?? ""}
editorErrors={editorErrors}
/>
</Col>
@@ -132,10 +131,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
<Form.Control
disabled={!!selectedPaste}
type="text"
placeholder="Título de la paste"
value={title}
onChange={(e) => setTitle(e.target.value)}
value={formData.title}
onChange={(e) => handleChange("title", e.target.value)}
isInvalid={!!fieldErrors.title}
/>
<Form.Control.Feedback type="invalid">{fieldErrors.title}</Form.Control.Feedback>
</FloatingLabel>
<FloatingLabel
@@ -149,8 +149,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
>
<Form.Select
disabled={!!selectedPaste}
value={syntax}
onChange={(e) => setSyntax(e.target.value)}
value={formData.syntax}
onChange={(e) => handleChange("syntax", e.target.value)}
>
<option value="">Sin resaltado</option>
<option value="javascript">JavaScript</option>
@@ -189,8 +189,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
disabled={!!selectedPaste}
id="burnAfter"
label="volátil"
checked={burnAfter}
onChange={(e) => setBurnAfter(e.target.checked)}
checked={formData.burnAfter}
onChange={(e) => handleChange("burnAfter", e.target.checked)}
className="ms-1 d-flex gap-2 align-items-center"
/>
@@ -199,13 +199,13 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
disabled={!!selectedPaste}
id="isPrivate"
label="privado"
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
checked={formData.isPrivate}
onChange={(e) => handleChange("isPrivate", e.target.checked)}
className="ms-1 d-flex gap-2 align-items-center"
/>
{isPrivate && (
<PasswordInput onChange={(e) => setPassword(e.target.value)} />
{formData.isPrivate && (
<PasswordInput onChange={(e) => handleChange("password", e.target.value)} />
)}
<div className="d-flex justify-content-end">
@@ -227,7 +227,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
onClose={() => setShowPasswordModal(false)}
onSubmit={(pwd) => {
setShowPasswordModal(false);
fetchPaste(pasteKey, pwd); // reintentas con la pass
fetchPaste(pasteKey, pwd);
}}
/>
</>