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