sidebar redesign
parent
933ceb2547
commit
06249fce3a
|
|
@ -6,6 +6,7 @@ import {
|
|||
IconButton,
|
||||
Tooltip,
|
||||
Box,
|
||||
alpha
|
||||
} from "@mui/material";
|
||||
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
||||
import useSidebarResize from "../hooks/useSidebarResize";
|
||||
|
|
@ -20,7 +21,8 @@ import {
|
|||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragOverlay
|
||||
DragOverlay,
|
||||
MeasuringStrategy
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
|
|
@ -38,13 +40,15 @@ const SidebarMenu = ({
|
|||
user,
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { sidebarWidth, startResizing } = useSidebarResize(290);
|
||||
const { sidebarWidth, startResizing } = useSidebarResize(320); // Увеличил минимальную ширину
|
||||
const [menuItems, setMenuItems] = useState(data.items || []);
|
||||
const [activeItem, setActiveItem] = useState(null);
|
||||
const [hoveredItem, setHoveredItem] = useState(null);
|
||||
const [dropIndicator, setDropIndicator] = useState({ show: false, position: null, targetId: null });
|
||||
|
||||
const sensors = useSensors(useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
distance: 4,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -61,9 +65,12 @@ const SidebarMenu = ({
|
|||
}
|
||||
}, [data]);
|
||||
|
||||
const handleToggleCollapse = () => setCollapsed(!collapsed);
|
||||
const handleToggleCollapse = () => {
|
||||
setCollapsed(!collapsed);
|
||||
setHoveredItem(null);
|
||||
};
|
||||
|
||||
// Функция для поиска элемента по ID во всем дереве
|
||||
// Функции для работы с деревом (остаются без изменений)
|
||||
const findItemInTree = (items, id) => {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item;
|
||||
|
|
@ -75,7 +82,6 @@ const SidebarMenu = ({
|
|||
return null;
|
||||
};
|
||||
|
||||
// Функция для удаления элемента из дерева
|
||||
const removeItemFromTree = (items, id) => {
|
||||
return items.filter(item => {
|
||||
if (item.id === id) return false;
|
||||
|
|
@ -86,7 +92,6 @@ const SidebarMenu = ({
|
|||
});
|
||||
};
|
||||
|
||||
// Функция для добавления элемента в конкретную папку
|
||||
const addItemToFolder = (items, folderId, newItem) => {
|
||||
return items.map(item => {
|
||||
if (item.id === folderId) {
|
||||
|
|
@ -105,7 +110,6 @@ const SidebarMenu = ({
|
|||
});
|
||||
};
|
||||
|
||||
// Функция для поиска родителя элемента
|
||||
const findParent = (items, childId, parent = null) => {
|
||||
for (const item of items) {
|
||||
if (item.id === childId) return parent;
|
||||
|
|
@ -117,7 +121,6 @@ const SidebarMenu = ({
|
|||
return null;
|
||||
};
|
||||
|
||||
// Функция для добавления элемента на тот же уровень
|
||||
const addItemAtSameLevel = (items, parentId, newItem, afterId = null) => {
|
||||
return items.map(item => {
|
||||
if (item.id === parentId) {
|
||||
|
|
@ -143,11 +146,13 @@ const SidebarMenu = ({
|
|||
const { active } = event;
|
||||
const item = findItemInTree(menuItems, active.id);
|
||||
setActiveItem(item);
|
||||
setDropIndicator({ show: false, position: null, targetId: null });
|
||||
};
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
setActiveItem(null);
|
||||
setHoveredItem(null);
|
||||
setDropIndicator({ show: false, position: null, targetId: null });
|
||||
|
||||
if (!over) return;
|
||||
if (active.id === over.id) return;
|
||||
|
|
@ -155,69 +160,190 @@ const SidebarMenu = ({
|
|||
const draggedItem = findItemInTree(menuItems, active.id);
|
||||
if (!draggedItem) return;
|
||||
|
||||
// Определяем тип целевого элемента (папка или элемент)
|
||||
const overItem = findItemInTree(menuItems, over.id);
|
||||
const isOverFolder = overItem && Array.isArray(overItem.items);
|
||||
|
||||
// Проверяем, не пытаемся ли переместить элемент в его же потомка
|
||||
if (isDescendant(draggedItem, overItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newTree;
|
||||
|
||||
if (isOverFolder) {
|
||||
// Перемещаем в папку
|
||||
if (dropIndicator.position === 'inside' && overItem && Array.isArray(overItem.items)) {
|
||||
// Вставка внутрь папки
|
||||
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);
|
||||
// Определяем позицию для вставки
|
||||
let insertAfterId = null;
|
||||
if (dropIndicator.position === 'below') {
|
||||
insertAfterId = over.id;
|
||||
} else if (dropIndicator.position === 'above') {
|
||||
const siblings = overParent.items || [];
|
||||
const overIndex = siblings.findIndex(item => item.id === over.id);
|
||||
if (overIndex > 0) {
|
||||
insertAfterId = siblings[overIndex - 1].id;
|
||||
}
|
||||
}
|
||||
|
||||
newTree = addItemAtSameLevel(newTree, overParent.id, draggedItem, insertAfterId);
|
||||
}
|
||||
|
||||
setMenuItems(newTree);
|
||||
localStorage.setItem("menuTree", JSON.stringify(newTree));
|
||||
};
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over) {
|
||||
setDropIndicator({ show: false, position: null, targetId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const overItem = findItemInTree(menuItems, over.id);
|
||||
const activeItem = findItemInTree(menuItems, active.id);
|
||||
|
||||
if (!overItem || !activeItem || active.id === over.id) {
|
||||
setDropIndicator({ show: false, position: null, targetId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, можно ли перемещать элемент
|
||||
if (isDescendant(activeItem, overItem)) {
|
||||
setDropIndicator({ show: false, position: null, targetId: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const overRect = over.rect.current;
|
||||
if (!overRect) return;
|
||||
|
||||
const relativeY = event.delta.y;
|
||||
const isOverFolder = overItem && Array.isArray(overItem.items);
|
||||
const isTopHalf = relativeY < overRect.height * 0.4;
|
||||
const isBottomHalf = relativeY > overRect.height * 0.6;
|
||||
|
||||
if (isOverFolder && !isTopHalf && !isBottomHalf) {
|
||||
// Показываем индикатор для вставки в папку
|
||||
setDropIndicator({
|
||||
show: true,
|
||||
position: 'inside',
|
||||
targetId: over.id
|
||||
});
|
||||
setHoveredItem(over.id);
|
||||
} else if (isTopHalf) {
|
||||
// Показываем индикатор для вставки выше
|
||||
setDropIndicator({
|
||||
show: true,
|
||||
position: 'above',
|
||||
targetId: over.id
|
||||
});
|
||||
setHoveredItem(null);
|
||||
} else if (isBottomHalf) {
|
||||
// Показываем индикатор для вставки ниже
|
||||
setDropIndicator({
|
||||
show: true,
|
||||
position: 'below',
|
||||
targetId: over.id
|
||||
});
|
||||
setHoveredItem(null);
|
||||
} else {
|
||||
setDropIndicator({ show: false, position: null, targetId: null });
|
||||
setHoveredItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
const isDescendant = (parent, child) => {
|
||||
if (!parent || !child || !parent.items) return false;
|
||||
|
||||
const checkChildren = (items, targetId) => {
|
||||
for (const item of items) {
|
||||
if (item.id === targetId) return true;
|
||||
if (item.items && checkChildren(item.items, targetId)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return checkChildren(parent.items, child.id);
|
||||
};
|
||||
|
||||
const SidebarResizer = styled("div")(({ theme }) => ({
|
||||
width: "4px",
|
||||
cursor: "ew-resize",
|
||||
backgroundColor: "transparent",
|
||||
width: "3px",
|
||||
cursor: "col-resize",
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.3),
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
transition: "background-color 0.2s ease",
|
||||
}));
|
||||
|
||||
const DropIndicator = ({ position, targetId }) => {
|
||||
if (!targetId) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '2px',
|
||||
backgroundColor: 'primary.main',
|
||||
zIndex: 1001,
|
||||
...(position === 'above' && { top: 0 }),
|
||||
...(position === 'below' && { bottom: 0 }),
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '-3px',
|
||||
left: '10%',
|
||||
width: '80%',
|
||||
height: '8px',
|
||||
backgroundColor: 'primary.main',
|
||||
borderRadius: '2px',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: collapsed ? 64 : sidebarWidth,
|
||||
transition: "width 0.3s ease",
|
||||
width: collapsed ? 72 : sidebarWidth,
|
||||
transition: "width 0.2s ease",
|
||||
height: "100vh",
|
||||
}}
|
||||
>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
width: collapsed ? 64 : sidebarWidth,
|
||||
width: collapsed ? 72 : sidebarWidth,
|
||||
flexShrink: 0,
|
||||
"& .MuiDrawer-paper": {
|
||||
width: collapsed ? 64 : sidebarWidth,
|
||||
width: collapsed ? 72 : sidebarWidth,
|
||||
boxSizing: "border-box",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "custom.sidebar",
|
||||
color: "custom.sidebarText",
|
||||
transition: "width 0.3s ease",
|
||||
backgroundColor: "background.paper",
|
||||
color: "text.primary",
|
||||
transition: "width 0.2s ease, background-color 0.2s ease",
|
||||
overflowX: "hidden",
|
||||
borderRight: "none",
|
||||
borderRight: "1px solid",
|
||||
borderColor: "divider",
|
||||
boxShadow: "0 2px 12px rgba(0, 0, 0, 0.08)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
@ -227,12 +353,14 @@ const SidebarMenu = ({
|
|||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
p: 1,
|
||||
p: 2,
|
||||
borderBottom: "1px solid",
|
||||
borderColor: "divider",
|
||||
backgroundColor: "custom.sidebar",
|
||||
backgroundColor: "background.paper",
|
||||
height: 80,
|
||||
position: "relative",
|
||||
transition: "all 0.2s ease",
|
||||
minHeight: 80,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
|
@ -242,32 +370,52 @@ const SidebarMenu = ({
|
|||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "all 0.2s ease",
|
||||
"& svg": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
padding: collapsed ? "8px" : "12px",
|
||||
width: "auto",
|
||||
height: "40px", // Фиксированная высота для лого
|
||||
objectFit: "contain",
|
||||
transition: "all 0.2s ease",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{collapsed ? (
|
||||
<LogoSmall style={{ color: "inherit" }} />
|
||||
<LogoSmall style={{
|
||||
color: "inherit",
|
||||
filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.1))",
|
||||
width: "32px",
|
||||
height: "32px"
|
||||
}} />
|
||||
) : (
|
||||
<LogoFull style={{ color: "inherit" }} />
|
||||
<LogoFull style={{
|
||||
color: "inherit",
|
||||
filter: "drop-shadow(0 2px 4px rgba(0,0,0,0.1))",
|
||||
maxWidth: "180px",
|
||||
height: "40px"
|
||||
}} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Tooltip title={collapsed ? "Развернуть меню" : "Свернуть меню"}>
|
||||
<Tooltip
|
||||
title={collapsed ? "Развернуть меню" : "Свернуть меню"}
|
||||
placement="right"
|
||||
>
|
||||
<IconButton
|
||||
onClick={handleToggleCollapse}
|
||||
size="small"
|
||||
sx={{
|
||||
color: "custom.sidebarText",
|
||||
"&:hover": { backgroundColor: "custom.sidebarHover" },
|
||||
color: "text.secondary",
|
||||
"&:hover": {
|
||||
backgroundColor: "action.hover",
|
||||
color: "text.primary"
|
||||
},
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
right: 12,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
transition: "all 0.2s ease",
|
||||
width: 32,
|
||||
height: 32,
|
||||
}}
|
||||
>
|
||||
{collapsed ? <ChevronRight /> : <ChevronLeft />}
|
||||
|
|
@ -277,23 +425,67 @@ const SidebarMenu = ({
|
|||
|
||||
{/* Основное содержимое меню */}
|
||||
<Box
|
||||
sx={{ flexGrow: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
measuring={{
|
||||
droppable: {
|
||||
strategy: MeasuringStrategy.Always
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SortableContext items={menuItems.map((i) => i.id)} strategy={verticalListSortingStrategy}>
|
||||
<List sx={{ overflowY: "auto", flex: "1 1 auto" }}>
|
||||
<List
|
||||
sx={{
|
||||
overflowY: "auto",
|
||||
flex: "1 1 auto",
|
||||
py: 1,
|
||||
px: 1,
|
||||
position: 'relative',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '6px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'text.disabled',
|
||||
borderRadius: '3px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: 'text.secondary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{menuItems.map((item) => (
|
||||
<SortableMenuItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
<Box key={item.id} position="relative">
|
||||
{dropIndicator.show && dropIndicator.targetId === item.id &&
|
||||
dropIndicator.position !== 'inside' && (
|
||||
<DropIndicator
|
||||
position={dropIndicator.position}
|
||||
targetId={dropIndicator.targetId}
|
||||
/>
|
||||
)}
|
||||
<SortableMenuItem
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
onSelectItem={onSelectItem}
|
||||
isHovered={hoveredItem === item.id}
|
||||
showDropIndicator={dropIndicator.show && dropIndicator.targetId === item.id && dropIndicator.position === 'inside'}
|
||||
sidebarWidth={sidebarWidth}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</SortableContext>
|
||||
|
|
@ -304,13 +496,18 @@ const SidebarMenu = ({
|
|||
sx={{
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'white',
|
||||
padding: 1,
|
||||
borderRadius: 1,
|
||||
boxShadow: 3,
|
||||
maxWidth: 200,
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)',
|
||||
maxWidth: 250,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
transform: 'rotate(5deg)',
|
||||
}}
|
||||
>
|
||||
{activeItem.title}
|
||||
|
|
@ -328,7 +525,11 @@ const SidebarMenu = ({
|
|||
/>
|
||||
</Box>
|
||||
|
||||
{!collapsed && <SidebarResizer onMouseDown={startResizing} />}
|
||||
{!collapsed && (
|
||||
<Tooltip title="Изменить ширину" placement="top">
|
||||
<SidebarResizer onMouseDown={startResizing} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,121 +1,121 @@
|
|||
// MenuItem.jsx
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Collapse,
|
||||
List,
|
||||
styled,
|
||||
Menu,
|
||||
MenuItem as MuiMenuItem
|
||||
} from "@mui/material";
|
||||
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||
import StatusIndicator from "./StatusIndicator";
|
||||
// // MenuItem.jsx
|
||||
// import React, { useState } from "react";
|
||||
// import {
|
||||
// ListItem,
|
||||
// ListItemIcon,
|
||||
// ListItemText,
|
||||
// Collapse,
|
||||
// List,
|
||||
// styled,
|
||||
// Menu,
|
||||
// MenuItem as MuiMenuItem
|
||||
// } from "@mui/material";
|
||||
// import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||
// import StatusIndicator from "./StatusIndicator";
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
||||
cursor: "pointer",
|
||||
paddingLeft: theme.spacing(2 + level * 2),
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: theme.palette.custom.sidebarHover,
|
||||
},
|
||||
}));
|
||||
// const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
||||
// cursor: "pointer",
|
||||
// paddingLeft: theme.spacing(2 + level * 2),
|
||||
// position: 'relative',
|
||||
// '&:hover': {
|
||||
// backgroundColor: theme.palette.action.hover,
|
||||
// },
|
||||
// '&.Mui-selected': {
|
||||
// backgroundColor: theme.palette.custom.sidebarHover,
|
||||
// },
|
||||
// }));
|
||||
|
||||
const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [contextMenu, setContextMenu] = useState(null);
|
||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||
// const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
|
||||
// const [isOpen, setIsOpen] = useState(false);
|
||||
// const [contextMenu, setContextMenu] = useState(null);
|
||||
// const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||
|
||||
const handleContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu(
|
||||
contextMenu === null
|
||||
? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 }
|
||||
: null
|
||||
);
|
||||
};
|
||||
// const handleContextMenu = (e) => {
|
||||
// e.preventDefault();
|
||||
// setContextMenu(
|
||||
// contextMenu === null
|
||||
// ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 }
|
||||
// : null
|
||||
// );
|
||||
// };
|
||||
|
||||
const handleCloseContextMenu = () => {
|
||||
setContextMenu(null);
|
||||
};
|
||||
// const handleCloseContextMenu = () => {
|
||||
// setContextMenu(null);
|
||||
// };
|
||||
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
// const handleToggle = (e) => {
|
||||
// e.stopPropagation();
|
||||
// setIsOpen(!isOpen);
|
||||
// };
|
||||
|
||||
const handleClick = () => {
|
||||
if (onSelectItem) {
|
||||
onSelectItem(item);
|
||||
}
|
||||
};
|
||||
// const handleClick = () => {
|
||||
// if (onSelectItem) {
|
||||
// onSelectItem(item);
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledListItem
|
||||
component="div"
|
||||
onClick={hasChildren ? handleToggle : handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
level={level}
|
||||
sx={{
|
||||
pl: collapsed ? 2 : 2 + level * 2,
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
}}
|
||||
>
|
||||
{!collapsed && <StatusIndicator status={item.status} />}
|
||||
// return (
|
||||
// <>
|
||||
// <StyledListItem
|
||||
// component="div"
|
||||
// onClick={hasChildren ? handleToggle : handleClick}
|
||||
// onContextMenu={handleContextMenu}
|
||||
// level={level}
|
||||
// sx={{
|
||||
// pl: collapsed ? 2 : 2 + level * 2,
|
||||
// justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
// }}
|
||||
// >
|
||||
// {!collapsed && <StatusIndicator status={item.status} />}
|
||||
|
||||
<ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
|
||||
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||
</ListItemIcon>
|
||||
// <ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
|
||||
// {hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||
// </ListItemIcon>
|
||||
|
||||
{!collapsed && (
|
||||
<>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText'
|
||||
}}
|
||||
/>
|
||||
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||
</>
|
||||
)}
|
||||
</StyledListItem>
|
||||
// {!collapsed && (
|
||||
// <>
|
||||
// <ListItemText
|
||||
// primary={item.title}
|
||||
// primaryTypographyProps={{
|
||||
// color: 'custom.sidebarText'
|
||||
// }}
|
||||
// />
|
||||
// {hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||
// </>
|
||||
// )}
|
||||
// </StyledListItem>
|
||||
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={handleCloseContextMenu}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
contextMenu !== null
|
||||
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
// <Menu
|
||||
// open={contextMenu !== null}
|
||||
// onClose={handleCloseContextMenu}
|
||||
// anchorReference="anchorPosition"
|
||||
// anchorPosition={
|
||||
// contextMenu !== null
|
||||
// ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
// : undefined
|
||||
// }
|
||||
// >
|
||||
|
||||
</Menu>
|
||||
// </Menu>
|
||||
|
||||
{hasChildren && !collapsed && (
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{item.items.map((child, index) => (
|
||||
<MenuItem
|
||||
key={child.id ?? index}
|
||||
item={child}
|
||||
onSelectItem={onSelectItem}
|
||||
onEdit={onEdit}
|
||||
level={level + 1}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
// {hasChildren && !collapsed && (
|
||||
// <Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
// <List component="div" disablePadding>
|
||||
// {item.items.map((child, index) => (
|
||||
// <MenuItem
|
||||
// key={child.id ?? index}
|
||||
// item={child}
|
||||
// onSelectItem={onSelectItem}
|
||||
// onEdit={onEdit}
|
||||
// level={level + 1}
|
||||
// collapsed={collapsed}
|
||||
// />
|
||||
// ))}
|
||||
// </List>
|
||||
// </Collapse>
|
||||
// )}
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default MenuItem;
|
||||
// export default MenuItem;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
import React, { useState } from "react";
|
||||
import { Brightness4, Brightness7 } from "@mui/icons-material";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { Brightness4, Brightness7, Settings, Help } from "@mui/icons-material";
|
||||
import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Box,
|
||||
Button,
|
||||
alpha
|
||||
} from "@mui/material";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
styled,
|
||||
Switch,
|
||||
Box,
|
||||
Button
|
||||
} from "@mui/material";
|
||||
import SettingsModal from "../SettingsModal";
|
||||
import { RoleBasedRender } from "../../UI/RoleBasedRender";
|
||||
|
||||
const FooterList = styled(List)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.custom.sidebar,
|
||||
backgroundColor: 'background.paper',
|
||||
padding: theme.spacing(1, 0),
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
marginTop: 'auto'
|
||||
|
|
@ -22,12 +26,15 @@ const FooterList = styled(List)(({ theme }) => ({
|
|||
|
||||
const FooterListItem = styled(ListItem)(({ theme }) => ({
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.custom.sidebarHover,
|
||||
backgroundColor: alpha(theme.palette.action.hover, 0.4),
|
||||
},
|
||||
padding: theme.spacing(1, 2),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
borderRadius: '8px',
|
||||
margin: '0 8px 4px',
|
||||
transition: 'all 0.2s ease',
|
||||
}));
|
||||
|
||||
const SidebarFooter = ({
|
||||
|
|
@ -46,72 +53,93 @@ const SidebarFooter = ({
|
|||
const handleSettingsClose = () => {
|
||||
setSettingsOpen(false);
|
||||
};
|
||||
/*console.log('SidebarFooter user with role:', {
|
||||
...user,
|
||||
hasRole: 'role' in user,
|
||||
roleValue: user?.role
|
||||
}); */
|
||||
|
||||
return (
|
||||
<>
|
||||
<FooterList>
|
||||
{!collapsed && (
|
||||
<FooterListItem button>
|
||||
<ListItemText
|
||||
primary="Помощь"
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText',
|
||||
variant: 'body2'
|
||||
}}
|
||||
/>
|
||||
</FooterListItem>
|
||||
)}
|
||||
<FooterListItem>
|
||||
{/* кнопка настроек */}
|
||||
<RoleBasedRender user={user} allowedRoles={['admin']}>
|
||||
{!collapsed && (
|
||||
{!collapsed ? (
|
||||
<>
|
||||
<FooterListItem>
|
||||
<Button
|
||||
onClick={handleSettingsOpen}
|
||||
startIcon={<Settings />}
|
||||
sx={{
|
||||
color: 'custom.sidebarText',
|
||||
color: 'text.secondary',
|
||||
textTransform: 'none',
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
marginRight: 'auto'
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
color: 'text.primary',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary="Настройки"
|
||||
primaryTypographyProps={{
|
||||
color: 'custom.sidebarText',
|
||||
variant: 'body2'
|
||||
}}
|
||||
/>
|
||||
Настройки
|
||||
</Button>
|
||||
)}
|
||||
</RoleBasedRender>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tooltip title="Переключить тему">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
sx={{ color: 'custom.sidebarText' }}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Tooltip title="Переключить тему">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
'&:hover': {
|
||||
color: 'text.primary',
|
||||
backgroundColor: alpha('#000000', 0.1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isDarkMode ? <Brightness4 /> : <Brightness7 />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onChange={() => setIsDarkMode(!isDarkMode)}
|
||||
size="small"
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
</FooterListItem>
|
||||
|
||||
<FooterListItem button>
|
||||
<Button
|
||||
startIcon={<Help />}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
textTransform: 'none',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
color: 'text.primary',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isDarkMode ? <Brightness4 /> : <Brightness7 />}
|
||||
Помощь
|
||||
</Button>
|
||||
</FooterListItem>
|
||||
</>
|
||||
) : (
|
||||
<FooterListItem sx={{ justifyContent: 'center' }}>
|
||||
<Tooltip title="Настройки" placement="right">
|
||||
<IconButton
|
||||
onClick={handleSettingsOpen}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
'&:hover': {
|
||||
color: 'text.primary',
|
||||
backgroundColor: alpha('#000000', 0.1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!collapsed && (
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onChange={() => setIsDarkMode(!isDarkMode)}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</FooterListItem>
|
||||
</FooterListItem>
|
||||
)}
|
||||
</FooterList>
|
||||
|
||||
{/* Используем RoleBasedRender для модального окна */}
|
||||
<RoleBasedRender user={user} allowedRoles={['admin']}>
|
||||
<SettingsModal
|
||||
open={settingsOpen}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,26 @@ import {
|
|||
Collapse,
|
||||
List,
|
||||
IconButton,
|
||||
Box
|
||||
Box,
|
||||
alpha,
|
||||
Typography,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import { Folder, FolderOpen, ExpandLess, ExpandMore, DragIndicator } from "@mui/icons-material";
|
||||
import { ChevronRight, DragIndicator, Folder, FolderOpen } from "@mui/icons-material";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
const SortableMenuItem = ({ item, collapsed, onSelectItem, level = 0 }) => {
|
||||
const SortableMenuItem = ({
|
||||
item,
|
||||
collapsed,
|
||||
onSelectItem,
|
||||
level = 0,
|
||||
isHovered = false,
|
||||
showDropIndicator = false,
|
||||
sidebarWidth = 300
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLocalHovered, setIsLocalHovered] = useState(false);
|
||||
|
||||
const {
|
||||
attributes,
|
||||
|
|
@ -21,22 +33,34 @@ const SortableMenuItem = ({ item, collapsed, onSelectItem, level = 0 }) => {
|
|||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging
|
||||
isDragging,
|
||||
isOver
|
||||
} = useSortable({
|
||||
id: item.id,
|
||||
data: {
|
||||
type: 'menu-item',
|
||||
item
|
||||
item,
|
||||
level
|
||||
}
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
transition: transition || 'all 0.2s ease',
|
||||
opacity: isDragging ? 0.6 : 1,
|
||||
zIndex: isDragging ? 1000 : 1,
|
||||
};
|
||||
|
||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||
const isFolder = hasChildren;
|
||||
const isHighlighted = isHovered || isOver;
|
||||
|
||||
// Рассчитываем максимальную ширину текста в зависимости от уровня вложенности
|
||||
const calculateMaxTextWidth = () => {
|
||||
const baseWidth = sidebarWidth - 40; // Отступы и иконки
|
||||
const levelOffset = level * 24; // Отступ для каждого уровня
|
||||
return baseWidth - levelOffset - 60; // Оставляем место для иконок и отступов
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -47,63 +71,146 @@ const SortableMenuItem = ({ item, collapsed, onSelectItem, level = 0 }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsLocalHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsLocalHovered(false);
|
||||
};
|
||||
|
||||
const getBackgroundColor = (theme) => {
|
||||
if (isDragging) return alpha(theme.palette.primary.main, 0.1);
|
||||
if (isHighlighted) return alpha(theme.palette.primary.main, 0.08);
|
||||
if (isLocalHovered) return alpha(theme.palette.action.hover, 0.4);
|
||||
return 'transparent';
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={setNodeRef} style={style}>
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
'&::before': isHighlighted ? {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
width: 3,
|
||||
backgroundColor: 'primary.main',
|
||||
borderRadius: '0 2px 2px 0',
|
||||
} : {},
|
||||
...(showDropIndicator && {
|
||||
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.1),
|
||||
border: (theme) => `2px dashed ${theme.palette.primary.main}`,
|
||||
borderRadius: '8px',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ListItem
|
||||
button
|
||||
sx={{
|
||||
pl: collapsed ? 2 : 2 + level * 2,
|
||||
pl: collapsed ? 1 : Math.max(0.1, 0.1 + level * 0.1),
|
||||
pr: 0.5,
|
||||
py: 0.25,
|
||||
minHeight: 32,
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
backgroundColor: isDragging ? 'action.hover' : 'transparent',
|
||||
position: 'relative',
|
||||
backgroundColor: (theme) => getBackgroundColor(theme),
|
||||
borderRadius: '6px',
|
||||
margin: '1px 4px',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
||||
{!collapsed && (
|
||||
<IconButton
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
size="small"
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
cursor: isDragging ? "grabbing" : "grab",
|
||||
mr: 1,
|
||||
'&:active': { cursor: 'grabbing' }
|
||||
opacity: isLocalHovered || isDragging ? 1 : 0.4,
|
||||
color: 'text.secondary',
|
||||
'&:hover': {
|
||||
color: 'text.primary',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<DragIndicator fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<ListItemIcon sx={{ minWidth: collapsed ? "auto" : 40 }}>
|
||||
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||
</ListItemIcon>
|
||||
|
||||
{!collapsed && (
|
||||
<>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
/>
|
||||
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
|
||||
<Tooltip title={item.title} placement="right" enterDelay={400} arrow>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontWeight: isFolder ? 600 : 400,
|
||||
color: isFolder ? 'text.primary' : 'text.secondary',
|
||||
maxWidth: calculateMaxTextWidth(),
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 2, // максимум 2 строки
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
lineHeight: 1.2,
|
||||
fontSize: "0.85rem", // компактнее текст
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mr: 0.5, flex: '1 1 auto', minWidth: 0 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
{hasChildren && (
|
||||
<ChevronRight
|
||||
sx={{
|
||||
fontSize: 18,
|
||||
color: 'text.disabled',
|
||||
transform: isOpen ? 'rotate(90deg)' : 'none',
|
||||
transition: 'transform 0.2s ease',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ListItem>
|
||||
|
||||
{hasChildren && !collapsed && (
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<List disablePadding>
|
||||
<List
|
||||
disablePadding
|
||||
sx={{
|
||||
pl: 1.5,
|
||||
borderLeft: (theme) => `1px solid ${alpha(theme.palette.divider, 0.1)}`,
|
||||
marginLeft: 2,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{item.items.map((child) => (
|
||||
<SortableMenuItem
|
||||
key={child.id}
|
||||
item={child}
|
||||
collapsed={collapsed}
|
||||
onSelectItem={onSelectItem}
|
||||
level={level + 1}
|
||||
/>
|
||||
<Box key={child.id} position="relative">
|
||||
<SortableMenuItem
|
||||
item={child}
|
||||
collapsed={collapsed}
|
||||
onSelectItem={onSelectItem}
|
||||
level={level + 1}
|
||||
isHovered={isHovered}
|
||||
showDropIndicator={showDropIndicator}
|
||||
sidebarWidth={sidebarWidth}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
|
|
|
|||
Loading…
Reference in New Issue