-
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { useState, useRef, useCallback, useEffect, type ReactNode } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Box, IconButton, Tooltip, Typography, alpha } from "@mui/material";
|
||||
import {
|
||||
ChevronLeft as CollapseLeftIcon,
|
||||
@@ -6,13 +12,8 @@ import {
|
||||
DragIndicator as DragIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
export interface PanelConfig {
|
||||
id: string;
|
||||
width: number;
|
||||
collapsed: boolean;
|
||||
position: "left" | "right";
|
||||
order: number;
|
||||
}
|
||||
// Data transfer type for panel drag and drop
|
||||
export const PANEL_DRAG_TYPE = "application/x-panel-id";
|
||||
|
||||
interface ResizablePanelProps {
|
||||
/** Unique panel identifier */
|
||||
@@ -21,32 +22,24 @@ interface ResizablePanelProps {
|
||||
title: string;
|
||||
/** Icon shown when panel is collapsed */
|
||||
icon: ReactNode;
|
||||
/** Panel position - determines collapse button direction and resize handle position */
|
||||
/** Panel position - determines collapse button direction */
|
||||
position: "left" | "right";
|
||||
/** Current width in pixels */
|
||||
width: number;
|
||||
/** Minimum width when expanded */
|
||||
minWidth?: number;
|
||||
/** Maximum width when expanded */
|
||||
maxWidth?: number;
|
||||
/** Flex value for vertical sizing (default 1) */
|
||||
flex?: number;
|
||||
/** Whether the panel is currently collapsed */
|
||||
collapsed: boolean;
|
||||
/** Panel width when collapsed (icon strip) */
|
||||
collapsedWidth?: number;
|
||||
/** Callback when width changes during resize */
|
||||
onWidthChange: (width: number) => void;
|
||||
/** Callback when flex changes during vertical resize */
|
||||
onFlexChange?: (flex: number) => void;
|
||||
/** Callback when collapse state changes */
|
||||
onToggleCollapse: () => void;
|
||||
/** Panel content */
|
||||
children: ReactNode;
|
||||
/** Optional badge content (e.g., count) */
|
||||
badge?: ReactNode;
|
||||
/** Enable drag handle for reordering */
|
||||
draggable?: boolean;
|
||||
/** Drag start handler */
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
/** Drag end handler */
|
||||
onDragEnd?: (e: React.DragEvent) => void;
|
||||
/** Whether this is the last panel in the sidebar (no bottom resize handle) */
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
export default function ResizablePanel({
|
||||
@@ -54,53 +47,68 @@ export default function ResizablePanel({
|
||||
title,
|
||||
icon,
|
||||
position,
|
||||
width,
|
||||
minWidth = 200,
|
||||
maxWidth = 500,
|
||||
flex = 1,
|
||||
collapsed,
|
||||
collapsedWidth = 44,
|
||||
onWidthChange,
|
||||
onFlexChange,
|
||||
onToggleCollapse,
|
||||
children,
|
||||
badge,
|
||||
draggable = false,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
isLast = false,
|
||||
}: ResizablePanelProps) {
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [isResizingHeight, setIsResizingHeight] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const startXRef = useRef(0);
|
||||
const startWidthRef = useRef(width);
|
||||
const startYRef = useRef(0);
|
||||
const startHeightRef = useRef(0);
|
||||
const startFlexRef = useRef(flex);
|
||||
|
||||
const CollapseIcon = position === "left" ? CollapseLeftIcon : CollapseRightIcon;
|
||||
const CollapseIcon =
|
||||
position === "left" ? CollapseLeftIcon : CollapseRightIcon;
|
||||
const ExpandIcon = position === "left" ? CollapseRightIcon : CollapseLeftIcon;
|
||||
|
||||
// Handle mouse down on resize handle
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
startXRef.current = e.clientX;
|
||||
startWidthRef.current = width;
|
||||
// Handle drag start - make header draggable
|
||||
const handleDragStart = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.dataTransfer.setData(PANEL_DRAG_TYPE, id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
setIsDragging(true);
|
||||
},
|
||||
[width]
|
||||
[id],
|
||||
);
|
||||
|
||||
// Handle mouse move during resize
|
||||
// Handle drag end
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
// Handle mouse down on vertical resize handle
|
||||
const handleResizeHeightStart = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!panelRef.current) return;
|
||||
setIsResizingHeight(true);
|
||||
startYRef.current = e.clientY;
|
||||
startHeightRef.current = panelRef.current.offsetHeight;
|
||||
startFlexRef.current = flex;
|
||||
},
|
||||
[flex],
|
||||
);
|
||||
|
||||
// Handle mouse move during vertical resize
|
||||
useEffect(() => {
|
||||
if (!isResizing) return;
|
||||
if (!isResizingHeight || !onFlexChange) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = position === "left"
|
||||
? e.clientX - startXRef.current
|
||||
: startXRef.current - e.clientX;
|
||||
|
||||
const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidthRef.current + delta));
|
||||
onWidthChange(newWidth);
|
||||
const deltaY = e.clientY - startYRef.current;
|
||||
const heightRatio =
|
||||
(startHeightRef.current + deltaY) / startHeightRef.current;
|
||||
const newFlex = Math.max(0.2, startFlexRef.current * heightRatio);
|
||||
onFlexChange(newFlex);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
setIsResizingHeight(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
@@ -110,34 +118,50 @@ export default function ResizablePanel({
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isResizing, position, minWidth, maxWidth, onWidthChange]);
|
||||
}, [isResizingHeight, onFlexChange]);
|
||||
|
||||
// Collapsed state - show icon strip
|
||||
// Collapsed state - show icon strip (also draggable)
|
||||
if (collapsed) {
|
||||
return (
|
||||
<Box
|
||||
ref={panelRef}
|
||||
data-panel-id={id}
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
sx={{
|
||||
width: collapsedWidth,
|
||||
minWidth: collapsedWidth,
|
||||
height: "100%",
|
||||
flex: flex,
|
||||
minHeight: 80,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.03),
|
||||
borderLeft: position === "right" ? 1 : 0,
|
||||
borderRight: position === "left" ? 1 : 0,
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
py: 1,
|
||||
flexShrink: 0,
|
||||
flexShrink: 1,
|
||||
flexGrow: flex,
|
||||
cursor: "grab",
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
"&:active": {
|
||||
cursor: "grabbing",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Expand button */}
|
||||
<Tooltip title={`Espandi: ${title}`} placement={position === "left" ? "right" : "left"}>
|
||||
<Tooltip
|
||||
title={`Espandi: ${title}`}
|
||||
placement={position === "left" ? "right" : "left"}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onToggleCollapse}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleCollapse();
|
||||
}}
|
||||
sx={{
|
||||
mb: 1,
|
||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.08),
|
||||
@@ -151,21 +175,19 @@ export default function ResizablePanel({
|
||||
</Tooltip>
|
||||
|
||||
{/* Panel icon with tooltip */}
|
||||
<Tooltip title={title} placement={position === "left" ? "right" : "left"}>
|
||||
<Tooltip
|
||||
title={`${title} - Trascina per spostare`}
|
||||
placement={position === "left" ? "right" : "left"}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
"&:hover": {
|
||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.08),
|
||||
},
|
||||
}}
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -209,15 +231,15 @@ export default function ResizablePanel({
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: 500,
|
||||
fontSize: "0.7rem",
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: "primary.main",
|
||||
},
|
||||
}}
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{/* Drag indicator at bottom */}
|
||||
<Box sx={{ mt: "auto", mb: 1, color: "text.disabled" }}>
|
||||
<DragIcon fontSize="small" />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -228,63 +250,26 @@ export default function ResizablePanel({
|
||||
ref={panelRef}
|
||||
data-panel-id={id}
|
||||
sx={{
|
||||
width,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
flex: flex,
|
||||
minHeight: 120,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
borderLeft: position === "right" ? 1 : 0,
|
||||
borderRight: position === "left" ? 1 : 0,
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
bgcolor: "background.paper",
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
flexShrink: 1,
|
||||
flexGrow: flex,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
>
|
||||
{/* Resize handle */}
|
||||
<Box
|
||||
onMouseDown={handleResizeStart}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
[position === "left" ? "right" : "left"]: -3,
|
||||
width: 6,
|
||||
cursor: "col-resize",
|
||||
zIndex: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"&:hover": {
|
||||
"& .resize-indicator": {
|
||||
opacity: 1,
|
||||
bgcolor: "primary.main",
|
||||
},
|
||||
},
|
||||
...(isResizing && {
|
||||
"& .resize-indicator": {
|
||||
opacity: 1,
|
||||
bgcolor: "primary.main",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="resize-indicator"
|
||||
sx={{
|
||||
width: 3,
|
||||
height: 40,
|
||||
borderRadius: 1,
|
||||
bgcolor: "divider",
|
||||
opacity: 0.5,
|
||||
transition: "opacity 0.15s, background-color 0.15s",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Header with collapse button */}
|
||||
{/* Header with collapse button - DRAGGABLE */}
|
||||
<Box
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -295,27 +280,37 @@ export default function ResizablePanel({
|
||||
borderColor: "divider",
|
||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.03),
|
||||
minHeight: 40,
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
"&:active": {
|
||||
cursor: "grabbing",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.75, minWidth: 0, flex: 1 }}>
|
||||
{/* Drag handle */}
|
||||
{draggable && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.75,
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Drag handle indicator */}
|
||||
<Tooltip title="Trascina per spostare">
|
||||
<Box
|
||||
draggable
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
color: "text.disabled",
|
||||
display: "flex",
|
||||
"&:hover": { color: "text.secondary" },
|
||||
"&:active": { cursor: "grabbing" },
|
||||
}}
|
||||
>
|
||||
<DragIcon fontSize="small" />
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ color: "primary.main", display: "flex", flexShrink: 0 }}>{icon}</Box>
|
||||
</Tooltip>
|
||||
<Box sx={{ color: "primary.main", display: "flex", flexShrink: 0 }}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
fontWeight={600}
|
||||
@@ -348,13 +343,20 @@ export default function ResizablePanel({
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip title="Comprimi">
|
||||
<IconButton size="small" onClick={onToggleCollapse} sx={{ flexShrink: 0 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleCollapse();
|
||||
}}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
<CollapseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Content - no internal scrollbar, content must handle its own overflow */}
|
||||
{/* Content */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
@@ -365,6 +367,49 @@ export default function ResizablePanel({
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
||||
{/* Vertical resize handle (only show if not last panel and onFlexChange is provided) */}
|
||||
{!isLast && onFlexChange && (
|
||||
<Box
|
||||
onMouseDown={handleResizeHeightStart}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: -4,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 8,
|
||||
cursor: "row-resize",
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"&:hover": {
|
||||
"& .resize-indicator-v": {
|
||||
opacity: 1,
|
||||
bgcolor: "primary.main",
|
||||
},
|
||||
},
|
||||
...(isResizingHeight && {
|
||||
"& .resize-indicator-v": {
|
||||
opacity: 1,
|
||||
bgcolor: "primary.main",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="resize-indicator-v"
|
||||
sx={{
|
||||
width: 50,
|
||||
height: 4,
|
||||
borderRadius: 1,
|
||||
bgcolor: "divider",
|
||||
opacity: 0.7,
|
||||
transition: "opacity 0.15s, background-color 0.15s",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
324
frontend/src/components/reportEditor/SidebarDropZone.tsx
Normal file
324
frontend/src/components/reportEditor/SidebarDropZone.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Box, alpha } from "@mui/material";
|
||||
import { PANEL_DRAG_TYPE } from "./ResizablePanel";
|
||||
|
||||
interface SidebarDropZoneProps {
|
||||
/** Sidebar position */
|
||||
position: "left" | "right";
|
||||
/** Sidebar width in pixels */
|
||||
width: number;
|
||||
/** Minimum width */
|
||||
minWidth?: number;
|
||||
/** Maximum width */
|
||||
maxWidth?: number;
|
||||
/** Callback when width changes */
|
||||
onWidthChange: (width: number) => void;
|
||||
/** Children (panels) */
|
||||
children: ReactNode;
|
||||
/** Callback when a panel is dropped */
|
||||
onPanelDrop: (
|
||||
panelId: string,
|
||||
targetPosition: "left" | "right",
|
||||
targetIndex: number,
|
||||
) => void;
|
||||
/** Current panel IDs in this sidebar (for calculating drop index) */
|
||||
panelIds: string[];
|
||||
}
|
||||
|
||||
export default function SidebarDropZone({
|
||||
position,
|
||||
width,
|
||||
minWidth = 180,
|
||||
maxWidth = 500,
|
||||
onWidthChange,
|
||||
children,
|
||||
onPanelDrop,
|
||||
panelIds,
|
||||
}: SidebarDropZoneProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [dropIndex, setDropIndex] = useState<number | null>(null);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startXRef = useRef(0);
|
||||
const startWidthRef = useRef(width);
|
||||
|
||||
// Handle horizontal resize
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsResizing(true);
|
||||
startXRef.current = e.clientX;
|
||||
startWidthRef.current = width;
|
||||
},
|
||||
[width],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta =
|
||||
position === "left"
|
||||
? e.clientX - startXRef.current
|
||||
: startXRef.current - e.clientX;
|
||||
|
||||
const newWidth = Math.min(
|
||||
maxWidth,
|
||||
Math.max(minWidth, startWidthRef.current + delta),
|
||||
);
|
||||
onWidthChange(newWidth);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isResizing, position, minWidth, maxWidth, onWidthChange]);
|
||||
|
||||
// Handle drag over
|
||||
const handleDragOver = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
// Only accept panel drags
|
||||
if (!e.dataTransfer.types.includes(PANEL_DRAG_TYPE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
setIsDragOver(true);
|
||||
|
||||
// Calculate drop index based on mouse position
|
||||
const container = e.currentTarget as HTMLElement;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
// Find which panel slot the mouse is over
|
||||
const panelElements = container.querySelectorAll("[data-panel-id]");
|
||||
let newDropIndex = panelIds.length; // Default to end
|
||||
|
||||
panelElements.forEach((el, index) => {
|
||||
const panelRect = el.getBoundingClientRect();
|
||||
const panelMiddle = panelRect.top + panelRect.height / 2 - rect.top;
|
||||
|
||||
if (mouseY < panelMiddle) {
|
||||
newDropIndex = Math.min(newDropIndex, index);
|
||||
}
|
||||
});
|
||||
|
||||
setDropIndex(newDropIndex);
|
||||
},
|
||||
[panelIds.length],
|
||||
);
|
||||
|
||||
// Handle drag leave
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
// Only reset if leaving the container entirely
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (!e.currentTarget.contains(relatedTarget)) {
|
||||
setIsDragOver(false);
|
||||
setDropIndex(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle drop
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const panelId = e.dataTransfer.getData(PANEL_DRAG_TYPE);
|
||||
console.log(
|
||||
"[SidebarDropZone] Drop:",
|
||||
panelId,
|
||||
"position:",
|
||||
position,
|
||||
"dropIndex:",
|
||||
dropIndex,
|
||||
);
|
||||
if (!panelId) return;
|
||||
|
||||
// Calculate final drop index
|
||||
const finalIndex = dropIndex ?? panelIds.length;
|
||||
|
||||
// If dropping in same sidebar, adjust index if needed
|
||||
const currentIndex = panelIds.indexOf(panelId);
|
||||
let adjustedIndex = finalIndex;
|
||||
|
||||
if (currentIndex !== -1 && currentIndex < finalIndex) {
|
||||
// If moving down in same sidebar, subtract 1 because the panel will be removed first
|
||||
adjustedIndex = finalIndex - 1;
|
||||
}
|
||||
|
||||
onPanelDrop(panelId, position, adjustedIndex);
|
||||
setDropIndex(null);
|
||||
},
|
||||
[dropIndex, panelIds, position, onPanelDrop],
|
||||
);
|
||||
|
||||
// Calculate drop indicator position
|
||||
const getDropIndicatorTop = useCallback(() => {
|
||||
if (dropIndex === null) return 0;
|
||||
|
||||
// We need to find the panel elements in the DOM
|
||||
const panelElements = document.querySelectorAll(
|
||||
`[data-sidebar="${position}"] [data-panel-id]`,
|
||||
);
|
||||
|
||||
if (dropIndex === 0) return 0;
|
||||
|
||||
if (dropIndex >= panelElements.length) {
|
||||
// Drop at end - position after last panel
|
||||
const lastPanel = panelElements[panelElements.length - 1];
|
||||
if (lastPanel) {
|
||||
const container = lastPanel.parentElement;
|
||||
if (container) {
|
||||
const rect = lastPanel.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
return rect.bottom - containerRect.top;
|
||||
}
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
const targetPanel = panelElements[dropIndex];
|
||||
if (targetPanel) {
|
||||
const container = targetPanel.parentElement;
|
||||
if (container) {
|
||||
const rect = targetPanel.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
return rect.top - containerRect.top;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}, [dropIndex, position]);
|
||||
|
||||
const hasNoPanels = panelIds.length === 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-sidebar={position}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
width: width,
|
||||
minWidth: hasNoPanels ? 60 : minWidth,
|
||||
maxWidth: maxWidth,
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
// Visual feedback during drag
|
||||
...(isDragOver && {
|
||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.05),
|
||||
}),
|
||||
transition: "background-color 0.2s",
|
||||
}}
|
||||
>
|
||||
{/* Horizontal resize handle */}
|
||||
<Box
|
||||
onMouseDown={handleResizeStart}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
[position === "left" ? "right" : "left"]: -4,
|
||||
width: 8,
|
||||
cursor: "col-resize",
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
"&:hover": {
|
||||
"& .resize-indicator": {
|
||||
opacity: 1,
|
||||
bgcolor: "primary.main",
|
||||
},
|
||||
},
|
||||
...(isResizing && {
|
||||
"& .resize-indicator": {
|
||||
opacity: 1,
|
||||
bgcolor: "primary.main",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="resize-indicator"
|
||||
sx={{
|
||||
width: 4,
|
||||
height: 50,
|
||||
borderRadius: 1,
|
||||
bgcolor: "divider",
|
||||
opacity: 0.7,
|
||||
transition: "opacity 0.15s, background-color 0.15s",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Drop indicator line */}
|
||||
{isDragOver && dropIndex !== null && !hasNoPanels && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
bgcolor: "primary.main",
|
||||
borderRadius: 1,
|
||||
zIndex: 100,
|
||||
top: getDropIndicatorTop(),
|
||||
pointerEvents: "none",
|
||||
// Box shadow for glow effect
|
||||
boxShadow: (theme) =>
|
||||
`0 0 8px 2px ${alpha(theme.palette.primary.main, 0.4)}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Panels */}
|
||||
{children}
|
||||
|
||||
{/* Empty state drop zone (when sidebar has no panels) */}
|
||||
{hasNoPanels && (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: isDragOver ? 2 : 1,
|
||||
borderColor: isDragOver ? "primary.main" : "divider",
|
||||
borderStyle: "dashed",
|
||||
borderRadius: 1,
|
||||
m: 1,
|
||||
p: 2,
|
||||
color: "text.secondary",
|
||||
fontSize: "0.75rem",
|
||||
textAlign: "center",
|
||||
transition: "all 0.2s",
|
||||
bgcolor: isDragOver
|
||||
? (theme) => alpha(theme.palette.primary.main, 0.1)
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
Trascina qui
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -3,50 +3,103 @@ import { useLocalStorage } from "./useLocalStorage";
|
||||
|
||||
export interface PanelState {
|
||||
id: string;
|
||||
width: number;
|
||||
/** Flex value for vertical sizing (1 = equal share, 2 = double share, etc.) */
|
||||
flex: number;
|
||||
collapsed: boolean;
|
||||
position: "left" | "right";
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface SidebarWidths {
|
||||
left: number;
|
||||
right: number;
|
||||
}
|
||||
|
||||
export interface PanelLayoutConfig {
|
||||
panels: PanelState[];
|
||||
sidebarWidths: SidebarWidths;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "apollinare-report-editor-panels";
|
||||
const CONFIG_VERSION = 1;
|
||||
const CONFIG_VERSION = 3; // Bumped version for sidebar widths
|
||||
|
||||
// Default sidebar widths
|
||||
const defaultSidebarWidths: SidebarWidths = {
|
||||
left: 250,
|
||||
right: 300,
|
||||
};
|
||||
|
||||
// Default panel configuration
|
||||
const defaultConfig: PanelLayoutConfig = {
|
||||
version: CONFIG_VERSION,
|
||||
sidebarWidths: defaultSidebarWidths,
|
||||
panels: [
|
||||
{ id: "pages", width: 220, collapsed: false, position: "left", order: 0 },
|
||||
{ id: "data", width: 280, collapsed: false, position: "right", order: 0 },
|
||||
{ id: "properties", width: 280, collapsed: false, position: "right", order: 1 },
|
||||
{
|
||||
id: "pages",
|
||||
flex: 1,
|
||||
collapsed: false,
|
||||
position: "left",
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
id: "data",
|
||||
flex: 1,
|
||||
collapsed: false,
|
||||
position: "right",
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
id: "properties",
|
||||
flex: 1,
|
||||
collapsed: false,
|
||||
position: "right",
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function usePanelLayout() {
|
||||
const [config, setConfig] = useLocalStorage<PanelLayoutConfig>(
|
||||
const [config, setConfig, clearConfig] = useLocalStorage<PanelLayoutConfig>(
|
||||
STORAGE_KEY,
|
||||
defaultConfig
|
||||
defaultConfig,
|
||||
);
|
||||
|
||||
// Ensure config is up to date (handle version migrations)
|
||||
const normalizedConfig = useMemo(() => {
|
||||
if (!config || config.version !== CONFIG_VERSION) {
|
||||
if (!config) {
|
||||
return defaultConfig;
|
||||
}
|
||||
return config;
|
||||
}, [config]);
|
||||
|
||||
// If version mismatch, migrate or reset
|
||||
if (config.version !== CONFIG_VERSION) {
|
||||
console.log(
|
||||
"[usePanelLayout] Version mismatch, resetting to default config",
|
||||
);
|
||||
// Clear old config and return default
|
||||
clearConfig();
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Ensure all panels have flex property
|
||||
const migratedPanels = config.panels.map((p) => ({
|
||||
...p,
|
||||
flex: p.flex ?? 1,
|
||||
}));
|
||||
|
||||
return {
|
||||
...config,
|
||||
sidebarWidths: config.sidebarWidths ?? defaultSidebarWidths,
|
||||
panels: migratedPanels,
|
||||
};
|
||||
}, [config, clearConfig]);
|
||||
|
||||
// Get panel state by id
|
||||
const getPanelState = useCallback(
|
||||
(panelId: string): PanelState | undefined => {
|
||||
return normalizedConfig.panels.find((p) => p.id === panelId);
|
||||
},
|
||||
[normalizedConfig.panels]
|
||||
[normalizedConfig.panels],
|
||||
);
|
||||
|
||||
// Update a single panel's state
|
||||
@@ -55,11 +108,11 @@ export function usePanelLayout() {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
panels: prev.panels.map((p) =>
|
||||
p.id === panelId ? { ...p, ...updates } : p
|
||||
p.id === panelId ? { ...p, ...updates } : p,
|
||||
),
|
||||
}));
|
||||
},
|
||||
[setConfig]
|
||||
[setConfig],
|
||||
);
|
||||
|
||||
// Toggle panel collapse state
|
||||
@@ -68,48 +121,110 @@ export function usePanelLayout() {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
panels: prev.panels.map((p) =>
|
||||
p.id === panelId ? { ...p, collapsed: !p.collapsed } : p
|
||||
p.id === panelId ? { ...p, collapsed: !p.collapsed } : p,
|
||||
),
|
||||
}));
|
||||
},
|
||||
[setConfig]
|
||||
[setConfig],
|
||||
);
|
||||
|
||||
// Update panel width
|
||||
const setPanelWidth = useCallback(
|
||||
(panelId: string, width: number) => {
|
||||
// Update sidebar width
|
||||
const setSidebarWidth = useCallback(
|
||||
(position: "left" | "right", width: number) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
sidebarWidths: {
|
||||
...prev.sidebarWidths,
|
||||
[position]: Math.max(180, Math.min(500, width)),
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setConfig],
|
||||
);
|
||||
|
||||
// Update panel flex value
|
||||
const setPanelFlex = useCallback(
|
||||
(panelId: string, flex: number) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
panels: prev.panels.map((p) =>
|
||||
p.id === panelId ? { ...p, width } : p
|
||||
p.id === panelId ? { ...p, flex: Math.max(0.2, flex) } : p,
|
||||
),
|
||||
}));
|
||||
},
|
||||
[setConfig]
|
||||
[setConfig],
|
||||
);
|
||||
|
||||
// Move panel to a different position (left/right sidebar)
|
||||
// Auto-redistributes flex values so all panels in the sidebar share space equally
|
||||
const movePanelToPosition = useCallback(
|
||||
(panelId: string, newPosition: "left" | "right", newOrder?: number) => {
|
||||
setConfig((prev) => {
|
||||
const movingPanel = prev.panels.find((p) => p.id === panelId);
|
||||
if (!movingPanel) return prev;
|
||||
|
||||
const isMovingToNewSidebar = movingPanel.position !== newPosition;
|
||||
|
||||
// Get panels in the target position (excluding the moving panel)
|
||||
const panelsInNewPosition = prev.panels.filter(
|
||||
(p) => p.position === newPosition && p.id !== panelId
|
||||
(p) => p.position === newPosition && p.id !== panelId,
|
||||
);
|
||||
|
||||
// Calculate new order
|
||||
const maxOrder =
|
||||
panelsInNewPosition.length > 0
|
||||
? Math.max(...panelsInNewPosition.map((p) => p.order)) + 1
|
||||
: 0;
|
||||
const finalOrder = newOrder ?? maxOrder;
|
||||
|
||||
// All panels get flex: 1 for equal distribution
|
||||
const equalFlex = 1;
|
||||
|
||||
// Get panels in the old position (excluding the moving panel) for rebalancing
|
||||
const panelsInOldPosition = prev.panels.filter(
|
||||
(p) => p.position === movingPanel.position && p.id !== panelId,
|
||||
);
|
||||
const maxOrder = panelsInNewPosition.length > 0
|
||||
? Math.max(...panelsInNewPosition.map((p) => p.order)) + 1
|
||||
: 0;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
panels: prev.panels.map((p) =>
|
||||
p.id === panelId
|
||||
? { ...p, position: newPosition, order: newOrder ?? maxOrder }
|
||||
: p
|
||||
),
|
||||
panels: prev.panels.map((p) => {
|
||||
// The panel being moved
|
||||
if (p.id === panelId) {
|
||||
return {
|
||||
...p,
|
||||
position: newPosition,
|
||||
order: finalOrder,
|
||||
flex: equalFlex,
|
||||
};
|
||||
}
|
||||
|
||||
// Panels in the target sidebar: reset to equal flex
|
||||
if (p.position === newPosition && isMovingToNewSidebar) {
|
||||
return { ...p, flex: equalFlex };
|
||||
}
|
||||
|
||||
// Panels in the old sidebar (after panel leaves): reset to equal flex
|
||||
if (
|
||||
isMovingToNewSidebar &&
|
||||
p.position === movingPanel.position &&
|
||||
panelsInOldPosition.length > 0
|
||||
) {
|
||||
return { ...p, flex: equalFlex };
|
||||
}
|
||||
|
||||
// Adjust order for panels in target position if inserting at specific index
|
||||
if (p.position === newPosition && newOrder !== undefined) {
|
||||
if (p.order >= newOrder) {
|
||||
return { ...p, order: p.order + 1 };
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
[setConfig]
|
||||
[setConfig],
|
||||
);
|
||||
|
||||
// Reorder panels within a sidebar
|
||||
@@ -124,7 +239,7 @@ export function usePanelLayout() {
|
||||
}),
|
||||
}));
|
||||
},
|
||||
[setConfig]
|
||||
[setConfig],
|
||||
);
|
||||
|
||||
// Get panels for a specific position, sorted by order
|
||||
@@ -134,7 +249,15 @@ export function usePanelLayout() {
|
||||
.filter((p) => p.position === position)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
},
|
||||
[normalizedConfig.panels]
|
||||
[normalizedConfig.panels],
|
||||
);
|
||||
|
||||
// Get sidebar width
|
||||
const getSidebarWidth = useCallback(
|
||||
(position: "left" | "right"): number => {
|
||||
return normalizedConfig.sidebarWidths[position];
|
||||
},
|
||||
[normalizedConfig.sidebarWidths],
|
||||
);
|
||||
|
||||
// Reset to default configuration
|
||||
@@ -147,7 +270,9 @@ export function usePanelLayout() {
|
||||
getPanelState,
|
||||
updatePanel,
|
||||
togglePanelCollapse,
|
||||
setPanelWidth,
|
||||
setSidebarWidth,
|
||||
getSidebarWidth,
|
||||
setPanelFlex,
|
||||
movePanelToPosition,
|
||||
reorderPanels,
|
||||
getPanelsForPosition,
|
||||
|
||||
@@ -64,6 +64,7 @@ import ImageUploadDialog, {
|
||||
} from "../components/reportEditor/ImageUploadDialog";
|
||||
import PageNavigator from "../components/reportEditor/PageNavigator";
|
||||
import ResizablePanel from "../components/reportEditor/ResizablePanel";
|
||||
import SidebarDropZone from "../components/reportEditor/SidebarDropZone";
|
||||
import {
|
||||
reportTemplateService,
|
||||
reportFontService,
|
||||
@@ -149,6 +150,18 @@ export default function ReportEditorPage() {
|
||||
// Panel layout configuration (persisted to localStorage)
|
||||
const panelLayout = usePanelLayout();
|
||||
|
||||
// Handle panel drag and drop between sidebars
|
||||
const handlePanelDrop = useCallback(
|
||||
(
|
||||
panelId: string,
|
||||
targetPosition: "left" | "right",
|
||||
targetIndex: number,
|
||||
) => {
|
||||
panelLayout.movePanelToPosition(panelId, targetPosition, targetIndex);
|
||||
},
|
||||
[panelLayout],
|
||||
);
|
||||
|
||||
// UI state
|
||||
const [saveDialog, setSaveDialog] = useState(false);
|
||||
const [previewDialog, setPreviewDialog] = useState(false);
|
||||
@@ -1901,88 +1914,96 @@ export default function ReportEditorPage() {
|
||||
{/* Main Editor Area */}
|
||||
<Box sx={{ display: "flex", flex: 1, overflow: "hidden" }}>
|
||||
{/* Left Sidebar - Panels on left side */}
|
||||
{!isMobile && (
|
||||
<Box sx={{ display: "flex", height: "100%" }}>
|
||||
{panelLayout.getPanelsForPosition("left").map((panelState) => {
|
||||
if (panelState.id === "pages") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Pagine"
|
||||
icon={<LayersIcon />}
|
||||
position="left"
|
||||
width={panelState.width}
|
||||
minWidth={180}
|
||||
maxWidth={350}
|
||||
collapsed={panelState.collapsed}
|
||||
onWidthChange={(w) =>
|
||||
panelLayout.setPanelWidth(panelState.id, w)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={template.pages.length}
|
||||
>
|
||||
{renderPageNavigator(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "data") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Campi Dati"
|
||||
icon={<DataIcon />}
|
||||
position="left"
|
||||
width={panelState.width}
|
||||
minWidth={220}
|
||||
maxWidth={400}
|
||||
collapsed={panelState.collapsed}
|
||||
onWidthChange={(w) =>
|
||||
panelLayout.setPanelWidth(panelState.id, w)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={
|
||||
selectedDatasets.length > 0
|
||||
? selectedDatasets.length
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{renderDataBindingPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "properties") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Proprietà"
|
||||
icon={<SettingsIcon />}
|
||||
position="left"
|
||||
width={panelState.width}
|
||||
minWidth={220}
|
||||
maxWidth={400}
|
||||
collapsed={panelState.collapsed}
|
||||
onWidthChange={(w) =>
|
||||
panelLayout.setPanelWidth(panelState.id, w)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
>
|
||||
{renderPropertiesPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
{!isMobile &&
|
||||
(() => {
|
||||
const leftPanels = panelLayout.getPanelsForPosition("left");
|
||||
return (
|
||||
<SidebarDropZone
|
||||
position="left"
|
||||
width={panelLayout.getSidebarWidth("left")}
|
||||
onWidthChange={(w) => panelLayout.setSidebarWidth("left", w)}
|
||||
onPanelDrop={handlePanelDrop}
|
||||
panelIds={leftPanels.map((p) => p.id)}
|
||||
>
|
||||
{leftPanels.map((panelState, index) => {
|
||||
const isLast = index === leftPanels.length - 1;
|
||||
if (panelState.id === "pages") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Pagine"
|
||||
icon={<LayersIcon />}
|
||||
position="left"
|
||||
flex={panelState.flex}
|
||||
collapsed={panelState.collapsed}
|
||||
onFlexChange={(f) =>
|
||||
panelLayout.setPanelFlex(panelState.id, f)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={template.pages.length}
|
||||
isLast={isLast}
|
||||
>
|
||||
{renderPageNavigator(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "data") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Campi Dati"
|
||||
icon={<DataIcon />}
|
||||
position="left"
|
||||
flex={panelState.flex}
|
||||
collapsed={panelState.collapsed}
|
||||
onFlexChange={(f) =>
|
||||
panelLayout.setPanelFlex(panelState.id, f)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={
|
||||
selectedDatasets.length > 0
|
||||
? selectedDatasets.length
|
||||
: undefined
|
||||
}
|
||||
isLast={isLast}
|
||||
>
|
||||
{renderDataBindingPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "properties") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Proprietà"
|
||||
icon={<SettingsIcon />}
|
||||
position="left"
|
||||
flex={panelState.flex}
|
||||
collapsed={panelState.collapsed}
|
||||
onFlexChange={(f) =>
|
||||
panelLayout.setPanelFlex(panelState.id, f)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
isLast={isLast}
|
||||
>
|
||||
{renderPropertiesPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</SidebarDropZone>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Center Area - Canvas Container (flex: 1 to take remaining space, centers the canvas) */}
|
||||
<Box
|
||||
@@ -2034,88 +2055,96 @@ export default function ReportEditorPage() {
|
||||
</Box>
|
||||
|
||||
{/* Right Sidebar - Panels on right side */}
|
||||
{!isMobile && (
|
||||
<Box sx={{ display: "flex", height: "100%" }}>
|
||||
{panelLayout.getPanelsForPosition("right").map((panelState) => {
|
||||
if (panelState.id === "pages") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Pagine"
|
||||
icon={<LayersIcon />}
|
||||
position="right"
|
||||
width={panelState.width}
|
||||
minWidth={180}
|
||||
maxWidth={350}
|
||||
collapsed={panelState.collapsed}
|
||||
onWidthChange={(w) =>
|
||||
panelLayout.setPanelWidth(panelState.id, w)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={template.pages.length}
|
||||
>
|
||||
{renderPageNavigator(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "data") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Campi Dati"
|
||||
icon={<DataIcon />}
|
||||
position="right"
|
||||
width={panelState.width}
|
||||
minWidth={220}
|
||||
maxWidth={400}
|
||||
collapsed={panelState.collapsed}
|
||||
onWidthChange={(w) =>
|
||||
panelLayout.setPanelWidth(panelState.id, w)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={
|
||||
selectedDatasets.length > 0
|
||||
? selectedDatasets.length
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{renderDataBindingPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "properties") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Proprietà"
|
||||
icon={<SettingsIcon />}
|
||||
position="right"
|
||||
width={panelState.width}
|
||||
minWidth={220}
|
||||
maxWidth={400}
|
||||
collapsed={panelState.collapsed}
|
||||
onWidthChange={(w) =>
|
||||
panelLayout.setPanelWidth(panelState.id, w)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
>
|
||||
{renderPropertiesPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
{!isMobile &&
|
||||
(() => {
|
||||
const rightPanels = panelLayout.getPanelsForPosition("right");
|
||||
return (
|
||||
<SidebarDropZone
|
||||
position="right"
|
||||
width={panelLayout.getSidebarWidth("right")}
|
||||
onWidthChange={(w) => panelLayout.setSidebarWidth("right", w)}
|
||||
onPanelDrop={handlePanelDrop}
|
||||
panelIds={rightPanels.map((p) => p.id)}
|
||||
>
|
||||
{rightPanels.map((panelState, index) => {
|
||||
const isLast = index === rightPanels.length - 1;
|
||||
if (panelState.id === "pages") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Pagine"
|
||||
icon={<LayersIcon />}
|
||||
position={panelState.position}
|
||||
flex={panelState.flex}
|
||||
collapsed={panelState.collapsed}
|
||||
onFlexChange={(f) =>
|
||||
panelLayout.setPanelFlex(panelState.id, f)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={template.pages.length}
|
||||
isLast={isLast}
|
||||
>
|
||||
{renderPageNavigator(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "data") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Campi Dati"
|
||||
icon={<DataIcon />}
|
||||
position={panelState.position}
|
||||
flex={panelState.flex}
|
||||
collapsed={panelState.collapsed}
|
||||
onFlexChange={(f) =>
|
||||
panelLayout.setPanelFlex(panelState.id, f)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
badge={
|
||||
selectedDatasets.length > 0
|
||||
? selectedDatasets.length
|
||||
: undefined
|
||||
}
|
||||
isLast={isLast}
|
||||
>
|
||||
{renderDataBindingPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
if (panelState.id === "properties") {
|
||||
return (
|
||||
<ResizablePanel
|
||||
key={panelState.id}
|
||||
id={panelState.id}
|
||||
title="Proprietà"
|
||||
icon={<SettingsIcon />}
|
||||
position={panelState.position}
|
||||
flex={panelState.flex}
|
||||
collapsed={panelState.collapsed}
|
||||
onFlexChange={(f) =>
|
||||
panelLayout.setPanelFlex(panelState.id, f)
|
||||
}
|
||||
onToggleCollapse={() =>
|
||||
panelLayout.togglePanelCollapse(panelState.id)
|
||||
}
|
||||
isLast={isLast}
|
||||
>
|
||||
{renderPropertiesPanel(true)}
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</SidebarDropZone>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
|
||||
{/* Mobile Bottom Navigation */}
|
||||
|
||||
BIN
src/Apollinare.API/apollinare.db-shm
Normal file
BIN
src/Apollinare.API/apollinare.db-shm
Normal file
Binary file not shown.
BIN
src/Apollinare.API/apollinare.db-wal
Normal file
BIN
src/Apollinare.API/apollinare.db-wal
Normal file
Binary file not shown.
Reference in New Issue
Block a user