Compare commits
2 Commits
30a46d5064
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c9310250e4 | |||
| a08164e914 |
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 SideNavigation from "./components/Navigations/SideNavigation";
|
||||||
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
import MarkdownContent from "./components/Markdowns/MarkdownContent";
|
||||||
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
import MarkdownEditor from "./components/Markdowns/MarkdownEditor";
|
||||||
|
import StandaloneMarkdownPage from "./components/Markdowns/StandaloneMarkdownPage";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Callback from "./components/KeycloakCallbacks/Callback";
|
import Callback from "./components/KeycloakCallbacks/Callback";
|
||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
@@ -18,30 +19,35 @@ const App = () => {
|
|||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
<div className="app-container">
|
<Routes>
|
||||||
<MainNavigation />
|
<Route path="/pg/*" element={<StandaloneMarkdownPage />} />
|
||||||
<div className="content-container">
|
<Route path="*" element={
|
||||||
<SideNavigation />
|
<div className="app-container">
|
||||||
<main className="main-content">
|
<MainNavigation />
|
||||||
<Routes>
|
<div className="content-container">
|
||||||
<Route
|
<SideNavigation />
|
||||||
path="/"
|
<main className="main-content">
|
||||||
element={<Navigate to = "/markdown/1"/>}
|
<Routes>
|
||||||
/>
|
<Route
|
||||||
<Route path="/testx" element={<h2>test2</h2>}/>
|
path="/"
|
||||||
<Route path="/markdown/:strId" element={<MarkdownContent />} />
|
element={<Navigate to = "/markdown/1"/>}
|
||||||
<Route path="/callback" element={<Callback />} />
|
/>
|
||||||
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
<Route path="/testx" element={<h2>test2</h2>}/>
|
||||||
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
<Route path="/markdown/:strId" element={<MarkdownContent />} />
|
||||||
<Route path="/markdown/edit/:strId" element={<MarkdownEditor />} />
|
<Route path="/callback" element={<Callback />} />
|
||||||
<Route path="/popup_callback" element={<PopupCallback />} />
|
<Route path="/test" element={<h1>TEST</h1>}></Route>
|
||||||
<Route path="/silent_callback" element={<SilentCallback />} />
|
<Route path="/markdown/create" element={<MarkdownEditor />} />
|
||||||
<Route path="/template/create" element={<MarkdownTemplateEditor />} />
|
<Route path="/markdown/edit/:strId" element={<MarkdownEditor />} />
|
||||||
<Route path="/template/edit/:strId" element={<MarkdownTemplateEditor />} />
|
<Route path="/popup_callback" element={<PopupCallback />} />
|
||||||
</Routes>
|
<Route path="/silent_callback" element={<SilentCallback />} />
|
||||||
</main>
|
<Route path="/template/create" element={<MarkdownTemplateEditor />} />
|
||||||
</div>
|
<Route path="/template/edit/:strId" element={<MarkdownTemplateEditor />} />
|
||||||
</div>
|
</Routes>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
</Router>
|
</Router>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -70,9 +70,11 @@ const ParametersManager = ({ parameters, onChange }) => {
|
|||||||
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
|
||||||
{_parameters.map((param, index) => (
|
{_parameters.map((param, index) => (
|
||||||
<div key={index} className="box" style={{ marginBottom: "0.5rem" }}>
|
<div key={index} className="box" style={{ marginBottom: "0.5rem" }}>
|
||||||
<div className="field is-grouped is-grouped-multiline">
|
<div className="field is-grouped is-align-items-end">
|
||||||
|
<div className="control">
|
||||||
|
<label className="label">Name:</label>
|
||||||
|
</div>
|
||||||
<div className="control is-expanded">
|
<div className="control is-expanded">
|
||||||
<label className="label mb-1">Name:</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input"
|
className="input"
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const TypeEditor = ({ type, onChange }) => {
|
|||||||
case 'template':
|
case 'template':
|
||||||
return (
|
return (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Template</label>
|
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<TemplateSelector
|
<TemplateSelector
|
||||||
template={_type.definition.template}
|
template={_type.definition.template}
|
||||||
@@ -95,7 +94,7 @@ const TypeEditor = ({ type, onChange }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="box">
|
<div className="box">
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label className="label">Type</label>
|
{/*<label className="label">Type</label>*/}
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<div className="select is-fullwidth">
|
<div className="select is-fullwidth">
|
||||||
<select
|
<select
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import {usePath} from "../../utils/queries/path-queries";
|
|||||||
import {useMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
import {useMarkdownSetting} from "../../utils/queries/markdown-setting-queries";
|
||||||
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
|
import {useMarkdownTemplate} from "../../utils/queries/markdown-template-queries";
|
||||||
import {useMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
|
import {useMarkdownTemplateSetting} from "../../utils/queries/markdown-template-setting-queries";
|
||||||
|
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
||||||
|
|
||||||
const MarkdownContent = () => {
|
const MarkdownContent = () => {
|
||||||
const { strId } = useParams();
|
const { strId } = useParams();
|
||||||
const id = Number(strId);
|
const id = Number(strId);
|
||||||
const [indexTitle, setIndexTitle] = useState(null);
|
const [indexTitle, setIndexTitle] = useState(null);
|
||||||
|
const [isSettingModalOpen, setSettingModalOpen] = useState(false);
|
||||||
const {data: markdown, isLoading, error} = useMarkdown(id);
|
const {data: markdown, isLoading, error} = useMarkdown(id);
|
||||||
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
|
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
|
||||||
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
|
const {data: setting, isFetching: isSettingFetching} = useMarkdownSetting(markdown?.setting_id);
|
||||||
@@ -50,15 +52,28 @@ const MarkdownContent = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="markdown-content-container">
|
<div className="markdown-content-container">
|
||||||
<div className="field has-addons markdown-content-container-header">
|
<div className="is-flex is-justify-content-space-between is-align-items-center markdown-content-container-header">
|
||||||
<h1 className="title control">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
|
<h1 className="title">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
|
||||||
<PermissionGuard rolesRequired={['admin']}>
|
<PermissionGuard rolesRequired={['admin']}>
|
||||||
<Link to={`/markdown/edit/${id}`} className="control button is-primary is-light">
|
<div className="field has-addons">
|
||||||
Edit
|
<button
|
||||||
</Link>
|
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>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
<MarkdownView content={JSON.parse(markdown.content)} template={template}/>
|
<MarkdownView content={JSON.parse(markdown.content)} template={template}/>
|
||||||
|
<MarkdownSettingModal
|
||||||
|
isOpen={isSettingModalOpen}
|
||||||
|
markdown={markdown}
|
||||||
|
onClose={() => setSettingModalOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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;
|
||||||
@@ -1,9 +1,30 @@
|
|||||||
import {Link} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import PermissionGuard from "../PermissionGuard";
|
import PermissionGuard from "../PermissionGuard";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import MarkdownSettingModal from "../Modals/MarkdownSettingModal";
|
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 MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
||||||
const [isMarkdownSettingModalOpen, setIsMarkdownSettingModalOpen] = useState(false);
|
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 (
|
return (
|
||||||
<li key={markdown.id}>
|
<li key={markdown.id}>
|
||||||
<div className="is-clickable field has-addons">
|
<div className="is-clickable field has-addons">
|
||||||
@@ -24,6 +45,18 @@ const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</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
|
<div
|
||||||
className="control is-flex is-flex-direction-column is-align-items-center"
|
className="control is-flex is-flex-direction-column is-align-items-center"
|
||||||
style={{marginLeft: "0.5rem"}}
|
style={{marginLeft: "0.5rem"}}
|
||||||
|
|||||||
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 = () => {
|
export const useSaveMarkdown = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const config = useConfig();
|
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) => {
|
export const useSearchMarkdown = (keyword) => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
Reference in New Issue
Block a user