add: template editor
This commit is contained in:
65
src/components/MarkdownTemplate/EnumsEditor.js
Normal file
65
src/components/MarkdownTemplate/EnumsEditor.js
Normal 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;
|
||||
16
src/components/MarkdownTemplate/LayoutEditor.js
Normal file
16
src/components/MarkdownTemplate/LayoutEditor.js
Normal 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;
|
||||
90
src/components/MarkdownTemplate/MarkdownTemplateEditor.js
Normal file
90
src/components/MarkdownTemplate/MarkdownTemplateEditor.js
Normal 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;
|
||||
84
src/components/MarkdownTemplate/ParametersManager.js
Normal file
84
src/components/MarkdownTemplate/ParametersManager.js
Normal 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;
|
||||
30
src/components/MarkdownTemplate/TemplateSelector.js
Normal file
30
src/components/MarkdownTemplate/TemplateSelector.js
Normal 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;
|
||||
94
src/components/MarkdownTemplate/TypeEditor.js
Normal file
94
src/components/MarkdownTemplate/TypeEditor.js
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
172
src/components/Markdowns/TemplatedEditor.js
Normal file
172
src/components/Markdowns/TemplatedEditor.js
Normal 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;
|
||||
76
src/components/Modals/MarkdownSettingModal.js
Normal file
76
src/components/Modals/MarkdownSettingModal.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import {useCreateMarkdownSetting, useMarkdownSetting} from "../../utils/queries/setting-queries";
|
||||
import {useSaveMarkdown} from "../../utils/queries/markdown-queries";
|
||||
import {useState} from "react";
|
||||
import MarkdownTemplateSettingPanel from "../Settings/MarkdownSettings/MarkdownTemplateSettingPanel";
|
||||
|
||||
const MarkdownSettingModal = ({isOpen, markdown, onClose}) => {
|
||||
const {data: markdownSetting, isFetching: markdownSettingIsFetching} = useMarkdownSetting(markdown?.setting_id || 0);
|
||||
const createMarkdownSetting = useCreateMarkdownSetting();
|
||||
const updateMarkdown = useSaveMarkdown();
|
||||
|
||||
const [activeTab, setActiveTab] = useState("template");
|
||||
|
||||
const handleCreateMarkdownSetting = () => {
|
||||
createMarkdownSetting.mutate({}, {
|
||||
onSuccess: (res) => {
|
||||
updateMarkdown.mutate({
|
||||
id: markdown.id,
|
||||
data: {
|
||||
setting_id: res.id
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if(markdownSettingIsFetching)
|
||||
return(<p>Loading...</p>);
|
||||
|
||||
return (
|
||||
<div className={`modal ${isOpen ? "is-active" : ""}`}>
|
||||
<div className="modal-background" onClick={onClose} />
|
||||
<div className="modal-card" style={{width: "60vw"}}>
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">Markdown Settings</p>
|
||||
<button
|
||||
className="delete"
|
||||
type="button"
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</header>
|
||||
{
|
||||
markdownSetting ? (
|
||||
<section className="modal-card-body">
|
||||
<button
|
||||
className="button is-primary"
|
||||
type="button"
|
||||
onClick={handleCreateMarkdownSetting}
|
||||
>
|
||||
Create Markdown Setting
|
||||
</button>
|
||||
</section>
|
||||
) : (
|
||||
<section className="modal-card-body">
|
||||
<div className="tabs">
|
||||
<ul>
|
||||
<li className={activeTab==="template" ? "is-active" : ""}>
|
||||
<a onClick={() => setActiveTab("template")}>Template</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{activeTab === "template" && (
|
||||
<MarkdownTemplateSettingPanel
|
||||
markdown={markdownSetting}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useUpdatePath} from "../../utils/path-queries";
|
||||
import {useCreatePathSetting, usePathSetting} from "../../utils/setting-queries";
|
||||
import {useUpdatePath} from "../../utils/queries/path-queries";
|
||||
import {useCreatePathSetting, usePathSetting} from "../../utils/queries/setting-queries";
|
||||
import WebhookSettingPanel from "../Settings/PathSettings/WebhookSettingPanel";
|
||||
import React, {useState} from "react";
|
||||
const PathSettingModal = ({ isOpen, path, onClose }) => {
|
||||
@@ -18,17 +18,22 @@ const PathSettingModal = ({ isOpen, path, onClose }) => {
|
||||
|
||||
|
||||
if(isPathSettingLoading)
|
||||
return <p>Loading...</p>
|
||||
return (<p>Loading...</p>);
|
||||
|
||||
return (
|
||||
<div className={`modal ${isOpen ? "is-active" : ""}`}>
|
||||
<div className="modal-background" onClick={onClose}></div>
|
||||
<div className="modal-background" onClick={onClose} />
|
||||
<div className="modal-card" style={{width: "60vw"}}>
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">Path Settings</p>
|
||||
<button type="button" className="delete" aria-label="close" onClick={onClose} />
|
||||
<button
|
||||
type="button"
|
||||
className="delete"
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</header>
|
||||
{!pathSetting && !isPathSettingLoading ? (
|
||||
{!pathSetting ? (
|
||||
<section className="modal-card-body">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -72,9 +72,7 @@ const MainNavigation = () => {
|
||||
console.log(contentDisposition);
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
||||
console.log(match);
|
||||
if (match && match[1]) {
|
||||
console.log(match[1]);
|
||||
filename = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, {useState} from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import PermissionGuard from "../PermissionGuard";
|
||||
import "./PathNode.css";
|
||||
import {useDeletePath, useMovePath, useUpdatePath} from "../../utils/path-queries";
|
||||
import {useIndexMarkdown, useMoveMarkdown} from "../../utils/markdown-queries";
|
||||
import {useDeletePath, useMovePath, useUpdatePath} from "../../utils/queries/path-queries";
|
||||
import {useIndexMarkdown, useMoveMarkdown} from "../../utils/queries/markdown-queries";
|
||||
import MarkdownNode from "./MarkdownNode";
|
||||
import PathSettingModal from "../Modals/PathSettingModal";
|
||||
|
||||
@@ -11,7 +11,7 @@ const PathNode = ({ path, isRoot = false }) => {
|
||||
const [isPathSettingModalOpen, setIsPathSettingModalOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(isRoot);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [newName, setNewName] = useState(path.name);
|
||||
const [newName, setNewName] = useState(path.name || "");
|
||||
|
||||
const deletePath = useDeletePath();
|
||||
const updatePath = useUpdatePath();
|
||||
@@ -130,8 +130,6 @@ const PathNode = ({ path, isRoot = false }) => {
|
||||
<button
|
||||
className="button is-small is-success"
|
||||
onClick={() => {
|
||||
|
||||
console.log("path", path);
|
||||
setIsPathSettingModalOpen(true);
|
||||
}}
|
||||
type="button"
|
||||
@@ -217,6 +215,7 @@ const PathNode = ({ path, isRoot = false }) => {
|
||||
<MarkdownNode
|
||||
markdown={markdown}
|
||||
handleMoveMarkdown={handleMoveMarkdown}
|
||||
key={markdown.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PermissionGuard from "../PermissionGuard";
|
||||
import PathNode from "./PathNode";
|
||||
import "./SideNavigation.css";
|
||||
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
||||
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/queries/path-queries";
|
||||
import React from 'react';
|
||||
import {useTree} from "../../utils/tree-queries";
|
||||
import {useTree} from "../../utils/queries/tree-queries";
|
||||
|
||||
const SideNavigation = () => {
|
||||
const {data: tree, isLoading, error} = useTree();
|
||||
@@ -71,6 +71,12 @@ const SideNavigation = () => {
|
||||
>
|
||||
Create New Markdown
|
||||
</a>
|
||||
<a
|
||||
href="/template/create"
|
||||
className="button is-primary is-small"
|
||||
>
|
||||
Create New Template
|
||||
</a>
|
||||
</PermissionGuard>
|
||||
{!filteredTree || filteredTree.length === 0 ?
|
||||
<p>No Result</p> :
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
|
||||
import React, {useEffect, useState, useRef, useContext} from "react";
|
||||
import {useCreatePath, usePath, usePaths} from "../utils/path-queries";
|
||||
import {useCreatePath, usePaths} from "../utils/queries/path-queries";
|
||||
import { useQueryClient } from "react-query";
|
||||
import "./PathManager.css";
|
||||
import {fetch_} from "../utils/request-utils";
|
||||
import {ConfigContext} from "../ConfigProvider";
|
||||
import PathSettingModal from "./Modals/PathSettingModal";
|
||||
|
||||
|
||||
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||
const { data: currentPath } = usePath(currentPathId);
|
||||
const [currentFullPath, setCurrentFullPath] = useState([{ name: "Root", id: 1 }]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [dropdownActive, setDropdownActive] = useState(false);
|
||||
@@ -20,11 +17,7 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
|
||||
const createPath = useCreatePath();
|
||||
const config = useContext(ConfigContext).config;
|
||||
const [isPathSettingModalOpen, setIsPathSettingModalModalOpen] = useState(false);
|
||||
|
||||
const handleSettingClick = () => {
|
||||
setIsPathSettingModalModalOpen(true);
|
||||
}
|
||||
|
||||
const buildFullPath = async (pathId) => {
|
||||
const path = [];
|
||||
@@ -117,31 +110,16 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||
{currentFullPath.map((path, index) => (
|
||||
<span
|
||||
key={path.id}
|
||||
className="breadcrumb-item is-clickable"
|
||||
className="tag is-clickable is-link is-light"
|
||||
onClick={() => handlePathClick(path.id, index)}
|
||||
>
|
||||
{path.name}
|
||||
{index < currentFullPath.length - 1 && " / "}
|
||||
{path.name + "/"}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="control">
|
||||
<span> </span>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-small is-primary"
|
||||
type="button"
|
||||
onClick={handleSettingClick}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
<PathSettingModal
|
||||
isOpen={isPathSettingModalOpen}
|
||||
path={currentPath}
|
||||
onClose={() => setIsPathSettingModalModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="path-manager-body">
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
useCreateMarkdownTemplateSetting,
|
||||
useMarkdownTemplate,
|
||||
useMarkdownTemplates,
|
||||
useMarkdownTemplateSetting, useUpdateMarkdownTemplateSetting
|
||||
} from "../../../utils/queries/template-queries";
|
||||
import {useState} from "react";
|
||||
import {useUpdateMarkdownSetting} from "../../../utils/queries/setting-queries";
|
||||
|
||||
const MarkdownTemplateSettingPanel = ({markdownSetting, onClose}) => {
|
||||
const {data: setting, isFetching: settingIsFetching } = useMarkdownTemplateSetting(markdownSetting.template_setting_id || 0);
|
||||
const {data: templates, isFetching: templatesAreFetching}=useMarkdownTemplates();
|
||||
const {data: template, isFetching: templateIsFetching} = useMarkdownTemplate(setting?.template_id);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState(template);
|
||||
|
||||
const createMarkdownTemplateSetting = useCreateMarkdownTemplateSetting();
|
||||
const updateMarkdownSetting = useUpdateMarkdownSetting();
|
||||
const updateMarkdownTemplateSetting = useUpdateMarkdownTemplateSetting();
|
||||
const handleCreateTemplateSetting = () => {
|
||||
createMarkdownTemplateSetting.mutate({}, {
|
||||
onSuccess: (data) => {
|
||||
updateMarkdownSetting.mutate({
|
||||
id: markdownSetting.id,
|
||||
data: {
|
||||
template_setting_id: data.id,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveMarkdownTemplateSetting = () => {
|
||||
updateMarkdownTemplateSetting.mutate({
|
||||
id: setting.id,
|
||||
data: {
|
||||
template_id: selectedTemplate.id,
|
||||
}
|
||||
}, {
|
||||
onSuccess: () => alert("Saved"),
|
||||
onError: () => alert("Failed to save"),
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (settingIsFetching || templatesAreFetching || templatesAreFetching || templateIsFetching) {
|
||||
return (<p>Loading...</p>);
|
||||
}
|
||||
return setting ? (
|
||||
<div className="box" style={{marginTop: "1rem"}}>
|
||||
<h4 className="title is-5">Template Setting</h4>
|
||||
<div className="field">
|
||||
<label className="label">Use Template</label>
|
||||
<div className="select is-fullwidth">
|
||||
<select
|
||||
value={selectedTemplate.title}
|
||||
onChange={(e) => setSelectedTemplate(e.target.value)}
|
||||
>
|
||||
<option value="">(default)</option>
|
||||
{templates.map((_template, index) => (
|
||||
<option key={index} value={_template}>{_template.title}</option>
|
||||
))}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="button is-primary"
|
||||
type="button"
|
||||
onClick={handleSaveMarkdownTemplateSetting}
|
||||
>
|
||||
Save Template Setting
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="button is-primary"
|
||||
type="button"
|
||||
onClick={handleCreateTemplateSetting}
|
||||
>
|
||||
Create Template Setting
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownTemplateSettingPanel;
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
useCreateWebhookSetting,
|
||||
useUpdateWebhookSetting,
|
||||
useWebhooks,
|
||||
} from "../../../utils/webhook-queries";
|
||||
} from "../../../utils/queries/webhook-queries";
|
||||
|
||||
import {
|
||||
useCreateWebhook,
|
||||
useUpdateWebhook,
|
||||
useDeleteWebhook,
|
||||
} from "../../../utils/webhook-queries";
|
||||
import {useUpdatePathSetting} from "../../../utils/setting-queries";
|
||||
} from "../../../utils/queries/webhook-queries";
|
||||
import {useUpdatePathSetting} from "../../../utils/queries/setting-queries";
|
||||
|
||||
const WebhookSettingPanel = ({pathSetting, onClose}) => {
|
||||
|
||||
@@ -89,7 +89,12 @@ const WebhookSettingPanel = ({pathSetting, onClose}) => {
|
||||
const handleCreateWebhookSetting = () => {
|
||||
createWebhookSetting.mutate({}, {
|
||||
onSuccess: (data) => {
|
||||
updatePathSetting.mutate({id: pathSetting.id, data: {webhook_setting_id: data.id}});
|
||||
updatePathSetting.mutate({
|
||||
id: pathSetting.id,
|
||||
data: {
|
||||
webhook_setting_id: data.id
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -115,9 +120,6 @@ const WebhookSettingPanel = ({pathSetting, onClose}) => {
|
||||
}
|
||||
|
||||
const found = webhooks.find((wh) => wh.id === setting.webhook_id);
|
||||
console.log("found", found);
|
||||
console.log("webhooks", webhooks);
|
||||
console.log("setting.webhook_id", setting.webhook_id);
|
||||
setSelectedUrl(found ? found.hook_url : "");
|
||||
} else {
|
||||
setEnabled(false);
|
||||
@@ -191,8 +193,6 @@ const WebhookSettingPanel = ({pathSetting, onClose}) => {
|
||||
on_events: onEvents,
|
||||
|
||||
};
|
||||
console.log(webhooks);
|
||||
console.log(payload);
|
||||
if(!setting || !setting.id){
|
||||
createWebhookSetting.mutate(payload, {
|
||||
onSuccess: (res) => {
|
||||
|
||||
Reference in New Issue
Block a user