Save Markdowns
This commit is contained in:
@@ -39,7 +39,7 @@ const MarkdownEditor = () => {
|
|||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const url = id ? `${config.BACKEND_HOST}/api/markdown/${id}` : `${config.BACKEND_HOST}/api/markdown`;
|
const url = id ? `${config.BACKEND_HOST}/api/markdown/${id}` : `${config.BACKEND_HOST}/api/markdown/`;
|
||||||
const method = id ? "PUT" : "POST";
|
const method = id ? "PUT" : "POST";
|
||||||
fetch_(url, {
|
fetch_(url, {
|
||||||
method,
|
method,
|
||||||
@@ -47,16 +47,12 @@ const MarkdownEditor = () => {
|
|||||||
}, {
|
}, {
|
||||||
use_cache: false,
|
use_cache: false,
|
||||||
use_token: true,
|
use_token: true,
|
||||||
}).then((res) => {
|
}).then((data) => {
|
||||||
if (res.ok) {
|
if(data.error)
|
||||||
navigate("/");
|
throw new Error(data.error.message);
|
||||||
} else {
|
navigate("/");
|
||||||
return res.json().then((data) => {
|
|
||||||
throw new Error(data.error || "Failed to save markdown");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error("Failed to save markdown", err);
|
console.error("Failed to load markdown", err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,39 @@
|
|||||||
.path-manager {
|
.path-manager-body {
|
||||||
border: 1px solid #ddd;
|
display: flex;
|
||||||
border-radius: 8px;
|
flex-direction: column;
|
||||||
padding: 1rem;
|
gap: 0.5rem;
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-manager-header {
|
.dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
position: relative;
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-path {
|
.dropdown .dropdown-input {
|
||||||
font-weight: bold;
|
flex: 1;
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-paths {
|
.dropdown .dropdown-menu {
|
||||||
list-style: none;
|
position: absolute;
|
||||||
padding: 0;
|
top: 100%;
|
||||||
margin: 0;
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-path-item {
|
.dropdown-item {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 1rem;
|
|
||||||
color: #4a4a4a;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-path-item:last-child {
|
.dropdown-item:hover {
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-path-item:hover {
|
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
color: #00d1b2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-manager-footer {
|
|
||||||
margin-top: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-manager-footer .input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-manager-footer .button {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
import { fetch_ } from "../utils/requestUtils";
|
import { fetch_ } from "../utils/requestUtils";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import "./PathManager.css";
|
import "./PathManager.css";
|
||||||
|
|
||||||
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
||||||
const [currentPath, setCurrentPath] = useState("");
|
const [currentPath, setCurrentPath] = useState([{ name: "Root", id: 1 }]);
|
||||||
const [currentId, setCurrentId] = useState(currentPathId);
|
const [currentId, setCurrentId] = useState(currentPathId);
|
||||||
const [subPaths, setSubPaths] = useState([]);
|
const [subPaths, setSubPaths] = useState([]);
|
||||||
const [newDirName, setNewDirName] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dropdownActive, setDropdownActive] = useState(false);
|
||||||
|
|
||||||
|
const inputRef = useRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSubPaths(currentId);
|
fetchSubPaths(currentId);
|
||||||
@@ -22,36 +25,34 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubPathClick = (subPath) => {
|
const handlePathClick = (pathId, pathIndex) => {
|
||||||
setCurrentPath(`${currentPath}/${subPath.name}`);
|
const newPath = currentPath.slice(0, pathIndex + 1);
|
||||||
setCurrentId(subPath.id);
|
setCurrentPath(newPath);
|
||||||
onPathChange(subPath.id);
|
setCurrentId(pathId);
|
||||||
|
onPathChange(pathId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleSubPathSelect = (subPath) => {
|
||||||
if (currentId === 1) return;
|
const updatedPath = [...currentPath, { name: subPath.name, id: subPath.id }];
|
||||||
const pathSegments = currentPath.split("/").filter(Boolean);
|
setCurrentPath(updatedPath);
|
||||||
pathSegments.pop();
|
setCurrentId(subPath.id);
|
||||||
setCurrentPath(pathSegments.length > 0 ? `/${pathSegments.join("/")}` : "");
|
onPathChange(subPath.id);
|
||||||
const parentId = pathSegments.length > 0
|
setSearchTerm("");
|
||||||
? subPaths.find((path) => path.name === pathSegments[pathSegments.length - 1])?.id || 0
|
setDropdownActive(false);
|
||||||
: 1;
|
|
||||||
setCurrentId(parentId);
|
|
||||||
onPathChange(parentId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddDirectory = () => {
|
const handleAddDirectory = () => {
|
||||||
if (!newDirName.trim()) {
|
if (!searchTerm.trim()) {
|
||||||
alert("Directory name cannot be empty.");
|
alert("Directory name cannot be empty.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch_(`${config.BACKEND_HOST}/api/path/`, {
|
fetch_(`${config.BACKEND_HOST}/api/path/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ name: newDirName.trim(), parent_id: currentId }),
|
body: JSON.stringify({ name: searchTerm.trim(), parent_id: currentId }),
|
||||||
}, { use_cache: false, use_token: true })
|
}, { use_cache: false, use_token: true })
|
||||||
.then((newDir) => {
|
.then((newDir) => {
|
||||||
setSubPaths([...subPaths, newDir]);
|
setSubPaths([...subPaths, newDir]);
|
||||||
setNewDirName("");
|
setSearchTerm("");
|
||||||
alert("Directory created successfully!");
|
alert("Directory created successfully!");
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -60,57 +61,81 @@ const PathManager = ({ currentPathId = 1, onPathChange }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInputFocus = () => setDropdownActive(true);
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
setTimeout(() => setDropdownActive(false), 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredSubPaths = subPaths.filter((path) =>
|
||||||
|
path.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="path-manager">
|
<div className="path-manager">
|
||||||
<div className="path-manager-header">
|
<div className="path-manager-header">
|
||||||
<button
|
<div className="current-path">
|
||||||
className="button is-small is-danger"
|
{currentPath.map((path, index) => (
|
||||||
onClick={handleBackClick}
|
<span
|
||||||
disabled={currentId === 1}
|
key={path.id}
|
||||||
type="button"
|
className="breadcrumb-item is-clickable"
|
||||||
>
|
onClick={() => handlePathClick(path.id, index)}
|
||||||
Back
|
>
|
||||||
</button>
|
{path.name}
|
||||||
<div className="current-path">Current Path: {currentPath || "/"}</div>
|
{index < currentPath.length - 1 && " / "}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="path-manager-body">
|
<div className="path-manager-body">
|
||||||
{loading ? (
|
<div className="field has-addons">
|
||||||
<p>Loading subdirectories...</p>
|
<div className="control">
|
||||||
) : (
|
<input
|
||||||
<ul className="sub-paths">
|
ref={inputRef}
|
||||||
{subPaths.map((subPath) => (
|
className="input is-small"
|
||||||
<li
|
type="text"
|
||||||
key={subPath.id}
|
placeholder="Search or create directory"
|
||||||
className="sub-path-item is-clickable"
|
value={searchTerm}
|
||||||
onClick={() => handleSubPathClick(subPath)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
>
|
onFocus={handleInputFocus}
|
||||||
{subPath.name}
|
onBlur={handleInputBlur}
|
||||||
</li>
|
/>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
<div className="control">
|
||||||
|
<button
|
||||||
|
className="button is-small is-primary"
|
||||||
|
onClick={handleAddDirectory}
|
||||||
|
disabled={loading || !searchTerm.trim()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Create "{searchTerm}"
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{dropdownActive && (
|
||||||
|
<div className="dropdown is-active">
|
||||||
|
<div className="dropdown-menu">
|
||||||
|
<div className="dropdown-content">
|
||||||
|
{filteredSubPaths.length > 0 ? (
|
||||||
|
filteredSubPaths.map((subPath) => (
|
||||||
|
<a
|
||||||
|
key={subPath.id}
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => handleSubPathSelect(subPath)}
|
||||||
|
>
|
||||||
|
{subPath.name}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="dropdown-item">No matches found</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="path-manager-footer">
|
|
||||||
<input
|
|
||||||
className="input is-small"
|
|
||||||
type="text"
|
|
||||||
placeholder="New directory name"
|
|
||||||
value={newDirName}
|
|
||||||
onChange={(e) => setNewDirName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="button is-small is-primary"
|
|
||||||
onClick={handleAddDirectory}
|
|
||||||
disabled={loading || !newDirName.trim()}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Add Directory
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PathManager;
|
export default PathManager;
|
||||||
|
|||||||
Reference in New Issue
Block a user