improve: adjust layout of path node
This commit is contained in:
@@ -4,8 +4,14 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Hangman Lab</title>
|
<title>Hangman Lab</title>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<link
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -35,3 +35,15 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin-left: 1rem;
|
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%;
|
||||||
|
}
|
||||||
@@ -5,16 +5,26 @@ import config from "../../config";
|
|||||||
import "./PathNode.css";
|
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 [children, setChildren] = useState([]);
|
||||||
const [markdowns, setMarkdowns] = useState([]);
|
const [markdowns, setMarkdowns] = useState([]);
|
||||||
const [isExpanded, setIsExpanded] = useState(isRoot); // Root is always expanded
|
const [isExpanded, setIsExpanded] = useState(isRoot); // Root is always expanded
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isEditing, setIsEditing] = useState(false); // Track if editing
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editData, setEditData] = useState({
|
const [newName, setNewName] = useState(path.name);
|
||||||
name: path.name,
|
|
||||||
parent_id: path.parent_id,
|
const handleSave = () => {
|
||||||
});
|
if (onSave) {
|
||||||
|
onSave(path.id, newName);
|
||||||
|
setIsEditing(false);
|
||||||
|
} else {
|
||||||
|
console.error("onSave is not defined");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleExpand = () => {
|
const toggleExpand = () => {
|
||||||
if (isRoot || isExpanded) {
|
if (isRoot || isExpanded) {
|
||||||
@@ -28,7 +38,7 @@ const PathNode = ({ path, isRoot = false }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
fetch_(`${config.BACKEND_HOST}/api/path/parent/${path.id}`, {}, {
|
fetch_(`${config.BACKEND_HOST}/api/path/parent/${path.id}`, {}, {
|
||||||
use_cache: true,
|
use_cache: true,
|
||||||
use_token: false
|
use_token: false,
|
||||||
})
|
})
|
||||||
.then((childPaths) => {
|
.then((childPaths) => {
|
||||||
setChildren(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 (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div className="path-node-header">
|
<div className="path-node-header is-clickable field has-addons" onClick={isRoot ? undefined : toggleExpand}>
|
||||||
<span
|
{isEditing ? (
|
||||||
className={`has-text-weight-bold ${
|
|
||||||
isRoot ? "" : "is-clickable"
|
<div className = "control has-icons-left">
|
||||||
}`}
|
<input
|
||||||
onClick={isRoot ? undefined : toggleExpand}
|
className="input is-small path-edit-input"
|
||||||
style={{ cursor: isRoot ? "default" : "pointer" }}
|
value={newName}
|
||||||
>
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
{isExpanded && !isRoot ? "▼ " : isRoot ? "" : "▶ "}
|
/>
|
||||||
{path.name}
|
</div>
|
||||||
</span>
|
|
||||||
|
) : (
|
||||||
|
<span className="path-name has-text-weight-bold control">{path.name}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Admin controls */}
|
|
||||||
<PermissionGuard rolesRequired={["admin"]}>
|
<PermissionGuard rolesRequired={["admin"]}>
|
||||||
<span className="path-node-actions">
|
<div className="field has-addons actions control is-justify-content-flex-end">
|
||||||
|
{isEditing ? (
|
||||||
|
<p className="control">
|
||||||
<button
|
<button
|
||||||
onClick={handleEdit}
|
className="button is-small is-success"
|
||||||
className="button is-small is-info is-light"
|
onClick={handleSave}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Edit
|
<span className="icon">
|
||||||
</button>
|
<i className="fas fa-check"></i>
|
||||||
<button
|
|
||||||
onClick={handleDelete}
|
|
||||||
className="button is-small is-danger is-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</span>
|
</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>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<ul>
|
<ul>
|
||||||
{/* Render child paths */}
|
|
||||||
{loading && <p>Loading...</p>}
|
{loading && <p>Loading...</p>}
|
||||||
{children.map((child) => (
|
{children.map((child) => (
|
||||||
<PathNode key={child.id} path={child} />
|
<PathNode
|
||||||
|
key={child.id}
|
||||||
|
path={child}
|
||||||
|
onSave={onSave}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Render markdowns */}
|
|
||||||
{markdowns.map((markdown) => (
|
{markdowns.map((markdown) => (
|
||||||
<li key={markdown.id} className="menu-list-item">
|
<li key={markdown.id}>
|
||||||
<Link
|
<Link to={`/markdown/${markdown.id}`} className="is-link">
|
||||||
to={`/markdown/${markdown.id}`}
|
|
||||||
className="is-link"
|
|
||||||
>
|
|
||||||
{markdown.title}
|
{markdown.title}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,46 @@ import PathNode from "./PathNode";
|
|||||||
import config from "../../config";
|
import config from "../../config";
|
||||||
import "./SideNavigation.css";
|
import "./SideNavigation.css";
|
||||||
import { fetch_ } from "../../utils/requestUtils";
|
import { fetch_ } from "../../utils/requestUtils";
|
||||||
|
|
||||||
const SideNavigation = () => {
|
const SideNavigation = () => {
|
||||||
const [paths, setPaths] = useState([]);
|
const [paths, setPaths] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
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) => {
|
.then((data) => {
|
||||||
setPaths(data);
|
setPaths(data);
|
||||||
})
|
})
|
||||||
@@ -36,6 +69,8 @@ const SideNavigation = () => {
|
|||||||
key={path.id}
|
key={path.id}
|
||||||
path={path}
|
path={path}
|
||||||
isRoot={false}
|
isRoot={false}
|
||||||
|
onSave={handleSave} // Ensure this is passed
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user