Compare commits
4 Commits
101666d26d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c9310250e4 | |||
| a08164e914 | |||
| 30a46d5064 | |||
| e5affe3465 |
54
src/App.js
54
src/App.js
@@ -6,6 +6,7 @@ import MainNavigation from "./components/Navigations/MainNavigation";
|
||||
import SideNavigation from "./components/Navigations/SideNavigation";
|
||||
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
||||
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
||||
import StandaloneMarkdownPage from "./components/Markdowns/StandaloneMarkdownPage";
|
||||
import "./App.css";
|
||||
import Callback from "./components/KeycloakCallbacks/Callback";
|
||||
import Footer from "./components/Footer";
|
||||
@@ -18,30 +19,35 @@ const App = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<div className="app-container">
|
||||
<MainNavigation />
|
||||
<div className="content-container">
|
||||
<SideNavigation />
|
||||
<main className="main-content">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Navigate to = "/markdown/1"/>}
|
||||
/>
|
||||
<Route path="/testx" element={<h2>test2</h2>}/>
|
||||
<Route path="/markdown/:strId" element={<MarkdownContent />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
||||
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
||||
<Route path="/markdown/edit/:strId" element={<MarkdownEditor />} />
|
||||
<Route path="/popup_callback" element={<PopupCallback />} />
|
||||
<Route path="/silent_callback" element={<SilentCallback />} />
|
||||
<Route path="/template/create" element={<MarkdownTemplateEditor />} />
|
||||
<Route path="/template/edit/:strId" element={<MarkdownTemplateEditor />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/pg/*" element={<StandaloneMarkdownPage />} />
|
||||
<Route path="*" element={
|
||||
<div className="app-container">
|
||||
<MainNavigation />
|
||||
<div className="content-container">
|
||||
<SideNavigation />
|
||||
<main className="main-content">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<Navigate to = "/markdown/1"/>}
|
||||
/>
|
||||
<Route path="/testx" element={<h2>test2</h2>}/>
|
||||
<Route path="/markdown/:strId" element={<MarkdownContent />} />
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
||||
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
||||
<Route path="/markdown/edit/:strId" element={<MarkdownEditor />} />
|
||||
<Route path="/popup_callback" element={<PopupCallback />} />
|
||||
<Route path="/silent_callback" element={<SilentCallback />} />
|
||||
<Route path="/template/create" element={<MarkdownTemplateEditor />} />
|
||||
<Route path="/template/edit/:strId" element={<MarkdownTemplateEditor />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</Router>
|
||||
</Provider>
|
||||
|
||||
@@ -5,6 +5,7 @@ const LayoutEditor = ({layout, onChange}) => {
|
||||
return (
|
||||
<textarea
|
||||
className="textarea"
|
||||
style={{ height: "60vh" }}
|
||||
value={_layout}
|
||||
onChange={(e) => {
|
||||
setLayout(e.target.value);
|
||||
|
||||
@@ -65,6 +65,7 @@ const MarkdownTemplateEditor = () => {
|
||||
<div className="columns is-variable is-8">
|
||||
<div className="column">
|
||||
<h3 className="title is-5">Layout</h3>
|
||||
|
||||
<div className="box">
|
||||
<LayoutEditor
|
||||
layout={layout}
|
||||
@@ -72,15 +73,15 @@ const MarkdownTemplateEditor = () => {
|
||||
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>
|
||||
<ParametersManager
|
||||
parameters={parameters}
|
||||
onChange={(newParameters) => setParameters(newParameters)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-grouped">
|
||||
|
||||
@@ -3,18 +3,36 @@ import TypeEditor from "./TypeEditor";
|
||||
|
||||
const ParametersManager = ({ parameters, onChange }) => {
|
||||
const [_parameters, setParameters] = useState(parameters || []);
|
||||
const [expandedStates, setExpandedStates] = useState({});
|
||||
|
||||
const handleAdd = () => {
|
||||
const updated = [
|
||||
..._parameters,
|
||||
{
|
||||
name: "",
|
||||
type: { base_type: "string" }
|
||||
type: {
|
||||
base_type: "string",
|
||||
definition: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
setParameters(updated);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const handleTypeChange = (index, newType) => {
|
||||
const updated = [..._parameters];
|
||||
if (newType.base_type === "list" && !newType.extend_type) {
|
||||
newType.extend_type = {
|
||||
base_type: "string",
|
||||
definition: {}
|
||||
};
|
||||
}
|
||||
updated[index].type = newType;
|
||||
setParameters(updated);
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setParameters(parameters);
|
||||
}, [parameters]);
|
||||
@@ -33,6 +51,13 @@ const ParametersManager = ({ parameters, onChange }) => {
|
||||
onChange(updated);
|
||||
};
|
||||
|
||||
const toggleExpand = (index) => {
|
||||
setExpandedStates(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="box">
|
||||
<div className="field">
|
||||
@@ -42,44 +67,53 @@ const ParametersManager = ({ parameters, onChange }) => {
|
||||
</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 style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
||||
{_parameters.map((param, index) => (
|
||||
<div key={index} className="box" style={{ marginBottom: "0.5rem" }}>
|
||||
<div className="field is-grouped is-align-items-end">
|
||||
<div className="control">
|
||||
<label className="label">Name:</label>
|
||||
</div>
|
||||
<div className="control is-expanded">
|
||||
<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="control">
|
||||
<button
|
||||
className="button is-danger"
|
||||
onClick={() => handleDelete(index)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<div className="field">
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-1">
|
||||
<label className="label mb-0">Type:</label>
|
||||
<button
|
||||
className="button is-small"
|
||||
onClick={() => toggleExpand(index)}
|
||||
>
|
||||
{expandedStates[index] ? "-" : "+"}
|
||||
</button>
|
||||
</div>
|
||||
{expandedStates[index] && (
|
||||
<div className="control">
|
||||
<TypeEditor
|
||||
type={param.type}
|
||||
onChange={(newType) => handleTypeChange(index, newType)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,7 +69,6 @@ const TypeEditor = ({ type, onChange }) => {
|
||||
case 'template':
|
||||
return (
|
||||
<div className="field">
|
||||
<label className="label">Template</label>
|
||||
<div className="control">
|
||||
<TemplateSelector
|
||||
template={_type.definition.template}
|
||||
@@ -95,7 +94,7 @@ const TypeEditor = ({ type, onChange }) => {
|
||||
return (
|
||||
<div className="box">
|
||||
<div className="field">
|
||||
<label className="label">Type</label>
|
||||
{/*<label className="label">Type</label>*/}
|
||||
<div className="control">
|
||||
<div className="select is-fullwidth">
|
||||
<select
|
||||
|
||||
@@ -9,11 +9,13 @@ import {usePath} from "../../utils/queries/path-queries";
|
||||
import {useMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
|
||||
import {useMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
|
||||
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
||||
|
||||
const MarkdownContent = () => {
|
||||
const { strId } = useParams();
|
||||
const id = Number(strId);
|
||||
const [indexTitle, setIndexTitle] = useState(null);
|
||||
const [isSettingModalOpen, setSettingModalOpen] = useState(false);
|
||||
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);
|
||||
@@ -50,15 +52,28 @@ const MarkdownContent = () => {
|
||||
|
||||
return (
|
||||
<div className="markdown-content-container">
|
||||
<div className="field has-addons markdown-content-container-header">
|
||||
<h1 className="title control">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center markdown-content-container-header">
|
||||
<h1 className="title">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
|
||||
<PermissionGuard rolesRequired={['admin']}>
|
||||
<Link to={`/markdown/edit/${id}`} className="control button is-primary is-light">
|
||||
Edit
|
||||
</Link>
|
||||
<div className="field has-addons">
|
||||
<button
|
||||
className="control button is-info is-light"
|
||||
onClick={() => setSettingModalOpen(true)}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
<Link to={`/markdown/edit/${id}`} className="control button is-primary is-light">
|
||||
Edit
|
||||
</Link>
|
||||
</div>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
<MarkdownView content={JSON.parse(markdown.content)} template={template}/>
|
||||
<MarkdownSettingModal
|
||||
isOpen={isSettingModalOpen}
|
||||
markdown={markdown}
|
||||
onClose={() => setSettingModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -277,7 +277,7 @@ const MarkdownEditor = () => {
|
||||
</div>
|
||||
) : (
|
||||
<TemplatedEditor
|
||||
style={{height: "70vh"}}
|
||||
style={{height: "40vh"}}
|
||||
content={content}
|
||||
template={!markdown?.id ? selectedTemplate : template}
|
||||
onContentChanged={(k, v) => setContent(
|
||||
|
||||
100
src/components/Markdowns/StandaloneMarkdownPage.js
Normal file
100
src/components/Markdowns/StandaloneMarkdownPage.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import "katex/dist/katex.min.css";
|
||||
import "./MarkdownContent.css";
|
||||
import MarkdownView from "./MarkdownView";
|
||||
import { useMarkdown } from "../../utils/queries/markdown-queries";
|
||||
import { useMarkdownSetting } from "../../utils/queries/markdown-setting-queries";
|
||||
import { useMarkdownTemplate } from "../../utils/queries/markdown-template-queries";
|
||||
import { useMarkdownTemplateSetting } from "../../utils/queries/markdown-template-setting-queries";
|
||||
import { useTree } from "../../utils/queries/tree-queries";
|
||||
import { getMarkdownIdByPath } from "../../utils/pathUtils";
|
||||
|
||||
const StandaloneMarkdownPage = () => {
|
||||
const location = useLocation();
|
||||
const [indexTitle, setIndexTitle] = useState(null);
|
||||
const [markdownId, setMarkdownId] = useState(null);
|
||||
|
||||
// Extract path from /pg/project/index -> project/index
|
||||
const pathString = location.pathname.replace(/^\/pg\//, '');
|
||||
|
||||
const { data: tree, isLoading: isTreeLoading } = useTree();
|
||||
const { data: markdown, isLoading: isMarkdownLoading, error } = useMarkdown(markdownId);
|
||||
const { data: setting, isFetching: isSettingFetching } = useMarkdownSetting(markdown?.setting_id);
|
||||
const { data: templateSetting, isFetching: isTemplateSettingFetching } = useMarkdownTemplateSetting(setting?.template_setting_id);
|
||||
const { data: template, isFetching: isTemplateFetching } = useMarkdownTemplate(templateSetting?.template_id);
|
||||
|
||||
// Resolve markdown ID from path using tree
|
||||
useEffect(() => {
|
||||
if (tree && pathString) {
|
||||
const resolvedId = getMarkdownIdByPath(tree, pathString);
|
||||
setMarkdownId(resolvedId);
|
||||
}
|
||||
}, [tree, pathString]);
|
||||
|
||||
useEffect(() => {
|
||||
if (markdown && markdown.title === "index" && pathString) {
|
||||
const pathParts = pathString.split('/').filter(part => part.length > 0);
|
||||
|
||||
if (pathParts.length === 0) {
|
||||
// Root index: /pg/ or /pg
|
||||
setIndexTitle("Home");
|
||||
} else {
|
||||
// Directory index: /pg/Projects or /pg/Projects/project1
|
||||
// Use the last directory name as title
|
||||
const directoryName = pathParts[pathParts.length - 1];
|
||||
setIndexTitle(directoryName);
|
||||
}
|
||||
}
|
||||
}, [markdown, pathString]);
|
||||
|
||||
const notReady = isTreeLoading || isMarkdownLoading || isSettingFetching || isTemplateSettingFetching || isTemplateFetching;
|
||||
|
||||
if (notReady) {
|
||||
return (
|
||||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||
<div>Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||
<div>Error: {error.message || "Failed to load content"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!notReady && !markdownId) {
|
||||
return (
|
||||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||
<div>Markdown not found for path: {pathString}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (markdown?.isMessage) {
|
||||
return (
|
||||
<div style={{ padding: "2rem" }}>
|
||||
<div className="notification is-info">
|
||||
<h4 className="title is-4">{markdown.title}</h4>
|
||||
<p>{markdown.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "2rem", maxWidth: "100%", margin: "0 auto" }}>
|
||||
<div style={{ marginBottom: "2rem" }}>
|
||||
<h1 className="title">{markdown?.title === "index" ? indexTitle : markdown?.title}</h1>
|
||||
</div>
|
||||
{markdown && (
|
||||
<MarkdownView content={JSON.parse(markdown.content)} template={template} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StandaloneMarkdownPage;
|
||||
@@ -28,9 +28,10 @@ const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged
|
||||
<label className="label">{__namespace}</label>
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={value}
|
||||
onChange={(e) => onContentChanged(variable.name, e.target.value)}
|
||||
style={{maxHeight: "10vh"}}
|
||||
className="textarea"
|
||||
value={value}
|
||||
onChange={(e) => onContentChanged(variable.name, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +153,7 @@ const TemplatedEditorComponent = ({ variable, value, namespace, onContentChanged
|
||||
return <>{renderField()}</>;
|
||||
};
|
||||
|
||||
const TemplatedEditor = ({ content, template, onContentChanged }) => {
|
||||
const TemplatedEditor = ({ content, template, onContentChanged, style }) => {
|
||||
const tpl = template || {
|
||||
parameters: [{ name: "markdown", type: { base_type: "markdown", definition: {} } }],
|
||||
layout: "<markdown/>",
|
||||
@@ -160,16 +161,27 @@ const TemplatedEditor = ({ content, template, onContentChanged }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="box">
|
||||
{tpl.parameters.map((variable, idx) => (
|
||||
<TemplatedEditorComponent
|
||||
key={idx}
|
||||
variable={variable}
|
||||
value={content[variable.name]}
|
||||
namespace={tpl.title}
|
||||
onContentChanged={onContentChanged}
|
||||
/>
|
||||
))}
|
||||
<div className="box" style={{
|
||||
...style,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
padding: "1rem"
|
||||
}}>
|
||||
{tpl.parameters.map((variable, idx) => (
|
||||
<TemplatedEditorComponent
|
||||
key={idx}
|
||||
variable={variable}
|
||||
value={content[variable.name]}
|
||||
namespace={tpl.title}
|
||||
onContentChanged={onContentChanged}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
42
src/components/Modals/JsonSchemaModal.js
Normal file
42
src/components/Modals/JsonSchemaModal.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
const JsonSchemaModal = ({ isActive, onClose, schema }) => {
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(JSON.stringify(schema, null, 2));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`modal ${isActive ? 'is-active' : ''}`}>
|
||||
<div className="modal-background" onClick={onClose}></div>
|
||||
<div className="modal-card">
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">JSON Schema</p>
|
||||
<button className="delete" aria-label="close" onClick={onClose}></button>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={JSON.stringify(schema, null, 2)}
|
||||
readOnly
|
||||
style={{ height: "50vh" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer className="modal-card-foot">
|
||||
<button className="button is-primary" onClick={handleCopy}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-copy"></i>
|
||||
</span>
|
||||
<span>copy</span>
|
||||
</button>
|
||||
<button className="button" onClick={onClose}>close</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonSchemaModal;
|
||||
@@ -1,9 +1,30 @@
|
||||
import {Link} from "react-router-dom";
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import PermissionGuard from "../PermissionGuard";
|
||||
import React, {useState} from "react";
|
||||
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
||||
import {useDeleteMarkdown} from "../../utils/queries/markdown-queries";
|
||||
import {useDeleteMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||
const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
||||
const [isMarkdownSettingModalOpen, setIsMarkdownSettingModalOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const deleteMarkdown = useDeleteMarkdown();
|
||||
const deleteMarkdownSetting = useDeleteMarkdownSetting();
|
||||
|
||||
const handleDeleteMarkdown = async () => {
|
||||
if (!window.confirm(`delete markdown "${markdown.title}" ? this action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteMarkdown.mutateAsync(markdown.id);
|
||||
|
||||
if (window.location.pathname === `/markdown/${markdown.id}`) {
|
||||
navigate('/');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('failed: ' + (error.message || 'unknown error'));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<li key={markdown.id}>
|
||||
<div className="is-clickable field has-addons">
|
||||
@@ -24,6 +45,18 @@ const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<p className="control">
|
||||
<button
|
||||
className="button is-small is-danger"
|
||||
onClick={handleDeleteMarkdown}
|
||||
type="button"
|
||||
disabled={deleteMarkdown.isLoading || deleteMarkdownSetting.isLoading}
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-trash"/>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
<div
|
||||
className="control is-flex is-flex-direction-column is-align-items-center"
|
||||
style={{marginLeft: "0.5rem"}}
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { useState } from "react";
|
||||
import { useMarkdownTemplates } from "../../../utils/queries/markdown-template-queries";
|
||||
import PermissionGuard from "../../PermissionGuard";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import JsonSchemaModal from "../../Modals/JsonSchemaModal";
|
||||
|
||||
const TemplateTab = () => {
|
||||
const { data: templates, isLoading, error } = useMarkdownTemplates();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [selectedSchema, setSelectedSchema] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const filteredTemplates = templates?.filter(template =>
|
||||
@@ -16,6 +18,99 @@ const TemplateTab = () => {
|
||||
navigate(`/template/edit/${templateId}`);
|
||||
};
|
||||
|
||||
const generateJsonSchema = (template) => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
$defs: {}
|
||||
};
|
||||
|
||||
const generateTypeSchema = (param, defName) => {
|
||||
switch (param.type.base_type) {
|
||||
case "string":
|
||||
return {
|
||||
type: "string"
|
||||
};
|
||||
case "markdown":
|
||||
return {
|
||||
type: "string",
|
||||
description: "Markdown content"
|
||||
};
|
||||
case "enum":
|
||||
return {
|
||||
type: "string",
|
||||
enum: param.type.definition.enums
|
||||
};
|
||||
case "list":
|
||||
if (param.type.extend_type.base_type === "string") {
|
||||
return {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
};
|
||||
} else if (param.type.extend_type.base_type === "list" ||
|
||||
param.type.extend_type.base_type === "template") {
|
||||
const itemsDefName = `${defName}_items`;
|
||||
schema.$defs[itemsDefName] = generateTypeSchema(
|
||||
{ type: param.type.extend_type },
|
||||
itemsDefName
|
||||
);
|
||||
return {
|
||||
type: "array",
|
||||
items: {
|
||||
$ref: `#/$defs/${itemsDefName}`
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "array",
|
||||
items: generateTypeSchema(
|
||||
{ type: param.type.extend_type },
|
||||
`${defName}_items`
|
||||
)
|
||||
};
|
||||
}
|
||||
case "template":
|
||||
const nestedTemplate = templates.find(t => t.id === param.type.definition.template.id);
|
||||
if (nestedTemplate) {
|
||||
const nestedSchema = {
|
||||
type: "object",
|
||||
properties: {}
|
||||
};
|
||||
nestedTemplate.parameters.forEach(nestedParam => {
|
||||
nestedSchema.properties[nestedParam.name] = generateTypeSchema(
|
||||
nestedParam,
|
||||
`${defName}_${nestedParam.name}`
|
||||
);
|
||||
});
|
||||
return nestedSchema;
|
||||
} else {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {}
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
type: "object",
|
||||
properties: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
template.parameters.forEach(param => {
|
||||
const defName = `param_${param.name}`;
|
||||
schema.properties[param.name] = generateTypeSchema(param, defName);
|
||||
});
|
||||
|
||||
if (Object.keys(schema.$defs).length === 0) {
|
||||
delete schema.$defs;
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
if (isLoading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error loading templates</p>;
|
||||
|
||||
@@ -38,23 +133,40 @@ const TemplateTab = () => {
|
||||
Create New Template
|
||||
</a>
|
||||
</PermissionGuard>
|
||||
|
||||
{!filteredTemplates || filteredTemplates.length === 0 ? (
|
||||
<p>No templates found</p>
|
||||
) : (
|
||||
<div className="template-list">
|
||||
{filteredTemplates.map(template => (
|
||||
<button
|
||||
key={template.id}
|
||||
className="button is-light is-fullwidth template-button"
|
||||
onClick={() => handleTemplateClick(template.id)}
|
||||
style={{ marginBottom: "5px", textAlign: "left", justifyContent: "flex-start" }}
|
||||
>
|
||||
{template.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ul className="menu-list">
|
||||
{filteredTemplates?.map((template) => (
|
||||
<li key={template.id}>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center">
|
||||
<span>{template.title}</span>
|
||||
<div className="field has-addons is-justify-content-flex-end">
|
||||
<button
|
||||
className="button is-small control"
|
||||
onClick={() => handleTemplateClick(template.id)}
|
||||
type="button"
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-edit"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="button is-small control"
|
||||
onClick={() => setSelectedSchema(generateJsonSchema(template))}
|
||||
type="button"
|
||||
>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<JsonSchemaModal
|
||||
isActive={selectedSchema !== null}
|
||||
onClose={() => setSelectedSchema(null)}
|
||||
schema={selectedSchema}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
45
src/utils/pathUtils.js
Normal file
45
src/utils/pathUtils.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export const findMarkdownByPath = (tree, pathString) => {
|
||||
if (!tree || !pathString) return null;
|
||||
|
||||
const pathSegments = pathString.split('/').filter(segment => segment.length > 0);
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
const rootIndex = tree.children?.find(
|
||||
child => child.type === 'markdown' && child.title === 'index'
|
||||
);
|
||||
return rootIndex || null;
|
||||
}
|
||||
let currentNode = tree;
|
||||
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const segment = pathSegments[i];
|
||||
|
||||
const childPath = currentNode.children?.find(
|
||||
child => child.type === 'path' && child.name === segment
|
||||
);
|
||||
|
||||
if (!childPath) {
|
||||
if (i === pathSegments.length - 1) {
|
||||
const markdownNode = currentNode.children?.find(
|
||||
child => child.type === 'markdown' && child.title === segment
|
||||
);
|
||||
return markdownNode || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
currentNode = childPath;
|
||||
}
|
||||
|
||||
|
||||
const indexMarkdown = currentNode.children?.find(
|
||||
child => child.type === 'markdown' && child.title === 'index'
|
||||
);
|
||||
|
||||
return indexMarkdown || null;
|
||||
};
|
||||
|
||||
export const getMarkdownIdByPath = (tree, pathString) => {
|
||||
const markdownNode = findMarkdownByPath(tree, pathString);
|
||||
return markdownNode?.id || null;
|
||||
};
|
||||
@@ -52,6 +52,7 @@ export const useMarkdownsByPath = (pathId) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useSaveMarkdown = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const config = useConfig();
|
||||
@@ -93,6 +94,24 @@ export const useMoveMarkdown = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteMarkdown = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const config = useConfig();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (markdownId) => {
|
||||
return fetch_(`${config.BACKEND_HOST}/api/markdown/${markdownId}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
},
|
||||
onSuccess: (data, markdownId) => {
|
||||
queryClient.invalidateQueries({queryKey: ["markdown", markdownId]});
|
||||
queryClient.invalidateQueries({queryKey: ["tree"]});
|
||||
queryClient.invalidateQueries({queryKey: ["markdownsByPath"]});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSearchMarkdown = (keyword) => {
|
||||
const config = useConfig();
|
||||
return useQuery({
|
||||
|
||||
Reference in New Issue
Block a user