add: template editor

This commit is contained in:
h z
2025-04-14 17:02:22 +01:00
parent 09338a2683
commit 947b59e3ea
29 changed files with 1277 additions and 166 deletions

View File

@@ -4,14 +4,18 @@ import "katex/dist/katex.min.css";
import "./MarkdownContent.css";
import MarkdownView from "./MarkdownView";
import PermissionGuard from "../PermissionGuard";
import {useMarkdown} from "../../utils/markdown-queries";
import {usePath} from "../../utils/path-queries";
import {useMarkdown} from "../../utils/queries/markdown-queries";
import {usePath} from "../../utils/queries/path-queries";
import {useMarkdownSetting} from "../../utils/queries/setting-queries";
import {useMarkdownTemplate} from "../../utils/queries/template-queries";
const MarkdownContent = () => {
const { id } = useParams();
const [indexTitle, setIndexTitle] = useState(null);
const {data: markdown, isLoading, error} = useMarkdown(id);
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
const {data: template_setting, isFetching: isTemplateSettingFetching} = useMarkdownTemplate(setting?.template_setting_id);
useEffect(() => {
@@ -21,7 +25,9 @@ const MarkdownContent = () => {
}, [markdown, path]);
if (isLoading || isPathFetching) {
const notReady = isLoading || isPathFetching || isSettingFetching || isTemplateSettingFetching;
if (notReady) {
return <div>Loading...</div>;
}
@@ -39,8 +45,7 @@ const MarkdownContent = () => {
</Link>
</PermissionGuard>
</div>
<MarkdownView content={markdown.content}/>
<MarkdownView content={JSON.parse(markdown.content)} template={template_setting}/>
</div>
);
};

View File

@@ -5,23 +5,29 @@ import "katex/dist/katex.min.css";
import "./MarkdownEditor.css";
import PathManager from "../PathManager";
import MarkdownView from "./MarkdownView";
import { useMarkdown, useSaveMarkdown } from "../../utils/markdown-queries";
import { useMarkdown, useSaveMarkdown } from "../../utils/queries/markdown-queries";
import {useMarkdownSetting} from "../../utils/queries/setting-queries";
import {useMarkdownTemplate} from "../../utils/queries/template-queries";
import TemplatedEditor from "./TemplatedEditor";
const MarkdownEditor = () => {
const { roles } = useContext(AuthContext);
const navigate = useNavigate();
const { id } = useParams();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [content, setContent] = useState({});
const [shortcut, setShortcut] = useState("");
const [pathId, setPathId] = useState(1);
const {data: markdown, isLoading, error} = useMarkdown(id);
const saveMarkdown = useSaveMarkdown();
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.id);
const {data: template, isFetching: isTemplateFetching} = useMarkdownTemplate(setting?.id);
const notReady = isLoading || isTemplateFetching || isSettingFetching;
useEffect(() => {
if(markdown){
setTitle(markdown.title);
setContent(markdown.content);
setContent(JSON.parse(markdown.content));
setShortcut(markdown.shortcut);
setPathId(markdown.path_id);
}
@@ -29,7 +35,7 @@ const MarkdownEditor = () => {
const handleSave = () => {
saveMarkdown.mutate(
{id, data: {title, content, path_id: pathId, shortcut}},
{id, data: {title, content: JSON.stringify(content), path_id: pathId, shortcut}},
{
onSuccess: () => {
navigate("/");
@@ -45,12 +51,12 @@ const MarkdownEditor = () => {
if (!hasPermission)
return <div className="notification is-danger">Permission Denied</div>;
if(isLoading)
if(notReady)
return <p>Loading...</p>;
console.log(markdown?.id);
if(error)
return <p>{error.message || "Failed to load markdown"}</p>;
return (
<div className="container mt-5 markdown-editor-container">
<h2 className="title is-4">{id ? "Edit Markdown" : "Create Markdown"}</h2>
@@ -93,13 +99,14 @@ const MarkdownEditor = () => {
<div className="field">
<label className="label">Content</label>
<div className="control">
<textarea
style={{ height: "70vh" }}
className="textarea"
placeholder="Enter Markdown content"
value={content}
onChange={(e) => setContent(e.target.value)}
></textarea>
<TemplatedEditor
style={{height: "70vh"}}
content={content}
template={template}
onContentChanged={(k, v) => setContent(
prev => ({...prev, [k]: v})
)}
/>
</div>
</div>
@@ -120,7 +127,7 @@ const MarkdownEditor = () => {
<div className="column is-half">
<h3 className="subtitle is-5">Preview</h3>
<MarkdownView content={content} height='70vh'/>
<MarkdownView content={content} template={template} height='70vh'/>
</div>
</div>
</div>

View File

@@ -8,18 +8,68 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { okaidia } from "react-syntax-highlighter/dist/esm/styles/prism";
import "katex/dist/katex.min.css";
import "./MarkdownView.css";
import {useLinks} from "../../utils/markdown-queries";
import {useLinks} from "../../utils/queries/markdown-queries";
const MarkdownView = ({ content, height="auto" }) => {
const Translate = ({variable, value}) => {
if (variable.type.base_type === "markdown" || variable.type.base_type === "string" || variable.type.base_type === "enum") {
return value;
}
if(variable.type.base_type === "list"){
if (!variable.type.extend_type)
return [];
return value.map((item, index) => Translate({
variable: {name: index, type: variable.type.extend_type},
value: item,
})).map((item) => variable.type.definition.iter_layout.replaceAll('<item/>', item));
}
if(variable.type.base_type === "template"){
return ParseTemplate({
template: variable.type.definition.template,
variables: value
});
}
};
const ParseTemplate = ({template, variables}) => {
let res = template.layout;
for (const parameter of template.parameters) {
res = res.replaceAll(`<${parameter.name}/>`, Translate({
variable: parameter,
value: variables[parameter.name]
}));
}
return res;
};
const MarkdownView = ({ content, template, height="auto" }) => {
const {data: links, isLoading} = useLinks();
if (isLoading)
return <p>Loading...</p>
const definitions = "\n<!-- Definitions -->\n" + links.join("\n");
return (<p>Loading...</p>);
const linkDefinitions = "\n<!-- Definitions -->\n" + links.join("\n");
const _template = template || {
parameters: [
{
name: "markdown",
type: {
base_type: "markdown",
definition: {}
}
}
],
layout: "<markdown/>",
title: "default"
};
return (
<div className="markdown-preview" style={{height}}>
<ReactMarkdown
children={content + "\n" + definitions}
children={ParseTemplate({
template: _template,
variables: content
}) + "\n" + linkDefinitions}
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex, rehypeRaw]}
components={{

View File

@@ -0,0 +1,172 @@
import React, {useState} from "react";
import {useMarkdownTemplate} from "../../utils/queries/template-queries";
const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged }) => {
if(variable.type.base_type === "string") {
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
<input
type="text"
className="input"
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
/>
<hr/>
</div>
);
}
if (variable.type.base_type === 'markdown') {
return(
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
<textarea
className="textarea"
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
/>
<hr/>
</div>
);
}
if(variable.type.base_type === "enum"){
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
<select
value={value}
onChange={(e) => onContentChanged(variable.name, e.target.value)}
>
{variable.type.definition.enums.map((item) => (
<option key={item} value={item}>{item}</option>
))}
</select>
<hr/>
</div>
);
}
if(variable.type.base_type === "list"){
const [cache, setCache] = useState(value);
const defaultValue = variable.type.definition.default || undefined;
const addItem = () => {
setCache(prev => {
const newCache = [...prev, defaultValue];
onContentChanged(variable.name, newCache);
return newCache;
});
};
const removeItem = (index) => {
setCache(prev => {
const newCache = prev.filter((_, i) => i!==index);
onContentChanged(variable.name, newCache);
return newCache;
})
};
const _onContentChanged = (index, value) => {
setCache(prev => {
const newCache = [...prev];
newCache[index] = value;
onContentChanged(variable.name, newCache);
return newCache;
});
};
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}[{variable.type.extend_type.base_type}]</label>
{cache.map((item, index) =>
(<div key={index}>
<TemplatedEditorComponent
variable={{name: index, type: variable.type.extend_type}}
value={item}
namespace={namespace}
onContentChanged={_onContentChanged}
/>
<button
className="button is-danger"
type="button"
onClick={() => removeItem(index)}
>
DELETE
</button>
</div>)
)}
<button className="button is-warning" type="button" onClick={addItem}>
ADD
</button>
<hr/>
</div>
);
}
if(variable.type.base_type === 'template'){
const {data: _template, isFetching: _templateIsFetching} = useMarkdownTemplate(variable.type.definition.template_id);
if(_templateIsFetching){
return(<p>Loading...</p>);
}
const _parameters = _template.parameters;
const _namespace = namespace + _template.title+ "::"
const _onSubChanged = (subKey, subValue) => {
const updated = {
...value,
[subKey]: subValue
};
onContentChanged(variable.name, updated);
};
return (
<div>
<label>{namespace}{variable.name} - {variable.type.base_type}</label>
{
_parameters.map((_variable, index) => (
<div key={index}>
<TemplatedEditorComponent
variable={_variable}
value={value[_variable.name]}
namespace={_namespace}
onContentChanged={_onSubChanged}
/>
</div>
))
}
<hr/>
</div>
);
}
};
const TemplatedEditor = ({content, template, onContentChanged}) => {
if(!template){
template = {
parameters: [
{
name: "markdown",
type: {
base_type: "markdown",
definition: {}
}
}
],
layout: "<markdown/>",
title: "default"
};
}
return(
<div>
{
template.parameters.map((variable, index) => (
<div key={index}>
<TemplatedEditorComponent
variable={variable}
value={content[variable.name]}
namespace={template.title+"::"}
onContentChanged={onContentChanged}
/>
</div>
))
}
</div>
);
};
export default TemplatedEditor;