add: markdown search feature

This commit is contained in:
h z
2025-01-17 09:20:20 +00:00
parent 3f4669f776
commit 76b298ac8b
4 changed files with 138 additions and 43 deletions

View File

@@ -0,0 +1,38 @@
import {Link} from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import React from "react";
const MarkdownNode = ({markdown, handleMoveMarkdown}) => {
return (
<li key={markdown.id}>
<div className="is-clickable field has-addons">
<span className="markdown-name has-text-weight-bold control">
<Link to={`/markdown/${markdown.id}`} className="is-link markdown-node">
{markdown.title}
</Link>
</span>
<PermissionGuard rolesRequired={['admin']}>
<div className="control">
<button
className="button is-small mb-1 move-forward"
style={{height: "1rem", padding: "0.25rem"}}
onClick={() => handleMoveMarkdown(markdown, "forward")}
type="button"
>
</button>
<button
className="button is-small mb-1 move-backward"
style={{height: "1rem", padding: "0.25rem"}}
onClick={() => handleMoveMarkdown(markdown, "backward")}
type="button"
>
</button>
</div>
</PermissionGuard>
</div>
</li>
);
}
export default MarkdownNode;

View File

@@ -4,6 +4,7 @@ import PermissionGuard from "../PermissionGuard";
import "./PathNode.css";
import {useDeletePath, useMovePath, usePaths, useUpdatePath} from "../../utils/path-queries";
import {useIndexMarkdown, useMarkdownsByPath, useMoveMarkdown} from "../../utils/markdown-queries";
import MarkdownNode from "./MarkdownNode";
const PathNode = ({ path, isRoot = false }) => {
const [isExpanded, setIsExpanded] = useState(isRoot);
@@ -175,34 +176,10 @@ const PathNode = ({ path, isRoot = false }) => {
))}
{sortedMarkdowns.filter(md => md.title !== "index").map((markdown) => (
<li key={markdown.id}>
<div className="is-clickable field has-addons">
<span className="markdown-name has-text-weight-bold control">
<Link to={`/markdown/${markdown.id}`} className="is-link markdown-node">
{markdown.title}
</Link>
</span>
<PermissionGuard rolesRequired={['admin']}>
<div className="control">
<button
className="button is-small mb-1 move-forward"
style={{ height: "1rem", padding: "0.25rem" }}
onClick={() => handleMoveMarkdown(markdown, "forward")}
type="button"
>
</button>
<button
className="button is-small mb-1 move-backward"
style={{ height: "1rem", padding: "0.25rem" }}
onClick={() => handleMoveMarkdown(markdown, "backward")}
type="button"
>
</button>
</div>
</PermissionGuard>
</div>
</li>
<MarkdownNode
markdown={markdown}
handleMoveMarkdown={handleMoveMarkdown}
/>
))}
</ul>
)}

View File

@@ -3,12 +3,20 @@ 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";
const SideNavigation = () => {
const {data: paths, isLoading, error } = usePaths(1);
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))
: [];
@@ -22,6 +30,14 @@ const SideNavigation = () => {
}
};
const handleSearch = () => {
setSearchMode(true);
setKeyword(searchTerm);
}
const exitSearch = () => {
setSearchMode(false);
}
const handleSave = (id, newName) => {
updatePath.mutate({ id, data: {name: newName }} , {
onError: (err) => {
@@ -29,17 +45,56 @@ const SideNavigation = () => {
}
});
};
if(isLoading){
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>;
}
return (
<aside className="menu">
<p className="menu-label">---</p>
<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>
<PermissionGuard rolesRequired={["admin", "creator"]}>
<a
href="/markdown/create"
@@ -48,18 +103,30 @@ const SideNavigation = () => {
Create New Markdown
</a>
</PermissionGuard>
<ul className="menu-list">
{isLoading && <p>Loading...</p>}
{sortedPaths.map((path) => (
<PathNode
key={path.id}
path={path}
isRoot={false}
onSave={handleSave}
onDelete={handleDelete}
/>
))}
</ul>
{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>
)}
</aside>
);
};

View File

@@ -91,3 +91,16 @@ export const useMoveMarkdown = () => {
}
);
};
export const useSearchMarkdown = (keyword) => {
const queryClient = useQueryClient();
const config = useConfig();
return useQuery(["markdownsByKeyword", keyword],
() => fetch_(
`${config.BACKEND_HOST}/api/markdown/search/${encodeURIComponent(keyword)}`,
),
{
enabled: !!keyword,
}
);
};