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

@@ -0,0 +1,65 @@
import React, { useState } from "react";
const EnumsEditor = ({ enums, onChange }) => {
const [_enums, setEnums] = useState(enums || []);
return (
<div className="box">
<ul>
{_enums.map((item, index) => (
<li key={index} className="field has-addons" style={{ marginBottom: "0.5rem" }}>
<div className="control is-expanded">
<input
className="input is-small"
type="text"
value={item}
onChange={(e) => {
const updated = [..._enums];
updated[index] = e.target.value;
setEnums(updated);
onChange(updated);
}}
/>
</div>
<div className="control">
<button
className="button is-small is-danger"
type="button"
onClick={() => {
const updated = [..._enums];
updated.splice(index, 1);
setEnums(updated);
onChange(updated);
}}
>
<span className="icon is-small">
<i className="fas fa-times" />
</span>
</button>
</div>
</li>
))}
</ul>
<div className="field">
<div className="control">
<button
className="button is-small is-primary"
type="button"
onClick={() => {
const updated = [..._enums, ""];
setEnums(updated);
onChange(updated);
}}
>
<span className="icon is-small">
<i className="fas fa-plus" />
</span>
<span>Add Enum</span>
</button>
</div>
</div>
</div>
);
};
export default EnumsEditor;

View File

@@ -0,0 +1,16 @@
import React, {useState} from 'react';
const LayoutEditor = ({layout, onChange}) => {
const [_layout, setLayout] = useState(layout || "");
return (
<textarea
className="textarea"
value={_layout}
onChange={(e) => {
setLayout(e.target.value);
onChange(layout);
}}
/>
);
};
export default LayoutEditor;

View File

@@ -0,0 +1,90 @@
import React, { useContext, useState } from "react";
import { AuthContext } from "../../AuthProvider";
import { useNavigate, useParams } from "react-router-dom";
import { useMarkdownTemplate, useSaveMarkdownTemplate } from "../../utils/queries/template-queries";
import LayoutEditor from "./LayoutEditor";
import ParametersManager from "./ParametersManager";
import "bulma/css/bulma.min.css";
const MarkdownTemplateEditor = () => {
const { roles } = useContext(AuthContext);
if (!roles.includes("admin") || roles.includes("creator"))
return <div className="notification is-danger">Permission Denied</div>;
const navigate = useNavigate();
const { id } = useParams();
const { data: template, isFetching: templateIsFetching } = useMarkdownTemplate(id);
const saveMarkdownTemplate = useSaveMarkdownTemplate();
const [title, setTitle] = useState(template?.id || "");
const [parameters, setParameters] = useState(template?.parameters || []);
const [layout, setLayout] = useState(template?.layout || "");
if (templateIsFetching) {
return <p>Loading...</p>;
}
const handleSave = () => {
saveMarkdownTemplate.mutate(
{ id, data: { title, parameters, layout } },
{
onSuccess: () => {
navigate("/");
},
onError: () => {
alert("Error saving template.");
}
}
);
};
return (
<section className="section">
<div className="container">
<h2 className="title is-4">Markdown Template Editor</h2>
<div className="field">
<label className="label">Title:</label>
<div className="control">
<input
className="input"
type="text"
placeholder="Enter template title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</div>
<div className="columns is-variable is-8">
<div className="column">
<h3 className="title is-5">Layout</h3>
<div className="box">
<LayoutEditor
layout={layout}
parameters={parameters}
onChange={(newLayout) => setLayout(newLayout)}
/>
</div>
</div>
<div className="column">
<h3 className="title is-5">Parameters</h3>
<div className="box">
<ParametersManager
parameters={parameters}
onChange={(newParameters) => setParameters(newParameters)}
/>
</div>
</div>
</div>
<div className="field is-grouped">
<div className="control">
<button className="button is-primary" onClick={handleSave}>
Save Template
</button>
</div>
</div>
</div>
</section>
);
};
export default MarkdownTemplateEditor;

View File

@@ -0,0 +1,84 @@
import React, { useState } from "react";
import TypeEditor from "./TypeEditor";
const ParametersManager = ({ parameters, onChange }) => {
const [_parameters, setParameters] = useState(parameters || []);
const handleAdd = () => {
const updated = [
..._parameters,
{
name: "",
type: { base_type: "string" }
}
];
setParameters(updated);
onChange(updated);
};
const handleNameChange = (index, newName) => {
const updated = [..._parameters];
updated[index].name = newName;
setParameters(updated);
onChange(updated);
};
const handleDelete = (index) => {
const updated = [..._parameters];
updated.splice(index, 1);
setParameters(updated);
onChange(updated);
};
return (
<div className="box">
<div className="field">
<div className="control">
<button className="button is-primary" onClick={handleAdd}>
Add Parameter
</button>
</div>
</div>
{_parameters.map((param, index) => (
<div key={index} className="box" style={{ marginBottom: "1rem" }}>
<div className="field is-grouped is-grouped-multiline">
<div className="control is-expanded">
<label className="label">Name:</label>
<input
type="text"
className="input"
value={param.name}
onChange={(e) => handleNameChange(index, e.target.value)}
placeholder="Parameter name"
/>
</div>
<div className="control">
<button
className="button is-danger"
onClick={() => handleDelete(index)}
>
Delete
</button>
</div>
</div>
<div className="field">
<label className="label">Type:</label>
<div className="control">
<TypeEditor
type={param.type}
onChange={(newType) => {
const updated = [..._parameters];
updated[index].type = newType;
setParameters(updated);
onChange(updated);
}}
/>
</div>
</div>
</div>
))}
</div>
);
};
export default ParametersManager;

View File

@@ -0,0 +1,30 @@
import React, {useState} from "react";
import {useMarkdownTemplates} from "../../utils/queries/template-queries";
const TemplateSelector = ({template, onChange}) => {
const [_template, setTemplate] = useState(template || {
title: "",
parameters: [],
layout: ""
});
const {data:templates, isFetching: templatesAreFetching} = useMarkdownTemplates();
if(templatesAreFetching) {
return <p>Loading...</p>
}
return (
<select
value={_template.title}
onChange={(e) => {
setTemplate(e.target.value);
onChange(e.target.value);
}}>
{
templates.map((tmpl, index) => (
<option key={index} value={tmpl} >{tmpl.title}</option>
))
}
</select>
);
};
export default TemplateSelector;

View File

@@ -0,0 +1,94 @@
import React from 'react';
import EnumsEditor from './EnumsEditor';
import TemplateSelector from './TemplateSelector';
const TypeEditor = ({ type, onChange }) => {
const [_type, setType] = React.useState(type || {});
const renderExtraFields = () => {
switch (_type.base_type) {
case 'enum':
return <EnumsEditor
enums={_type.definition.enums}
onChange={(newEnums) => {
const updated = {
..._type,
definition: {
..._type.definition,
enums: newEnums
}
};
setType(updated);
onChange(updated);
}}
/>;
case 'list':
return (
<div>
<TypeEditor
extendType={_type.extend_type}
onChange={(extendType) => {
const updated = {..._type, extend_type: extendType};
setType(updated);
onChange(updated);
}}
/>
<textarea
className="textarea"
value={_type.definition.iter_layout}
onChange={(e) => {
const updated = {
..._type,
definition: {
..._type.definition,
iter_layout: e.target.value
}
};
setType(updated);
onChange(updated);
}}
/>
</div>
);
case 'template':
return <TemplateSelector
template={_type.definition.template}
onChange={(newTemplate) => {
const updated = {
..._type,
definition: {
..._type.definition,
template: newTemplate
}
};
setType(updated);
onChange(updated);
}}
/>;
default:
return null;
}
};
return (
<div>
<label>type:</label>
<select value={_type.base_type} onChange={(e) => {
const updated = {
base_type: e.target.value,
definition: {}
};
setType(updated);
onChange(updated);
}}>
<option value="string">string</option>
<option value="markdown">markdown</option>
<option value="enum">enum</option>
<option value="list">list</option>
<option value="template">template</option>
</select>
{renderExtraFields()}
</div>
);
};
export default TypeEditor;