diff --git a/package.json b/package.json
index ca982a3..f44909a 100755
--- a/package.json
+++ b/package.json
@@ -31,7 +31,10 @@
"reactflow": "^11.11.4",
"recharts": "^2.15.1",
"socket.io-client": "^4.8.1",
- "vite-plugin-svgr": "^4.3.0"
+ "vite-plugin-svgr": "^4.3.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@dnd-kit/core": "^6.3.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
@@ -45,4 +48,4 @@
"globals": "^15.14.0",
"vite": "^7.1.0"
}
-}
+}
\ No newline at end of file
diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx
index 3c07130..87b61e2 100644
--- a/src/Components/Layout/SidebarMenu.jsx
+++ b/src/Components/Layout/SidebarMenu.jsx
@@ -1,5 +1,4 @@
-// SidebarMenu.jsx
-import React, { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
import {
Drawer,
List,
@@ -8,13 +7,27 @@ import {
Tooltip,
Box,
} from "@mui/material";
-import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
import useSidebarResize from "../hooks/useSidebarResize";
-import ChevronLeft from '@mui/icons-material/ChevronLeft';
-import ChevronRight from '@mui/icons-material/ChevronRight';
-import LogoFull from '../../assets/images/logo.svg?react';
-import LogoSmall from '../../assets/images/system_monitor_icon.svg?react';
+import ChevronLeft from "@mui/icons-material/ChevronLeft";
+import ChevronRight from "@mui/icons-material/ChevronRight";
+import LogoFull from "../../assets/images/logo.svg?react";
+import LogoSmall from "../../assets/images/system_monitor_icon.svg?react";
+
+import {
+ DndContext,
+ closestCenter,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ DragOverlay
+} from "@dnd-kit/core";
+import {
+ SortableContext,
+ verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+
+import SortableMenuItem from "./SidebarMenuComponents/SortableMenuItem";
const SidebarMenu = ({
data,
@@ -22,25 +35,161 @@ const SidebarMenu = ({
setIsDarkMode,
onSelectItem,
forceRefreshMenu,
- user
+ user,
}) => {
const [collapsed, setCollapsed] = useState(false);
const { sidebarWidth, startResizing } = useSidebarResize(290);
- const [hovered, setHovered] = useState(false);
+ const [menuItems, setMenuItems] = useState(data.items || []);
+ const [activeItem, setActiveItem] = useState(null);
- const handleToggleCollapse = () => {
- setCollapsed(!collapsed);
+ const sensors = useSensors(useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }));
+
+ useEffect(() => {
+ const cached = localStorage.getItem("menuTree");
+ if (cached) {
+ try {
+ setMenuItems(JSON.parse(cached));
+ } catch {
+ setMenuItems(data.items || []);
+ }
+ } else {
+ setMenuItems(data.items || []);
+ }
+ }, [data]);
+
+ const handleToggleCollapse = () => setCollapsed(!collapsed);
+
+ // Функция для поиска элемента по ID во всем дереве
+ const findItemInTree = (items, id) => {
+ for (const item of items) {
+ if (item.id === id) return item;
+ if (item.items) {
+ const found = findItemInTree(item.items, id);
+ if (found) return found;
+ }
+ }
+ return null;
};
- const SidebarResizer = styled('div')(({ theme }) => ({
- width: '4px',
- cursor: 'ew-resize',
- backgroundColor: 'transparent',
- '&:hover': {
+ // Функция для удаления элемента из дерева
+ const removeItemFromTree = (items, id) => {
+ return items.filter(item => {
+ if (item.id === id) return false;
+ if (item.items) {
+ item.items = removeItemFromTree(item.items, id);
+ }
+ return true;
+ });
+ };
+
+ // Функция для добавления элемента в конкретную папку
+ const addItemToFolder = (items, folderId, newItem) => {
+ return items.map(item => {
+ if (item.id === folderId) {
+ return {
+ ...item,
+ items: [...(item.items || []), newItem]
+ };
+ }
+ if (item.items) {
+ return {
+ ...item,
+ items: addItemToFolder(item.items, folderId, newItem)
+ };
+ }
+ return item;
+ });
+ };
+
+ // Функция для поиска родителя элемента
+ const findParent = (items, childId, parent = null) => {
+ for (const item of items) {
+ if (item.id === childId) return parent;
+ if (item.items) {
+ const found = findParent(item.items, childId, item);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+
+ // Функция для добавления элемента на тот же уровень
+ const addItemAtSameLevel = (items, parentId, newItem, afterId = null) => {
+ return items.map(item => {
+ if (item.id === parentId) {
+ const children = item.items || [];
+ const insertIndex = afterId ? children.findIndex(i => i.id === afterId) + 1 : children.length;
+
+ const newChildren = [
+ ...children.slice(0, insertIndex),
+ newItem,
+ ...children.slice(insertIndex)
+ ];
+
+ return { ...item, items: newChildren };
+ }
+ if (item.items) {
+ return { ...item, items: addItemAtSameLevel(item.items, parentId, newItem, afterId) };
+ }
+ return item;
+ });
+ };
+
+ const handleDragStart = (event) => {
+ const { active } = event;
+ const item = findItemInTree(menuItems, active.id);
+ setActiveItem(item);
+ };
+
+ const handleDragEnd = (event) => {
+ const { active, over } = event;
+ setActiveItem(null);
+
+ if (!over) return;
+ if (active.id === over.id) return;
+
+ const draggedItem = findItemInTree(menuItems, active.id);
+ if (!draggedItem) return;
+
+ // Определяем тип целевого элемента (папка или элемент)
+ const overItem = findItemInTree(menuItems, over.id);
+ const isOverFolder = overItem && Array.isArray(overItem.items);
+
+ let newTree;
+
+ if (isOverFolder) {
+ // Перемещаем в папку
+ newTree = removeItemFromTree([...menuItems], active.id);
+ newTree = addItemToFolder(newTree, over.id, draggedItem);
+ } else {
+ // Перемещаем на тот же уровень
+ const overParent = findParent(menuItems, over.id);
+ if (!overParent) return;
+
+ // Удаляем из старого места
+ newTree = removeItemFromTree([...menuItems], active.id);
+
+ // Добавляем на новый уровень
+ newTree = addItemAtSameLevel(newTree, overParent.id, draggedItem, over.id);
+ }
+
+ setMenuItems(newTree);
+ localStorage.setItem("menuTree", JSON.stringify(newTree));
+ };
+
+ const SidebarResizer = styled("div")(({ theme }) => ({
+ width: "4px",
+ cursor: "ew-resize",
+ backgroundColor: "transparent",
+ "&:hover": {
backgroundColor: theme.palette.action.hover,
},
- height: '100%',
- position: 'absolute',
+ height: "100%",
+ position: "absolute",
top: 0,
right: 0,
zIndex: 1000,
@@ -48,12 +197,10 @@ const SidebarMenu = ({
return (
setHovered(true)}
- onMouseLeave={() => setHovered(false)}
sx={{
- position: 'relative',
+ position: "relative",
width: collapsed ? 64 : sidebarWidth,
- transition: 'width 0.3s ease',
+ transition: "width 0.3s ease",
}}
>
{/* Заголовок с логотипом */}
-
- {/* Логотип (занимает все пространство) */}
-
+
+
{collapsed ? (
-
+
) : (
-
+
)}
- {/* Кнопка сворачивания (абсолютное позиционирование) */}
{collapsed ? : }
@@ -131,18 +276,48 @@ const SidebarMenu = ({
{/* Основное содержимое меню */}
-
-
- {data && (
-
+
+ {activeItem ? (
+
+ {activeItem.title}
+
+ ) : null}
+
+
- {!collapsed && (
-
- )}
+
+ {!collapsed && }
);
diff --git a/src/Components/Layout/SidebarMenuComponents/SortableMenuItem.jsx b/src/Components/Layout/SidebarMenuComponents/SortableMenuItem.jsx
new file mode 100644
index 0000000..20ae851
--- /dev/null
+++ b/src/Components/Layout/SidebarMenuComponents/SortableMenuItem.jsx
@@ -0,0 +1,115 @@
+import { useState } from "react";
+import {
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Collapse,
+ List,
+ IconButton,
+ Box
+} from "@mui/material";
+import { Folder, FolderOpen, ExpandLess, ExpandMore, DragIndicator } from "@mui/icons-material";
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+
+const SortableMenuItem = ({ item, collapsed, onSelectItem, level = 0 }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging
+ } = useSortable({
+ id: item.id,
+ data: {
+ type: 'menu-item',
+ item
+ }
+ });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ };
+
+ const hasChildren = Array.isArray(item.items) && item.items.length > 0;
+
+ const handleClick = (e) => {
+ e.stopPropagation();
+ if (hasChildren) {
+ setIsOpen(!isOpen);
+ } else {
+ onSelectItem?.(item);
+ }
+ };
+
+ return (
+
+
+ {!collapsed && (
+
+
+
+ )}
+
+
+ {hasChildren ? (isOpen ? : ) : }
+
+
+ {!collapsed && (
+ <>
+
+ {hasChildren && (isOpen ? : )}
+ >
+ )}
+
+
+ {hasChildren && !collapsed && (
+
+
+ {item.items.map((child) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default SortableMenuItem;
\ No newline at end of file