add: markdown deletion
This commit is contained in:
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 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"}}
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -94,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