improve: use react-query for caching

This commit is contained in:
h z
2024-12-08 17:11:14 +00:00
parent a31cec7ef0
commit 0e6fd8409a
15 changed files with 640 additions and 353 deletions

View File

@@ -2,50 +2,37 @@ import React, { useEffect, useState } from "react";
import {Link, useParams} from "react-router-dom";
import "katex/dist/katex.min.css";
import "./MarkdownContent.css";
import { fetch_ } from "../../utils/requestUtils";
import config from "../../config";
import MarkdownView from "./MarkdownView";
import PermissionGuard from "../PermissionGuard";
import {useMarkdown} from "../../utils/markdown-queries";
import {usePath} from "../../utils/path-queries";
const MarkdownContent = () => {
const { id } = useParams();
const [content, setContent] = useState(null);
const [title, setTitle] = useState(null);
const [error, setError] = useState(null);
const [indexTitle, setIndexTitle] = useState(null);
const {data: markdown, isLoading, error} = useMarkdown(id);
const {data: path, isFetching: isPathFetching} = usePath(markdown?.path_id);
useEffect(() => {
fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`, {}, {
use_cache: true,
use_token: false
})
.then((data) => {
setTitle(data.title);
setContent(data.content);
if(data.title === "index"){
fetch_(`${config.BACKEND_HOST}/api/path/${data.path_id}`, {}, {
use_cache: true,
use_token: false
}).then((path_data) => {
setIndexTitle(path_data.id === 1 ? "Home" : path_data.name);
}).catch((err) => setError(error));
}
})
.catch((error) => setError(error));
}, [id]);
if(markdown && markdown.title === "index" && path){
setIndexTitle(path.id === 1 ? "Home" : path.name);
}
}, [markdown, path]);
if (isLoading || isPathFetching) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message || "Failed to load content"}</div>;
}
if (!content) {
return <div>Loading...</div>;
}
return (
<div className="markdown-content-container">
<div className="field has-addons markdown-content-container-header">
<h1 className="title control">{title === "index" ? indexTitle : title}</h1>
<h1 className="title control">{markdown.title === "index" ? indexTitle : markdown.title}</h1>
<PermissionGuard rolesRequired={['admin']}>
<Link to={`/markdown/edit/${id}`} className="control button is-primary is-light">
Edit
@@ -53,7 +40,7 @@ const MarkdownContent = () => {
</PermissionGuard>
</div>
<MarkdownView content={content}/>
<MarkdownView content={markdown.content}/>
</div>
);
};

View File

@@ -3,10 +3,9 @@ import { AuthContext } from "../../AuthProvider";
import { useNavigate, useParams } from "react-router-dom";
import "katex/dist/katex.min.css";
import "./MarkdownEditor.css";
import config from "../../config";
import { fetch_ } from "../../utils/requestUtils";
import PathManager from "../PathManager";
import MarkdownView from "./MarkdownView";
import { useMarkdown, useSaveMarkdown } from "../../utils/markdown-queries";
const MarkdownEditor = () => {
const { roles } = useContext(AuthContext);
@@ -15,55 +14,45 @@ const MarkdownEditor = () => {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [pathId, setPathId] = useState(1);
const [loading, setLoading] = useState(false);
const {data: markdown, isLoading, error} = useMarkdown(id);
const saveMarkdown = useSaveMarkdown();
useEffect(() => {
if (id) {
setLoading(true);
fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`, {}, {
use_cache: true,
use_token: false
})
.then((data) => {
setTitle(data.title);
setContent(data.content);
setPathId(data.path_id);
})
.catch((err) => {
console.error("Failed to load markdown", err);
})
.finally(() => {
setLoading(false);
});
if(markdown){
setTitle(markdown.title);
setContent(markdown.content);
setPathId(markdown.path_id);
}
}, [id]);
}, [markdown]);
const handleSave = () => {
const url = id ? `${config.BACKEND_HOST}/api/markdown/${id}` : `${config.BACKEND_HOST}/api/markdown/`;
const method = id ? "PUT" : "POST";
fetch_(url, {
method,
body: JSON.stringify({ title, content, path_id: pathId }),
}, {
use_cache: false,
use_token: true,
}).then((data) => {
if(data.error)
throw new Error(data.error.message);
navigate("/");
}).catch((err) => {
console.error("Failed to load markdown", err);
});
saveMarkdown.mutate(
{id, data: {title, content, path_id: pathId}},
{
onSuccess: () => {
navigate("/");
},
onError: () => {
alert("Error saving markdown file");
}
});
};
const hasPermission = roles.includes("admin") || roles.includes("creator");
if (!hasPermission) {
return <div className="notification is-danger">Permission Denied</div>;
}
return loading ? (
<p>loading</p>
) : (
if(isLoading)
return <p>Loading...</p>;
if(error)
return <p>{error.message || "Failed to load markdown"}</p>;
return (
<div className="container mt-5 markdown-editor-container">
<h2 className="title is-4">{id ? "Edit Markdown" : "Create Markdown"}</h2>
<div className="columns">
@@ -114,8 +103,9 @@ const MarkdownEditor = () => {
className="button is-primary"
type="button"
onClick={handleSave}
disabled={saveMarkdown.isLoading}
>
Save
{saveMarkdown.isLoading ? "Saving..." : "Save"}
</button>
</div>
</div>

View File

@@ -1,71 +1,51 @@
import React, {useEffect, useState} from "react";
import React, {useState} from "react";
import { Link } from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import config from "../../config";
import "./PathNode.css";
import { fetch_ } from "../../utils/requestUtils";
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
import {useIndexMarkdown, useMarkdownsByPath} from "../../utils/markdown-queries";
const PathNode = ({ path, isRoot = false, onDelete, onSave }) => {
const [children, setChildren] = useState([]);
const [markdowns, setMarkdowns] = useState([]);
const PathNode = ({ path, isRoot = false }) => {
const [isExpanded, setIsExpanded] = useState(isRoot);
const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [newName, setNewName] = useState(path.name);
const [indexMarkdownId, setIndexMarkdownId] = useState(null);
useEffect(() => {
fetch_(`${config.BACKEND_HOST}/api/markdown/get_index/${path.id}`, {}, {
use_cache: true,
use_token: false
})
.then((data) => setIndexMarkdownId(data.id))
.catch((error) => setIndexMarkdownId(null) );
}, [path]);
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();
const handleSave = () => {
if (onSave) {
onSave(path.id, newName);
setIsEditing(false);
} else {
console.error("onSave is not defined");
}
const {data: indexMarkdown, isLoading: isIndexLoading, error: indexMarkdownError} = useIndexMarkdown(path.id);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
const handleSave = () => {
console.log(`handleSave ${path.id}`);
updatePath.mutate({id: path.id, data: {name: newName}}, {
onsuccess: () => setIsEditing(false),
onError: err => alert("failed to update this path"),
})
};
const handleDelete = () => {
if(window.confirm("Are you sure?")) {
deletePath.mutate(path.id, {
onError: err => alert("failed to delete this path"),
})
}
};
const handleEdit = () => {
setIsEditing(true);
};
const toggleExpand = () => {
if (isRoot || isExpanded) {
setIsExpanded(false);
return;
}
setIsExpanded(true);
if (children.length === 0 && markdowns.length === 0) {
setLoading(true);
fetch_(`${config.BACKEND_HOST}/api/path/parent/${path.id}`, {}, {
use_cache: true,
use_token: false,
})
.then((childPaths) => {
setChildren(childPaths);
return fetch_(
`${config.BACKEND_HOST}/api/markdown/by_path/${path.id}`
);
})
.then((markdownData) => {
const filteredMarkdowns = markdownData
.filter(markdown => markdown.title !== "index");
setMarkdowns(filteredMarkdowns);
})
.catch((error) => console.error(error))
.finally(() => setLoading(false));
}
if(childError || markdownError){
return <li>Error...</li>;
}
};
return (
<li>
@@ -83,8 +63,8 @@ const PathNode = ({ path, isRoot = false, onDelete, onSave }) => {
) : (
<span className="path-name has-text-weight-bold control">
{
indexMarkdownId ? (
<Link to={`/markdown/${indexMarkdownId}`} className="is-link">
indexMarkdown ? (
<Link to={`/markdown/${indexMarkdown.id}`} className="is-link">
{path.name}
</Link>
) : (
@@ -125,7 +105,7 @@ const PathNode = ({ path, isRoot = false, onDelete, onSave }) => {
<p className="control">
<button
className="button is-danger is-small"
onClick={() => onDelete(path.id)}
onClick={handleDelete}
type="button">
<span className="icon">
<i className="fas fa-trash"></i>
@@ -138,17 +118,15 @@ const PathNode = ({ path, isRoot = false, onDelete, onSave }) => {
{isExpanded && (
<ul>
{loading && <p>Loading...</p>}
{children.map((child) => (
{ isChildLoading && <p>Loading...</p>}
{ childPaths.map((child) => (
<PathNode
key={child.id}
path={child}
onSave={onSave}
onDelete={onDelete}
/>
))}
{markdowns.map((markdown) => (
{ markdowns.filter(md => md.title !== "index").map((markdown) => (
<li key={markdown.id}>
<Link to={`/markdown/${markdown.id}`} className="is-link">
{markdown.title}

View File

@@ -1,55 +1,38 @@
import React, { useEffect, useState } from "react";
import PermissionGuard from "../PermissionGuard";
import PathNode from "./PathNode";
import config from "../../config";
import "./SideNavigation.css";
import { fetch_ } from "../../utils/requestUtils";
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
const SideNavigation = () => {
const [paths, setPaths] = useState([]);
const [loading, setLoading] = useState(false);
const {data: paths, isLoading, error} = usePaths(1);
const deletePath = useDeletePath();
const updatePath = useUpdatePath();
const handleDelete = (id) => {
fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
method: "DELETE",
},{
use_cache: false,
use_token: true,
}).then(() => {
setPaths((prevPaths) => prevPaths.filter((path) => path.id !== id));
})
.catch((error) => console.error(error));
if (window.confirm("Are you sure you want to delete this path?")){
deletePath.mutate(id, {
onError: (err) => {
alert("Failed to delete path");
},
});
}
};
const handleSave = (id, newName) => {
fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
method: "PUT",
body: JSON.stringify({ name: newName, parent_id: 1 }), // Update with actual parent_id
}, {
use_cache: false,
use_token: true,
}).then(() => {
setPaths((prevPaths) =>
prevPaths.map((path) =>
path.id === id ? { ...path, name: newName } : path
)
);
})
.catch((error) => console.error("Failed to update path", error));
updatePath.mutate({ id, data: {name: newName}} , {
onError: (err) => {
alert("Failed to update path");
}
});
};
if(isLoading){
return <aside className="menu"><p>Loading...</p></aside>;
}
useEffect(() => {
setLoading(true);
fetch_(`${config.BACKEND_HOST}/api/path/`, {},{
use_cache: true,
use_token: false,
})
.then((data) => {
setPaths(data);
})
.catch((error) => console.log(error))
.finally(() => setLoading(false));
}, []);
if(error){
return <aside className="menu"><p>Error...</p></aside>;
}
return (
<aside className="menu">
@@ -63,7 +46,7 @@ const SideNavigation = () => {
</a>
</PermissionGuard>
<ul className="menu-list">
{loading && <p>Loading...</p>}
{isLoading && <p>Loading...</p>}
{paths.map((path) => (
<PathNode
key={path.id}

View File

@@ -1,70 +1,60 @@
// src/components/PathManager.js
import React, { useEffect, useState, useRef } from "react";
import { fetch_ } from "../utils/requestUtils";
import config from "../config";
import { useCreatePath, usePaths } from "../utils/path-queries";
import { useQueryClient } from "react-query";
import "./PathManager.css";
import {fetch_} from "../utils/request-utils";
import config from "../config";
const PathManager = ({ currentPathId = 1, onPathChange }) => {
const [currentPath, setCurrentPath] = useState([{ name: "Root", id: 1 }]);
//const [currentPath, setCurrentPath] = useState(buildPath(currentPathId));
const [currentId, setCurrentId] = useState(currentPathId);
const [subPaths, setSubPaths] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
const [dropdownActive, setDropdownActive] = useState(false);
const inputRef = useRef();
const buildPath = async (path_id) => {
const path = [];
let current_id = path_id;
while (current_id) {
const pathData = await fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`, {},{
use_cache: true,
use_token: false,
});
current_id = pathData.parent_id;
path.unshift(pathData);
const queryClient = useQueryClient();
const { data: subPaths, isLoading: isSubPathsLoading, error: subPathsError } = usePaths(currentPathId);
const createPath = useCreatePath();
const buildPath = async (pathId) => {
const path = [];
let current_id = pathId;
while (current_id) {
try {
const pathData = await queryClient.fetchQuery(
["path", current_id],
() => fetch_(`${config.BACKEND_HOST}/api/path/${current_id}`)
);
if (!pathData) break;
path.unshift({ name: pathData.name, id: pathData.id });
current_id = pathData.parent_id;
} catch (error) {
console.error(`Failed to fetch path with id ${current_id}:`, error);
break;
}
}
return path;
}
useEffect(() => {
fetchSubPaths(currentId);
}, [currentId]);
};
useEffect(() => {
const init = async () => {
const pth = await buildPath(currentPathId);
setCurrentPath(pth);
const path = await buildPath(currentPathId);
setCurrentPath(path);
};
init();
}, [currentPathId]);
const fetchSubPaths = (pathId) => {
setLoading(true);
fetch_(`${config.BACKEND_HOST}/api/path/parent/${pathId}`, {}, {
use_cache: false,
use_token: true
})
.then((data) => setSubPaths(data))
.catch((error) => console.error("Failed to fetch subdirectories:", error))
.finally(() => setLoading(false));
};
}, [currentPathId, queryClient]);
const handlePathClick = (pathId, pathIndex) => {
const newPath = currentPath.slice(0, pathIndex + 1);
setCurrentPath(newPath);
setCurrentId(pathId);
onPathChange(pathId);
};
const handleSubPathSelect = (subPath) => {
const updatedPath = [...currentPath, { name: subPath.name, id: subPath.id }];
setCurrentPath(updatedPath);
setCurrentId(subPath.id);
onPathChange(subPath.id);
setSearchTerm("");
setDropdownActive(false);
@@ -75,32 +65,44 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
alert("Directory name cannot be empty.");
return;
}
fetch_(`${config.BACKEND_HOST}/api/path/`, {
method: "POST",
body: JSON.stringify({ name: searchTerm.trim(), parent_id: currentId }),
}, { use_cache: false, use_token: true })
.then((newDir) => {
setSubPaths([...subPaths, newDir]);
setSearchTerm("");
alert("Directory created successfully!");
})
.catch((error) => {
console.error("Failed to create directory:", error);
alert("Failed to create directory.");
});
createPath.mutate(
{ name: searchTerm.trim(), parent_id: currentPathId },
{
onSuccess: (newDir) => {
queryClient.setQueryData(["path", newDir.id], newDir);
queryClient.invalidateQueries(["paths", currentPathId]);
setSearchTerm("");
alert("Directory created successfully.");
},
onError: (error) => {
console.error("Failed to create directory:", error);
alert("Failed to create directory.");
},
}
);
};
const handleInputFocus = () => setDropdownActive(true);
const handleInputBlur = () => {
setTimeout(() => setDropdownActive(false), 150);
};
const filteredSubPaths = subPaths.filter((path) =>
path.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredSubPaths = subPaths
? subPaths.filter((path) =>
path.name.toLowerCase().includes(searchTerm.toLowerCase())
)
: [];
const handleKeyDown = (e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddDirectory();
}
};
return (
<div className="path-manager">
<div className="path-manager-header">
<div className="current-path">
@@ -129,13 +131,14 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
onChange={(e) => setSearchTerm(e.target.value)}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
onKeyDown={handleKeyDown}
/>
</div>
<div className="control">
<button
className="button is-small is-primary"
onClick={handleAddDirectory}
disabled={loading || !searchTerm.trim()}
disabled={isSubPathsLoading || !searchTerm.trim()}
type="button"
>
Create "{searchTerm}"
@@ -163,6 +166,8 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
</div>
</div>
)}
{isSubPathsLoading && <p>Loading...</p>}
{subPathsError && <p>Error loading subdirectories.</p>}
</div>
</div>
);