Compare commits

..

2 Commits

Author SHA1 Message Date
dd1ee9fd5c add: load backup 2025-03-05 17:33:17 +00:00
2911f8722e add: tree / search 2025-03-05 01:23:09 +00:00
5 changed files with 143 additions and 93 deletions

View File

@@ -12,6 +12,46 @@ const MainNavigation = () => {
if (config===undefined) {
return <div>Loading ...</div>;
}
const handleLoadBackup = async () => {
try{
const input = document.createElement("input");
input.type = "file";
input.accept=".zip";
input.onchange = async (event) => {
const file = event.target.files[0];
if(!file)
return;
const formData = new FormData();
formData.append("file", file);
try{
const response = await fetch(
`${config.BACKEND_HOST}/api/backup/load`, {
method: "POST",
headers: {
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
body: formData
}
);
if(response.ok){
const result = await response.json();
alert("Backup loaded");
} else {
const error = await response.json();
alert(`failed to load ${error.error}`);
}
} catch (error) {
console.error(error);
alert("error when loading backup");
}
};
input.click();
} catch (error) {
console.error(error);
alert(`Unexpected error`);
}
}
const handleGetBackup = async () => {
try{
const response = await fetch(
@@ -115,6 +155,13 @@ const MainNavigation = () => {
>
Get Backup
</button>
<button
className="button is-primary dropdown-option"
onClick={handleLoadBackup}
type="button"
>
Load Backup
</button>
<button
className="button is-danger dropdown-option"
onClick={logout}

View File

@@ -2,7 +2,7 @@ import React, {useState} from "react";
import { Link } from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import "./PathNode.css";
import {useDeletePath, useMovePath, usePaths, useUpdatePath} from "../../utils/path-queries";
import {useDeletePath, useMovePath, usePath, usePaths, useUpdatePath} from "../../utils/path-queries";
import {useIndexMarkdown, useMarkdownsByPath, useMoveMarkdown} from "../../utils/markdown-queries";
import MarkdownNode from "./MarkdownNode";
@@ -11,8 +11,8 @@ const PathNode = ({ path, isRoot = false }) => {
const [isEditing, setIsEditing] = useState(false);
const [newName, setNewName] = useState(path.name);
const { data: childPaths, isLoading: isChildLoading, error: childError } = usePaths(path.id);
const { data: markdowns, isLoading: isMarkdownLoading, error: markdownError } = useMarkdownsByPath(path.id);
// const { data: childPaths, isLoading: isChildLoading, error: childError } = usePaths(path.id);
// const { data: markdowns, isLoading: isMarkdownLoading, error: markdownError } = useMarkdownsByPath(path.id);
const deletePath = useDeletePath();
const updatePath = useUpdatePath();
@@ -60,18 +60,40 @@ const PathNode = ({ path, isRoot = false }) => {
})
};
const childPaths = path.children.filter(x => x.type==="path");
const sortedPaths = childPaths
? childPaths.slice().sort((a, b) => a.order.localeCompare(b.order))
: [];
const markdowns = path.children.filter(x => x.type==="markdown");
const sortedMarkdowns = markdowns
? markdowns.filter(md => md.title !== "index").sort((a, b) => a.order.localeCompare(b.order))
: [];
if(childError || markdownError){
/* if(childError || markdownError){
return <li>Error...</li>;
}
}*/
if(isRoot)
return (
<ul className="menu-list">
{sortedPaths.map((path) => (
<PathNode
key={path.id}
path={path}
isRoot={false}
onSave={handleSave}
onDelete={handleDelete}
/>
))}
{sortedMarkdowns.filter(md => md.title !== "index").map((markdown) => (
<MarkdownNode
markdown={markdown}
handleMoveMarkdown={handleMoveMarkdown}
/>
))}
</ul>
);
return (
<li>
<div className="path-node-header field has-addons">
@@ -167,7 +189,6 @@ const PathNode = ({ path, isRoot = false }) => {
{isExpanded && (
<ul>
{isChildLoading && <p>Loading...</p>}
{sortedPaths.map((child) => (
<PathNode
key={child.id}

View File

@@ -3,23 +3,15 @@ import PathNode from "./PathNode";
import "./SideNavigation.css";
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
import React from 'react';
import {useSearchMarkdown} from "../../utils/markdown-queries";
import MarkdownNode from "./MarkdownNode";
import {useTree} from "../../utils/tree-queries";
const SideNavigation = () => {
const {data: paths, isLoading, error } = usePaths(1);
const {data: tree, isLoading, error} = useTree();
const deletePath = useDeletePath();
const updatePath = useUpdatePath();
const [searchTerm, setSearchTerm] = React.useState("");
const [keyword, setKeyword] = React.useState("");
const [searchMode, setSearchMode] = React.useState(false);
const {data: searchResults, isLoading: isSearching} = useSearchMarkdown(keyword, {
enabled: searchMode && !!searchMode,
});
const sortedPaths = paths
? paths.slice().sort((a, b) => a.order.localeCompare(b.order))
: [];
const handleDelete = (id) => {
if (window.confirm("Are you sure you want to delete this path?")){
deletePath.mutate(id, {
@@ -30,13 +22,30 @@ const SideNavigation = () => {
}
};
const handleSearch = () => {
setSearchMode(true);
setKeyword(searchTerm);
}
const exitSearch = () => {
setSearchMode(false);
}
const filterTree = (t, k) => {
if(t === undefined)
return undefined;
if (t.type === "path") {
if (t.name.includes(k)) {
return { ...t };
}
const filteredChildren = (t.children || [])
.map(c => filterTree(c, k))
.filter(Boolean);
if (filteredChildren.length > 0) {
return { ...t, children: filteredChildren };
}
} else if (t.type === "markdown") {
if (t.title.includes(k)) {
return { ...t };
}
}
return undefined;
};
const filteredTree = filterTree(tree, keyword);
const handleSave = (id, newName) => {
updatePath.mutate({ id, data: {name: newName }} , {
@@ -45,55 +54,17 @@ const SideNavigation = () => {
}
});
};
if(!searchMode && isLoading){
return <aside className="menu"><p>Loading...</p></aside>;
}
if(searchMode && isSearching){
return <aside className="menu"><p>Loading...</p></aside>;
}
if(error){
return <aside className="menu"><p>Error...</p></aside>;
}
if (isLoading) return <aside className="menu"><p>Loading...</p></aside>;
if (error) return <aside className="menu"><p>Error loading tree</p></aside>;
return (
<aside className="menu">
<div className="field has-addons mb-2">
<div className="control is-expanded">
<input
className="input is-small"
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="control">
<button
className="button is-small is-info"
onClick={handleSearch}
disabled={!searchTerm.trim()}
type="button"
>
<span className="icon">
<i className="fa fa-search"></i>
</span>
</button>
</div>
{searchMode && (
<div className="control">
<button
className="button is-small is-danger"
onClick={exitSearch}
type="button"
>
<span className="icon">
<i className={"fa fa-window-close"}></i>
</span>
</button>
</div>
)}
<div className="control is-expanded">
<input
className="input is-small"
type="text"
placeholder="Search..."
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
<PermissionGuard rolesRequired={["admin", "creator"]}>
<a
@@ -103,29 +74,17 @@ const SideNavigation = () => {
Create New Markdown
</a>
</PermissionGuard>
{searchMode ? (
<ul>
{searchResults.map((markdown, i) => (
<MarkdownNode
markdown={markdown}
handleMoveMarkdown={() => {}}
/>
))}
</ul>
) : (
<ul className="menu-list">
{isLoading && <p>Loading...</p>}
{sortedPaths.map((path) => (
<PathNode
key={path.id}
path={path}
isRoot={false}
onSave={handleSave}
onDelete={handleDelete}
/>
))}
</ul>
)}
{!filteredTree || filteredTree.length === 0 ?
<p>No Result</p> :
<PathNode
key={1}
path={filteredTree}
isRoot={true}
onSave={handleSave}
onDelete={handleDelete}
/>
}
</aside>
);

View File

@@ -55,6 +55,7 @@ export const useCreatePath = () => {
onSuccess: (res, variables) => {
console.log(JSON.stringify(variables));
queryClient.invalidateQueries(["paths", variables.parent_id]);
queryClient.invalidateQueries("tree");
},
}
);
@@ -73,6 +74,7 @@ export const useUpdatePath = () => {
onSuccess: (res, variables) => {
queryClient.invalidateQueries(["paths", res.parent_id]);
queryClient.invalidateQueries(["path", variables.data.id]);
queryClient.invalidateQueries("tree");
},
}
);
@@ -89,6 +91,7 @@ export const useDeletePath = () => {
{
onSuccess: () => {
queryClient.invalidateQueries("paths");
queryClient.invalidateQueries("tree");
},
}
);
@@ -107,6 +110,7 @@ export const useMovePath = () => {
{
onSuccess: () => {
queryClient.invalidateQueries("paths");
queryClient.invalidateQueries("tree");
}
}
);

19
src/utils/tree-queries.js Normal file
View File

@@ -0,0 +1,19 @@
import {useQuery, useMutation, useQueryClient} from "react-query";
import {fetch_} from "./request-utils";
import {useConfig} from "../ConfigProvider";
export const useTree = () => {
const queryClient = useQueryClient();
const config = useConfig();
return useQuery(
"tree",
() => fetch_(`${config.BACKEND_HOST}/api/tree/`),
{
onSuccess: data => {
if(data)
queryClient.setQueryData("tree", data);
}
}
);
}