improve: use react-query for caching
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import React, {createContext, useEffect, useMemo, useState} from "react";
|
||||
import React, { createContext, useEffect, useMemo, useState } from "react";
|
||||
import { UserManager } from "oidc-client-ts";
|
||||
import config from "./config";
|
||||
|
||||
|
||||
export const AuthContext = createContext({
|
||||
user: null,
|
||||
login: () => {},
|
||||
@@ -13,8 +12,7 @@ export const AuthContext = createContext({
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [roles, setRoles] = useState([]);
|
||||
const userManager =
|
||||
useMemo(() => new UserManager(config.OIDC_CONFIG), []);
|
||||
const userManager = useMemo(() => new UserManager(config.OIDC_CONFIG), []);
|
||||
|
||||
useEffect(() => {
|
||||
userManager.getUser()
|
||||
@@ -30,27 +28,54 @@ const AuthProvider = ({ children }) => {
|
||||
.then((newUser) => {
|
||||
setUser(newUser);
|
||||
localStorage.setItem("accessToken", newUser.access_token);
|
||||
const clientRoles =
|
||||
newUser?.profile?.resource_access?.[config.KC_CLIENT_ID]?.roles || [];
|
||||
const clientRoles = newUser?.profile?.resource_access?.[config.KC_CLIENT_ID]?.roles || [];
|
||||
setRoles(clientRoles);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
logout();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
logout();
|
||||
});
|
||||
}, [userManager]);
|
||||
|
||||
const onUserLoaded = (loadedUser) => {
|
||||
setUser(loadedUser);
|
||||
localStorage.setItem("accessToken", loadedUser.access_token);
|
||||
const clientRoles = loadedUser?.profile?.resource_access?.[config.KC_CLIENT_ID]?.roles || [];
|
||||
setRoles(clientRoles);
|
||||
};
|
||||
|
||||
const onUserUnloaded = () => {
|
||||
setUser(null);
|
||||
setRoles([]);
|
||||
localStorage.removeItem("accessToken");
|
||||
};
|
||||
|
||||
userManager.events.addUserLoaded(onUserLoaded);
|
||||
userManager.events.addUserUnloaded(onUserUnloaded);
|
||||
|
||||
return () => {
|
||||
userManager.events.removeUserLoaded(onUserLoaded);
|
||||
userManager.events.removeUserUnloaded(onUserUnloaded);
|
||||
};
|
||||
}, [userManager]);
|
||||
|
||||
const login = () => {
|
||||
userManager
|
||||
.signinRedirect()
|
||||
.catch(
|
||||
(err) => {
|
||||
console.log(config);
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
const logout = () => userManager.signoutRedirect();
|
||||
.catch((err) => {
|
||||
console.log(config);
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
userManager.signoutRedirect();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, roles, login, logout }}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,6 @@ const config = {
|
||||
FRONTEND_HOST: null,
|
||||
KC_CLIENT_ID: null,
|
||||
OIDC_CONFIG: {},
|
||||
TEST_PASS1: null,
|
||||
TEST_PASS2: null
|
||||
}
|
||||
|
||||
export default config;
|
||||
51
src/index.js
51
src/index.js
@@ -1,13 +1,56 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import AuthProvider from "./AuthProvider";
|
||||
import AuthProvider, {AuthContext} from "./AuthProvider";
|
||||
import "bulma/css/bulma.min.css";
|
||||
import {QueryClient, QueryClientProvider} from "react-query"
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTimeout: 5 * 60 * 1000,
|
||||
onError: (error) => {
|
||||
if (error.message === "Unauthorized"){
|
||||
const {logout} = queryClient
|
||||
.getQueryCache()
|
||||
.getAll()
|
||||
.find(query => query.queryKey.includes("auth"))?.state?.context || {};
|
||||
if (logout) {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const EnhancedAuthProvider = ({children}) => {
|
||||
const auth = React.useContext(AuthContext);
|
||||
const logout = () => {
|
||||
auth.logout();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
queryClient.setQueryDefaults("auths", {
|
||||
context: {logout}
|
||||
});
|
||||
}, [logout]);
|
||||
return children;
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<EnhancedAuthProvider>
|
||||
<App />
|
||||
</EnhancedAuthProvider>
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
56
src/utils/markdown-queries.js
Normal file
56
src/utils/markdown-queries.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import {useQuery, useMutation, useQueryClient} from 'react-query';
|
||||
import {fetch_} from "./request-utils";
|
||||
import config from "../config";
|
||||
|
||||
export const useMarkdown = (id) => {
|
||||
return useQuery(
|
||||
["markdown", id],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/${id}`),
|
||||
{
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useIndexMarkdown = (path_id) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useQuery(
|
||||
["index_markdown", path_id],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/get_index/${path_id}`),{
|
||||
enabled: !!path_id,
|
||||
onSuccess: (data) => {
|
||||
if(data && data.id){
|
||||
queryClient.setQueryData(["markdown", data.id], data);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useMarkdownsByPath = (pathId) => {
|
||||
return useQuery(
|
||||
["markdownsByPath", pathId],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/markdown/by_path/${pathId}`),
|
||||
{
|
||||
enabled: !!pathId
|
||||
});
|
||||
};
|
||||
|
||||
export const useSaveMarkdown = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(({id, data}) => {
|
||||
const url = id
|
||||
? `${config.BACKEND_HOST}/api/markdown/${id}`
|
||||
: `${config.BACKEND_HOST}/api/markdown/`;
|
||||
const method = id ? "PUT" : "POST";
|
||||
return fetch_(url, {
|
||||
method,
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
},{
|
||||
onSuccess: (res, variables) => {
|
||||
queryClient.invalidateQueries(["markdownsByPath", variables.data.parent_id]);
|
||||
queryClient.invalidateQueries(["markdown", variables.data.id]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
90
src/utils/path-queries.js
Normal file
90
src/utils/path-queries.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "react-query";
|
||||
import { fetch_ } from "./request-utils";
|
||||
import config from "../config";
|
||||
|
||||
|
||||
export const usePaths = (parent_id) => {
|
||||
const queryClient = useQueryClient();
|
||||
return useQuery(
|
||||
["paths", parent_id],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/path/parent/${parent_id}`),
|
||||
{
|
||||
enabled: !!parent_id,
|
||||
onSuccess: (data) => {
|
||||
if(data) {
|
||||
for (const pth of data)
|
||||
{
|
||||
queryClient.setQueryData(["path", pth.id], pth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const usePath = (id) => {
|
||||
const queryClient = useQueryClient();
|
||||
const cachedData = queryClient.getQueryData(["path", id]);
|
||||
|
||||
|
||||
return useQuery(
|
||||
["path", id],
|
||||
() => fetch_(`${config.BACKEND_HOST}/api/path/${id}`),
|
||||
{
|
||||
enabled: !!id,
|
||||
onSuccess: (data) => {
|
||||
console.log(`path ${id} - ${cachedData}` );
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const useCreatePath = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(data) => fetch_(`${config.BACKEND_HOST}/api/path/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
{
|
||||
onSuccess: (res, variables) => {
|
||||
console.log(JSON.stringify(variables));
|
||||
queryClient.invalidateQueries(["paths", variables.parent_id]);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdatePath = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
({ id, data }) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
{
|
||||
onSuccess: (res, variables) => {
|
||||
queryClient.invalidateQueries(["paths", res.parent_id]);
|
||||
queryClient.invalidateQueries(["path", variables.data.id]);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const useDeletePath = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(id) => fetch_(`${config.BACKEND_HOST}/api/path/${id}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries("paths");
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
28
src/utils/request-utils.js
Normal file
28
src/utils/request-utils.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export async function fetch_(url, init = {}) {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(init.headers || {}),
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...init, headers });
|
||||
|
||||
if (response.status === 304) {
|
||||
return Promise.reject(new Error("Not Modified"));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const error = new Error(response.statusText);
|
||||
error.data = errorData;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import {data} from "react-router-dom";
|
||||
|
||||
const ongoingRequests = new Map();
|
||||
|
||||
|
||||
function _default_hash_function(url, method, body){
|
||||
const url_obj = new URL(url, window.location.origin);
|
||||
|
||||
const query_params = [...url_obj.searchParams.entries()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("&");
|
||||
const normalized_url = `${url_obj.origin}${url_obj.pathname}${query_params ? "?" + query_params : ""}`;
|
||||
const normalized_body = body
|
||||
? JSON.stringify(
|
||||
Object.keys(body)
|
||||
.sort()
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = body[key];
|
||||
return acc;
|
||||
}, {})
|
||||
)
|
||||
: "";
|
||||
return `${method.toUpperCase()}:${normalized_url}:${normalized_body}`;
|
||||
}
|
||||
|
||||
|
||||
export async function fetch_(url, init = {}, init_options = {}){
|
||||
const default_options = {
|
||||
use_cache: true,
|
||||
cache_key: _default_hash_function(url, init.method || "GET", init.body || null),
|
||||
cache_expires: 60,
|
||||
use_token: true,
|
||||
};
|
||||
const options = { ...default_options, ...init_options };
|
||||
|
||||
|
||||
const token = options.use_token ? localStorage.getItem("accessToken") : null;
|
||||
|
||||
const request_options = {
|
||||
...init,
|
||||
headers: {
|
||||
...(init.headers || {}),
|
||||
...(token ? {Authorization: `Bearer ${token}`} : {}),
|
||||
...(init.method && ['PUT', 'POST'].includes(init.method.toUpperCase())
|
||||
? {'Content-Type': 'application/json'}
|
||||
: {}),
|
||||
}
|
||||
};
|
||||
|
||||
if(options.use_cache && ongoingRequests.has(options.cache_key)){
|
||||
return ongoingRequests.get(options.cache_key);
|
||||
}
|
||||
const now = Date.now();
|
||||
const cached_data = localStorage.getItem(options.cache_key);
|
||||
if(options.use_cache && cached_data){
|
||||
const {data, timestamp} = JSON.parse(cached_data);
|
||||
if(now - timestamp < options.cache_expires * 1000)
|
||||
return data;
|
||||
}
|
||||
|
||||
try {
|
||||
const fetchPromise = fetch(url, request_options)
|
||||
.then((response) => {
|
||||
if(!response.ok) {
|
||||
throw new Error(`RESPONSE_ERROR: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
|
||||
if (options.use_cache)
|
||||
{
|
||||
localStorage.setItem(
|
||||
options.cache_key,
|
||||
JSON.stringify({ data, timestamp: now })
|
||||
);
|
||||
ongoingRequests.delete(options.cache_key);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
if(options.use_cache){
|
||||
ongoingRequests.set(options.cache_key, fetchPromise);
|
||||
}
|
||||
return await fetchPromise;
|
||||
}catch(error){
|
||||
if(options.use_cache){
|
||||
ongoingRequests.delete(options.cache_key);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user