add: markdown search feature
This commit is contained in:
38
src/components/Navigations/MarkdownNode.js
Normal file
38
src/components/Navigations/MarkdownNode.js
Normal 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;
|
||||||
@@ -4,6 +4,7 @@ import PermissionGuard from "../PermissionGuard";
|
|||||||
import "./PathNode.css";
|
import "./PathNode.css";
|
||||||
import {useDeletePath, useMovePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
import {useDeletePath, useMovePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
||||||
import {useIndexMarkdown, useMarkdownsByPath, useMoveMarkdown} from "../../utils/markdown-queries";
|
import {useIndexMarkdown, useMarkdownsByPath, useMoveMarkdown} from "../../utils/markdown-queries";
|
||||||
|
import MarkdownNode from "./MarkdownNode";
|
||||||
|
|
||||||
const PathNode = ({ path, isRoot = false }) => {
|
const PathNode = ({ path, isRoot = false }) => {
|
||||||
const [isExpanded, setIsExpanded] = useState(isRoot);
|
const [isExpanded, setIsExpanded] = useState(isRoot);
|
||||||
@@ -175,34 +176,10 @@ const PathNode = ({ path, isRoot = false }) => {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{sortedMarkdowns.filter(md => md.title !== "index").map((markdown) => (
|
{sortedMarkdowns.filter(md => md.title !== "index").map((markdown) => (
|
||||||
<li key={markdown.id}>
|
<MarkdownNode
|
||||||
<div className="is-clickable field has-addons">
|
markdown={markdown}
|
||||||
<span className="markdown-name has-text-weight-bold control">
|
handleMoveMarkdown={handleMoveMarkdown}
|
||||||
<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>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ import PathNode from "./PathNode";
|
|||||||
import "./SideNavigation.css";
|
import "./SideNavigation.css";
|
||||||
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
import {useDeletePath, usePaths, useUpdatePath} from "../../utils/path-queries";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {useSearchMarkdown} from "../../utils/markdown-queries";
|
||||||
|
import MarkdownNode from "./MarkdownNode";
|
||||||
|
|
||||||
const SideNavigation = () => {
|
const SideNavigation = () => {
|
||||||
const {data: paths, isLoading, error } = usePaths(1);
|
const {data: paths, isLoading, error } = usePaths(1);
|
||||||
const deletePath = useDeletePath();
|
const deletePath = useDeletePath();
|
||||||
const updatePath = useUpdatePath();
|
const updatePath = useUpdatePath();
|
||||||
const [searchTerm, setSearchTerm] = React.useState("");
|
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
|
const sortedPaths = paths
|
||||||
? paths.slice().sort((a, b) => a.order.localeCompare(b.order))
|
? 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) => {
|
const handleSave = (id, newName) => {
|
||||||
updatePath.mutate({ id, data: {name: newName }} , {
|
updatePath.mutate({ id, data: {name: newName }} , {
|
||||||
onError: (err) => {
|
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>;
|
return <aside className="menu"><p>Loading...</p></aside>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(error){
|
if(error){
|
||||||
return <aside className="menu"><p>Error...</p></aside>;
|
return <aside className="menu"><p>Error...</p></aside>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="menu">
|
<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"]}>
|
<PermissionGuard rolesRequired={["admin", "creator"]}>
|
||||||
<a
|
<a
|
||||||
href="/markdown/create"
|
href="/markdown/create"
|
||||||
@@ -48,18 +103,30 @@ const SideNavigation = () => {
|
|||||||
Create New Markdown
|
Create New Markdown
|
||||||
</a>
|
</a>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
<ul className="menu-list">
|
{searchMode ? (
|
||||||
{isLoading && <p>Loading...</p>}
|
<ul>
|
||||||
{sortedPaths.map((path) => (
|
{searchResults.map((markdown, i) => (
|
||||||
<PathNode
|
<MarkdownNode
|
||||||
key={path.id}
|
markdown={markdown}
|
||||||
path={path}
|
handleMoveMarkdown={() => {}}
|
||||||
isRoot={false}
|
/>
|
||||||
onSave={handleSave}
|
))}
|
||||||
onDelete={handleDelete}
|
</ul>
|
||||||
/>
|
) : (
|
||||||
))}
|
<ul className="menu-list">
|
||||||
</ul>
|
{isLoading && <p>Loading...</p>}
|
||||||
|
{sortedPaths.map((path) => (
|
||||||
|
<PathNode
|
||||||
|
key={path.id}
|
||||||
|
path={path}
|
||||||
|
isRoot={false}
|
||||||
|
onSave={handleSave}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user