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 Editor from "@monaco-editor/react";
import { useTheme } from "@/hooks/useTheme"; import { useTheme } from "@/hooks/useTheme";
import { useRef } from "react"; import { useEffect, useRef } from "react";
import PropTypes from "prop-types"; 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 { theme } = useTheme();
const editorRef = useRef(null); const editorRef = useRef(null);
const onMount = (editor) => { useEffect(() => {
editorRef.current = editor; if (!editorRef.current) return;
editor.focus(); 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 ( return (
<div className={`code-editor ${className}`}> <div className={`code-editor ${className}`}>
@@ -18,7 +31,7 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
language={syntax || "plaintext"} language={syntax || "plaintext"}
value={value || ""} value={value || ""}
theme={theme === "dark" ? "vs-dark" : "vs-light"} theme={theme === "dark" ? "vs-dark" : "vs-light"}
onChange={(value) => onChange?.(value)} onChange={onChange}
onMount={onMount} onMount={onMount}
options={{ options={{
minimap: { enabled: false }, minimap: { enabled: false },
@@ -30,10 +43,6 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
scrollbar: { verticalScrollbarSize: 0 }, scrollbar: { verticalScrollbarSize: 0 },
wordWrap: "on", wordWrap: "on",
formatOnPaste: true, formatOnPaste: true,
suggest: {
showFields: true,
showFunctions: true,
},
readOnly: readOnly || false, readOnly: readOnly || false,
}} }}
/> />
@@ -47,6 +56,7 @@ CodeEditor.propTypes = {
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
onChange: PropTypes.func, onChange: PropTypes.func,
value: PropTypes.string, 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 { pasteKey } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { getData } = useDataContext(); 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 [selectedPaste, setSelectedPaste] = useState(null);
const [error, setError] = useState(null);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const handleSubmit = (e) => { const [formData, setFormData] = useState({
e.preventDefault(); title: "",
const paste = { content: "",
title, syntax: "",
content, burnAfter: false,
syntax, isPrivate: false,
burnAfter: burnAfter, password: ""
isPrivate: isPrivate,
password: password || null,
};
if (onSubmit) onSubmit(paste);
};
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) { const [selectedPaste, setSelectedPaste] = useState(null);
if (error?.status === 401) { const [editorErrors, setEditorErrors] = useState([]);
setShowPasswordModal(true); const [fieldErrors, setFieldErrors] = useState({});
return; const [showPasswordModal, setShowPasswordModal] = useState(false);
} else {
setError(error);
setSelectedPaste(null);
return;
}
}
setError(null); const handleSubmit = async (e) => {
setSelectedPaste(data); e.preventDefault();
setTitle(data.title); setFieldErrors({});
setContent(data.content); setEditorErrors([]);
setSyntax(data.syntax || "plaintext");
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 {
showError(error);
}
}
}; };
useEffect(() => { const handleSelectPaste = async (key) => navigate(`/${key}`);
if (pasteKey) fetchPaste(pasteKey);
}, [pasteKey]); 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 ( return (
<> <>
<div className="paste-panel border-0 flex-fill d-flex flex-column min-h-0 p-3"> <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"> <Form onSubmit={handleSubmit} className="flex-fill d-flex flex-column min-h-0">
<Row className="g-3 flex-fill 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"> <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"> <Col xs={12} lg={7} className="d-flex flex-column flex-fill min-h-0 overflow-hidden">
<CodeEditor <CodeEditor
className="flex-fill custom-border rounded-4 overflow-hidden pt-4 pe-4" className="flex-fill custom-border rounded-4 overflow-hidden pt-4 pe-4"
syntax={syntax} syntax={formData.syntax}
readOnly={!!selectedPaste} readOnly={!!selectedPaste}
onChange={selectedPaste ? undefined : setContent} onChange={(val) => handleChange("content", val)}
value={content} value={formData.content ?? ""}
editorErrors={editorErrors}
/> />
</Col> </Col>
@@ -132,10 +131,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
<Form.Control <Form.Control
disabled={!!selectedPaste} disabled={!!selectedPaste}
type="text" type="text"
placeholder="Título de la paste" value={formData.title}
value={title} onChange={(e) => handleChange("title", e.target.value)}
onChange={(e) => setTitle(e.target.value)} isInvalid={!!fieldErrors.title}
/> />
<Form.Control.Feedback type="invalid">{fieldErrors.title}</Form.Control.Feedback>
</FloatingLabel> </FloatingLabel>
<FloatingLabel <FloatingLabel
@@ -149,8 +149,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
> >
<Form.Select <Form.Select
disabled={!!selectedPaste} disabled={!!selectedPaste}
value={syntax} value={formData.syntax}
onChange={(e) => setSyntax(e.target.value)} onChange={(e) => handleChange("syntax", e.target.value)}
> >
<option value="">Sin resaltado</option> <option value="">Sin resaltado</option>
<option value="javascript">JavaScript</option> <option value="javascript">JavaScript</option>
@@ -189,8 +189,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
disabled={!!selectedPaste} disabled={!!selectedPaste}
id="burnAfter" id="burnAfter"
label="volátil" label="volátil"
checked={burnAfter} checked={formData.burnAfter}
onChange={(e) => setBurnAfter(e.target.checked)} onChange={(e) => handleChange("burnAfter", e.target.checked)}
className="ms-1 d-flex gap-2 align-items-center" className="ms-1 d-flex gap-2 align-items-center"
/> />
@@ -199,13 +199,13 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
disabled={!!selectedPaste} disabled={!!selectedPaste}
id="isPrivate" id="isPrivate"
label="privado" label="privado"
checked={isPrivate} checked={formData.isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)} onChange={(e) => handleChange("isPrivate", e.target.checked)}
className="ms-1 d-flex gap-2 align-items-center" className="ms-1 d-flex gap-2 align-items-center"
/> />
{isPrivate && ( {formData.isPrivate && (
<PasswordInput onChange={(e) => setPassword(e.target.value)} /> <PasswordInput onChange={(e) => handleChange("password", e.target.value)} />
)} )}
<div className="d-flex justify-content-end"> <div className="d-flex justify-content-end">
@@ -227,7 +227,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
onClose={() => setShowPasswordModal(false)} onClose={() => setShowPasswordModal(false)}
onSubmit={(pwd) => { onSubmit={(pwd) => {
setShowPasswordModal(false); setShowPasswordModal(false);
fetchPaste(pasteKey, pwd); // reintentas con la pass fetchPaste(pasteKey, pwd);
}} }}
/> />
</> </>

View File

@@ -92,10 +92,10 @@ export const useData = (config, onError) => {
if (config?.baseUrl) fetchData(); if (config?.baseUrl) fetchData();
}, [config, fetchData]); }, [config, fetchData]);
const requestWrapper = async (method, endpoint, payload = null, refresh = false, extraHeaders = {}) => { const requestWrapper = async (method, endpoint, payload = null, refresh = false) => {
try { try {
const isFormData = payload instanceof FormData; const isFormData = payload instanceof FormData;
const headers = { ...getAuthHeaders(isFormData), ...extraHeaders }; const headers = getAuthHeaders(isFormData);
const cfg = { headers }; const cfg = { headers };
let response; let response;
@@ -131,7 +131,7 @@ export const useData = (config, onError) => {
dataLoading, dataLoading,
dataError, dataError,
clearError, clearError,
getData: (url, params, refresh = true, headers = {}) => requestWrapper("get", url, params, refresh, headers), getData: (url, params, refresh = true) => requestWrapper("get", url, params, refresh),
postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh), postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh),
putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh), putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh),
deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh), deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh),