-
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useHistory } from "../hooks/useHistory";
|
||||
import { usePanelLayout } from "../hooks/usePanelLayout";
|
||||
import { useCollaborationRoom } from "../contexts/CollaborationContext";
|
||||
import type {
|
||||
DataChangeMessage,
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
Settings as SettingsIcon,
|
||||
Description as PageIcon,
|
||||
Close as CloseIcon,
|
||||
Layers as LayersIcon,
|
||||
} from "@mui/icons-material";
|
||||
import EditorCanvas, {
|
||||
type ContextMenuEvent,
|
||||
@@ -61,6 +63,7 @@ import ImageUploadDialog, {
|
||||
type ImageData,
|
||||
} from "../components/reportEditor/ImageUploadDialog";
|
||||
import PageNavigator from "../components/reportEditor/PageNavigator";
|
||||
import ResizablePanel from "../components/reportEditor/ResizablePanel";
|
||||
import {
|
||||
reportTemplateService,
|
||||
reportFontService,
|
||||
@@ -101,7 +104,6 @@ export default function ReportEditorPage() {
|
||||
// Responsive breakpoints
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // < 600px
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between("sm", "lg")); // 600-1200px
|
||||
const isDesktop = useMediaQuery(theme.breakpoints.up("lg")); // > 1200px
|
||||
|
||||
// Template state with robust undo/redo (100 states history)
|
||||
const [templateHistory, historyActions] = useHistory<AprtTemplate>(
|
||||
@@ -144,6 +146,9 @@ export default function ReportEditorPage() {
|
||||
// Mobile panel state
|
||||
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
|
||||
|
||||
// Panel layout configuration (persisted to localStorage)
|
||||
const panelLayout = usePanelLayout();
|
||||
|
||||
// UI state
|
||||
const [saveDialog, setSaveDialog] = useState(false);
|
||||
const [previewDialog, setPreviewDialog] = useState(false);
|
||||
@@ -1756,7 +1761,8 @@ export default function ReportEditorPage() {
|
||||
}
|
||||
|
||||
// Render panels based on screen size
|
||||
const renderPageNavigator = () => (
|
||||
// embedded=true when used inside CollapsiblePanel (removes internal borders/headers)
|
||||
const renderPageNavigator = (embedded = false) => (
|
||||
<PageNavigator
|
||||
pages={template.pages}
|
||||
elements={template.elements}
|
||||
@@ -1767,19 +1773,21 @@ export default function ReportEditorPage() {
|
||||
onDeletePage={handleDeletePage}
|
||||
onRenamePage={handleRenamePage}
|
||||
onMovePage={handleMovePage}
|
||||
embedded={embedded}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderDataBindingPanel = () => (
|
||||
const renderDataBindingPanel = (embedded = false) => (
|
||||
<DataBindingPanel
|
||||
schemas={schemas}
|
||||
selectedDatasets={selectedDatasets}
|
||||
onInsertBinding={handleInsertBinding}
|
||||
onRemoveDataset={handleRemoveDataset}
|
||||
embedded={embedded}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPropertiesPanel = () => (
|
||||
const renderPropertiesPanel = (embedded = false) => (
|
||||
<PropertiesPanel
|
||||
element={selectedElement || null}
|
||||
onUpdateElement={handleUpdateSelectedElement}
|
||||
@@ -1805,6 +1813,7 @@ export default function ReportEditorPage() {
|
||||
),
|
||||
}));
|
||||
}}
|
||||
embedded={embedded}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1891,66 +1900,222 @@ export default function ReportEditorPage() {
|
||||
|
||||
{/* Main Editor Area */}
|
||||
<Box sx={{ display: "flex", flex: 1, overflow: "hidden" }}>
|
||||
{/* Desktop: Show all panels */}
|
||||
{isDesktop && (
|
||||
<>
|
||||
{renderPageNavigator()}
|
||||
{renderDataBindingPanel()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tablet: Show page navigator and data panel in collapsible sidebars */}
|
||||
{isTablet && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 180,
|
||||
borderRight: 1,
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{renderPageNavigator()}
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Canvas - show only elements for current page */}
|
||||
<EditorCanvas
|
||||
ref={canvasRef}
|
||||
template={{
|
||||
...template,
|
||||
elements: currentPageElements,
|
||||
// Use current page settings if available, otherwise template defaults
|
||||
meta: {
|
||||
...template.meta,
|
||||
pageSize:
|
||||
(currentPage?.pageSize as PageSize) || template.meta.pageSize,
|
||||
orientation:
|
||||
(currentPage?.orientation as PageOrientation) ||
|
||||
template.meta.orientation,
|
||||
margins: currentPage?.margins || template.meta.margins,
|
||||
},
|
||||
{/* Center Area - Canvas Container (flex: 1 to take remaining space, centers the canvas) */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
overflow: "auto",
|
||||
bgcolor: (theme) =>
|
||||
theme.palette.mode === "dark" ? "#1a1a1a" : "#e0e0e0",
|
||||
position: "relative",
|
||||
}}
|
||||
selectedElementIds={selectedElementIds}
|
||||
onSelectElement={(ids) => {
|
||||
setSelectedElementIds(ids);
|
||||
// On mobile, auto-open properties when selecting element
|
||||
if (isMobile && ids.length > 0) {
|
||||
setMobilePanel("properties");
|
||||
}
|
||||
}}
|
||||
onUpdateElement={handleUpdateElementWithoutHistory}
|
||||
onUpdateElementComplete={historyActions.commit}
|
||||
zoom={zoom}
|
||||
showGrid={showGrid}
|
||||
gridSize={gridSize}
|
||||
snapOptions={snapOptions}
|
||||
onContextMenu={handleContextMenu}
|
||||
/>
|
||||
>
|
||||
{/* Canvas - show only elements for current page */}
|
||||
<EditorCanvas
|
||||
ref={canvasRef}
|
||||
template={{
|
||||
...template,
|
||||
elements: currentPageElements,
|
||||
// Use current page settings if available, otherwise template defaults
|
||||
meta: {
|
||||
...template.meta,
|
||||
pageSize:
|
||||
(currentPage?.pageSize as PageSize) || template.meta.pageSize,
|
||||
orientation:
|
||||
(currentPage?.orientation as PageOrientation) ||
|
||||
template.meta.orientation,
|
||||
margins: currentPage?.margins || template.meta.margins,
|
||||
},
|
||||
}}
|
||||
selectedElementIds={selectedElementIds}
|
||||
onSelectElement={(ids) => {
|
||||
setSelectedElementIds(ids);
|
||||
// On mobile, auto-open properties when selecting element
|
||||
if (isMobile && ids.length > 0) {
|
||||
setMobilePanel("properties");
|
||||
}
|
||||
}}
|
||||
onUpdateElement={handleUpdateElementWithoutHistory}
|
||||
onUpdateElementComplete={historyActions.commit}
|
||||
zoom={zoom}
|
||||
showGrid={showGrid}
|
||||
gridSize={gridSize}
|
||||
snapOptions={snapOptions}
|
||||
onContextMenu={handleContextMenu}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Desktop/Tablet: Properties Panel */}
|
||||
{!isMobile && renderPropertiesPanel()}
|
||||
{/* 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>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Mobile Bottom Navigation */}
|
||||
|
||||
Reference in New Issue
Block a user