Compare commits
2 Commits
39a69ca5b8
...
dd1ee9fd5c
| Author | SHA1 | Date | |
|---|---|---|---|
| dd1ee9fd5c | |||
| 2911f8722e |
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
19
src/utils/tree-queries.js
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user