This commit is contained in:
2025-11-29 02:22:43 +01:00
parent 53c366c20e
commit dc6f223fd9
10 changed files with 967 additions and 115 deletions

View File

@@ -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 */}