improve: adjust layout of path node

This commit is contained in:
h z
2024-12-06 19:01:03 +00:00
parent df7ba4c490
commit a1473e51e7
4 changed files with 132 additions and 164 deletions

View File

@@ -4,8 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hangman Lab</title>
</head>
<body>
<div id="root"></div>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
rel="stylesheet"
/>
</body>
</html>

View File

@@ -35,3 +35,15 @@
font-size: 0.9rem;
margin-left: 1rem;
}
.actions {
display: flex;
justify-content: flex-end;
margin-left: auto;
}
.path-node-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}

View File

@@ -3,18 +3,28 @@ import { Link } from "react-router-dom";
import PermissionGuard from "../PermissionGuard";
import config from "../../config";
import "./PathNode.css";
import {fetch_} from "../../utils/requestUtils";
import { fetch_ } from "../../utils/requestUtils";
const PathNode = ({ path, isRoot = false }) => {
const PathNode = ({ path, isRoot = false, onDelete, onSave }) => {
const [children, setChildren] = useState([]);
const [markdowns, setMarkdowns] = useState([]);
const [isExpanded, setIsExpanded] = useState(isRoot); // Root is always expanded
const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false); // Track if editing
const [editData, setEditData] = useState({
name: path.name,
parent_id: path.parent_id,
});
const [isEditing, setIsEditing] = useState(false);
const [newName, setNewName] = useState(path.name);
const handleSave = () => {
if (onSave) {
onSave(path.id, newName);
setIsEditing(false);
} else {
console.error("onSave is not defined");
}
};
const handleEdit = () => {
setIsEditing(true);
};
const toggleExpand = () => {
if (isRoot || isExpanded) {
@@ -28,7 +38,7 @@ const PathNode = ({ path, isRoot = false }) => {
setLoading(true);
fetch_(`${config.BACKEND_HOST}/api/path/parent/${path.id}`, {}, {
use_cache: true,
use_token: false
use_token: false,
})
.then((childPaths) => {
setChildren(childPaths);
@@ -42,180 +52,85 @@ const PathNode = ({ path, isRoot = false }) => {
}
};
const handleDelete = () => {
if (window.confirm(`Are you sure you want to delete ${path.name}?`)) {
fetch_(`${config.BACKEND_HOST}/api/path/${path.id}`, {
method: "DELETE"
}, {
use_cache: false,
use_token: true
})
.then((response) => {
if (response.ok) {
alert("Path deleted successfully!");
setChildren([]);
setMarkdowns([]);
} else {
response.json().then((data) => {
alert(data.error || "Failed to delete path.");
});
}
})
.catch((error) => console.error(error));
}
};
const handleEdit = () => {
setIsEditing(true);
};
const handleEditSubmit = (e) => {
e.preventDefault();
fetch_(`${config.BACKEND_HOST}/api/path/${path.id}`, {
method: "PUT",
body: JSON.stringify(editData),
},{
use_cache: false,
use_token: true
})
.then((response) => {
if (response.ok) {
alert("Path updated successfully!");
path.name = editData.name;
path.parent_id = editData.parent_id;
setIsEditing(false);
} else {
response.json().then((data) => {
alert(data.error || "Failed to update path.");
});
}
})
.catch((error) => console.error(error));
};
const handleEditCancel = () => {
setEditData({ name: path.name, parent_id: path.parent_id });
setIsEditing(false);
};
return (
<li>
<div className="path-node-header">
<span
className={`has-text-weight-bold ${
isRoot ? "" : "is-clickable"
}`}
onClick={isRoot ? undefined : toggleExpand}
style={{ cursor: isRoot ? "default" : "pointer" }}
>
{isExpanded && !isRoot ? "▼ " : isRoot ? "" : "▶ "}
{path.name}
</span>
<div className="path-node-header is-clickable field has-addons" onClick={isRoot ? undefined : toggleExpand}>
{isEditing ? (
<div className = "control has-icons-left">
<input
className="input is-small path-edit-input"
value={newName}
onChange={(e) => setNewName(e.target.value)}
/>
</div>
) : (
<span className="path-name has-text-weight-bold control">{path.name}</span>
)}
{/* Admin controls */}
<PermissionGuard rolesRequired={["admin"]}>
<span className="path-node-actions">
<button
onClick={handleEdit}
className="button is-small is-info is-light"
type="button"
>
Edit
</button>
<button
onClick={handleDelete}
className="button is-small is-danger is-light"
type="button"
>
Delete
</button>
</span>
<div className="field has-addons actions control is-justify-content-flex-end">
{isEditing ? (
<p className="control">
<button
className="button is-small is-success"
onClick={handleSave}
type="button"
>
<span className="icon">
<i className="fas fa-check"></i>
</span>
</button>
</p>
) : (
<p className="control">
<button
className="button is-small is-info"
onClick={handleEdit}
type="button"
>
<span className="icon">
<i className="fas fa-pen"></i>
</span>
</button>
</p>
)}
<p className="control">
<button
className="button is-danger is-small"
onClick={() => onDelete(path.id)}
type="button">
<span className="icon">
<i className="fas fa-trash"></i>
</span>
</button>
</p>
</div>
</PermissionGuard>
</div>
{isExpanded && (
<ul>
{/* Render child paths */}
{loading && <p>Loading...</p>}
{children.map((child) => (
<PathNode key={child.id} path={child} />
<PathNode
key={child.id}
path={child}
onSave={onSave}
onDelete={onDelete}
/>
))}
{/* Render markdowns */}
{markdowns.map((markdown) => (
<li key={markdown.id} className="menu-list-item">
<Link
to={`/markdown/${markdown.id}`}
className="is-link"
>
<li key={markdown.id}>
<Link to={`/markdown/${markdown.id}`} className="is-link">
{markdown.title}
</Link>
</li>
))}
</ul>
)}
{/* Edit Popup */}
{isEditing && (
<div className="popup">
<div className="popup-content">
<form onSubmit={handleEditSubmit}>
<h3>Edit Path</h3>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input
className="input"
type="text"
value={editData.name}
onChange={(e) =>
setEditData({
...editData,
name: e.target.value,
})
}
required
/>
</div>
</div>
<div className="field">
<label className="label">Parent ID</label>
<div className="control">
<input
className="input"
type="number"
value={editData.parent_id}
onChange={(e) =>
setEditData({
...editData,
parent_id: Number(
e.target.value
),
})
}
required
/>
</div>
</div>
<div className="buttons">
<button
type="submit"
className="button is-primary"
>
Save
</button>
<button
type="button"
className="button is-light"
onClick={handleEditCancel}
>
Cancel
</button>
</div>
</form>
</div>
</div>
)}
</li>
);
};

View File

@@ -3,14 +3,47 @@ import PermissionGuard from "../PermissionGuard";
import PathNode from "./PathNode";
import config from "../../config";
import "./SideNavigation.css";
import {fetch_} from "../../utils/requestUtils";
import { fetch_ } from "../../utils/requestUtils";
const SideNavigation = () => {
const [paths, setPaths] = useState([]);
const [loading, setLoading] = useState(false);
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));
};
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));
};
useEffect(() => {
setLoading(true);
fetch_(`${config.BACKEND_HOST}/api/path/`, {},{ use_cache: true, use_token:false })
fetch_(`${config.BACKEND_HOST}/api/path/`, {},{
use_cache: true,
use_token: false,
})
.then((data) => {
setPaths(data);
})
@@ -36,6 +69,8 @@ const SideNavigation = () => {
key={path.id}
path={path}
isRoot={false}
onSave={handleSave} // Ensure this is passed
onDelete={handleDelete}
/>
))}
</ul>