-
This commit is contained in:
@@ -23,7 +23,21 @@ import {
|
||||
MenuItem,
|
||||
Alert,
|
||||
Snackbar,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
IconButton,
|
||||
BottomNavigation,
|
||||
BottomNavigationAction,
|
||||
Paper,
|
||||
SwipeableDrawer,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Storage as DataIcon,
|
||||
Settings as SettingsIcon,
|
||||
Description as PageIcon,
|
||||
Close as CloseIcon,
|
||||
} from "@mui/icons-material";
|
||||
import EditorCanvas, {
|
||||
type ContextMenuEvent,
|
||||
} from "../components/reportEditor/EditorCanvas";
|
||||
@@ -67,11 +81,20 @@ import {
|
||||
defaultPage,
|
||||
} from "../types/report";
|
||||
|
||||
// Panel types for mobile navigation
|
||||
type MobilePanel = "pages" | "data" | "properties" | null;
|
||||
|
||||
export default function ReportEditorPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const isNew = !id;
|
||||
const theme = useTheme();
|
||||
|
||||
// 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>(
|
||||
@@ -102,7 +125,7 @@ export default function ReportEditorPage() {
|
||||
const [selectedElementId, setSelectedElementId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [zoom, setZoom] = useState(isMobile ? 0.5 : 1);
|
||||
const [showGrid, setShowGrid] = useState(true);
|
||||
const [snapOptions, setSnapOptions] = useState<SnapOptions>({
|
||||
grid: false,
|
||||
@@ -113,6 +136,9 @@ export default function ReportEditorPage() {
|
||||
});
|
||||
const [gridSize] = useState(5); // 5mm grid
|
||||
|
||||
// Mobile panel state
|
||||
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
|
||||
|
||||
// UI state
|
||||
const [saveDialog, setSaveDialog] = useState(false);
|
||||
const [previewDialog, setPreviewDialog] = useState(false);
|
||||
@@ -139,6 +165,24 @@ export default function ReportEditorPage() {
|
||||
});
|
||||
const [clipboard, setClipboard] = useState<AprtElement | null>(null);
|
||||
|
||||
// Track unsaved changes - reset when saved, set when modified
|
||||
const [lastSavedUndoCount, setLastSavedUndoCount] = useState(0);
|
||||
const hasUnsavedChanges = templateHistory.undoCount !== lastSavedUndoCount;
|
||||
|
||||
// Auto-save feature - enabled by default
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
||||
|
||||
// Update zoom on screen size change
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setZoom(0.5);
|
||||
} else if (isTablet) {
|
||||
setZoom(0.75);
|
||||
} else {
|
||||
setZoom(1);
|
||||
}
|
||||
}, [isMobile, isTablet]);
|
||||
|
||||
// Load existing template
|
||||
const { data: existingTemplate, isLoading: isLoadingTemplate } = useQuery({
|
||||
queryKey: ["report-template", id],
|
||||
@@ -301,6 +345,8 @@ export default function ReportEditorPage() {
|
||||
severity: "success",
|
||||
});
|
||||
setSaveDialog(false);
|
||||
// Mark current state as saved
|
||||
setLastSavedUndoCount(templateHistory.undoCount);
|
||||
if (isNew) {
|
||||
navigate(`/report-editor/${result.id}`, { replace: true });
|
||||
}
|
||||
@@ -531,8 +577,13 @@ export default function ReportEditorPage() {
|
||||
elements: [...prev.elements, newElement],
|
||||
}));
|
||||
setSelectedElementId(newElement.id);
|
||||
|
||||
// On mobile, open properties panel after adding element
|
||||
if (isMobile) {
|
||||
setMobilePanel("properties");
|
||||
}
|
||||
},
|
||||
[historyActions, currentPageId],
|
||||
[historyActions, currentPageId, isMobile],
|
||||
);
|
||||
|
||||
// Update element without history (for continuous updates like dragging)
|
||||
@@ -1176,6 +1227,31 @@ export default function ReportEditorPage() {
|
||||
selectedElementId,
|
||||
]);
|
||||
|
||||
// Auto-save effect - saves after 1 second of inactivity when there are unsaved changes
|
||||
useEffect(() => {
|
||||
if (
|
||||
!autoSaveEnabled ||
|
||||
!hasUnsavedChanges ||
|
||||
isNew ||
|
||||
saveMutation.isPending
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
saveMutation.mutate({ template, info: templateInfo });
|
||||
}, 1000); // 1 second debounce
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [
|
||||
autoSaveEnabled,
|
||||
hasUnsavedChanges,
|
||||
isNew,
|
||||
template,
|
||||
templateInfo,
|
||||
saveMutation,
|
||||
]);
|
||||
|
||||
if (isLoadingTemplate && id) {
|
||||
return (
|
||||
<Box
|
||||
@@ -1189,24 +1265,107 @@ export default function ReportEditorPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Render panels based on screen size
|
||||
const renderPageNavigator = () => (
|
||||
<PageNavigator
|
||||
pages={template.pages}
|
||||
elements={template.elements}
|
||||
currentPageId={currentPageId}
|
||||
onSelectPage={handleSelectPage}
|
||||
onAddPage={handleAddPage}
|
||||
onDuplicatePage={handleDuplicatePage}
|
||||
onDeletePage={handleDeletePage}
|
||||
onRenamePage={handleRenamePage}
|
||||
onMovePage={handleMovePage}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderDataBindingPanel = () => (
|
||||
<DataBindingPanel
|
||||
schemas={schemas}
|
||||
selectedDatasets={selectedDatasets}
|
||||
onInsertBinding={handleInsertBinding}
|
||||
onRemoveDataset={handleRemoveDataset}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPropertiesPanel = () => (
|
||||
<PropertiesPanel
|
||||
element={selectedElement || null}
|
||||
onUpdateElement={handleUpdateSelectedElement}
|
||||
pageSize={(currentPage?.pageSize as PageSize) || template.meta.pageSize}
|
||||
orientation={
|
||||
(currentPage?.orientation as PageOrientation) ||
|
||||
template.meta.orientation
|
||||
}
|
||||
margins={currentPage?.margins || template.meta.margins}
|
||||
onUpdatePage={handleUpdatePage}
|
||||
fontFamilies={fontFamilies}
|
||||
availableDatasets={availableDatasets}
|
||||
dataSchemas={dataSchemaMap}
|
||||
onOpenImageUpload={() => setImageUploadDialog(true)}
|
||||
// Page-specific props
|
||||
currentPage={currentPage}
|
||||
onUpdateCurrentPage={(updates) => {
|
||||
if (!currentPage) return;
|
||||
historyActions.set((prev) => ({
|
||||
...prev,
|
||||
pages: prev.pages.map((p) =>
|
||||
p.id === currentPage.id ? { ...p, ...updates } : p,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Mobile drawer content
|
||||
const renderMobileDrawerContent = () => {
|
||||
switch (mobilePanel) {
|
||||
case "pages":
|
||||
return renderPageNavigator();
|
||||
case "data":
|
||||
return renderDataBindingPanel();
|
||||
case "properties":
|
||||
return renderPropertiesPanel();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getMobilePanelTitle = () => {
|
||||
switch (mobilePanel) {
|
||||
case "pages":
|
||||
return "Pagine";
|
||||
case "data":
|
||||
return "Campi Dati";
|
||||
case "properties":
|
||||
return "Proprietà";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "calc(100vh - 64px)",
|
||||
mx: -3,
|
||||
mt: -3,
|
||||
height: { xs: "calc(100vh - 56px)", sm: "calc(100vh - 64px)" },
|
||||
mx: { xs: -1.5, sm: -2, md: -3 },
|
||||
mt: { xs: -1.5, sm: -2, md: -3 },
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Dataset Selector */}
|
||||
<DatasetSelector
|
||||
availableDatasets={availableDatasets}
|
||||
selectedDatasets={selectedDatasets}
|
||||
onAddDataset={handleAddDataset}
|
||||
onRemoveDataset={handleRemoveDataset}
|
||||
onOpenDatasetManager={() => setDatasetManagerDialog(true)}
|
||||
/>
|
||||
{/* Dataset Selector - hide on mobile, show in compact mode on tablet */}
|
||||
{!isMobile && (
|
||||
<DatasetSelector
|
||||
availableDatasets={availableDatasets}
|
||||
selectedDatasets={selectedDatasets}
|
||||
onAddDataset={handleAddDataset}
|
||||
onRemoveDataset={handleRemoveDataset}
|
||||
onOpenDatasetManager={() => setDatasetManagerDialog(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Toolbar */}
|
||||
<EditorToolbar
|
||||
@@ -1244,30 +1403,40 @@ export default function ReportEditorPage() {
|
||||
setSelectedElementId(null);
|
||||
}
|
||||
}}
|
||||
// New props for enhanced toolbar
|
||||
selectedElement={selectedElement}
|
||||
onUpdateSelectedElement={handleUpdateSelectedElement}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
// Auto-save props
|
||||
autoSaveEnabled={autoSaveEnabled}
|
||||
onAutoSaveToggle={setAutoSaveEnabled}
|
||||
/>
|
||||
|
||||
{/* Main Editor Area */}
|
||||
<Box sx={{ display: "flex", flex: 1, overflow: "hidden" }}>
|
||||
{/* Page Navigator */}
|
||||
<PageNavigator
|
||||
pages={template.pages}
|
||||
elements={template.elements}
|
||||
currentPageId={currentPageId}
|
||||
onSelectPage={handleSelectPage}
|
||||
onAddPage={handleAddPage}
|
||||
onDuplicatePage={handleDuplicatePage}
|
||||
onDeletePage={handleDeletePage}
|
||||
onRenamePage={handleRenamePage}
|
||||
onMovePage={handleMovePage}
|
||||
/>
|
||||
{/* Desktop: Show all panels */}
|
||||
{isDesktop && (
|
||||
<>
|
||||
{renderPageNavigator()}
|
||||
{renderDataBindingPanel()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Data Binding Panel */}
|
||||
<DataBindingPanel
|
||||
schemas={schemas}
|
||||
selectedDatasets={selectedDatasets}
|
||||
onInsertBinding={handleInsertBinding}
|
||||
onRemoveDataset={handleRemoveDataset}
|
||||
/>
|
||||
{/* 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()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Canvas - show only elements for current page */}
|
||||
<EditorCanvas
|
||||
@@ -1286,7 +1455,13 @@ export default function ReportEditorPage() {
|
||||
},
|
||||
}}
|
||||
selectedElementId={selectedElementId}
|
||||
onSelectElement={setSelectedElementId}
|
||||
onSelectElement={(id) => {
|
||||
setSelectedElementId(id);
|
||||
// On mobile, auto-open properties when selecting element
|
||||
if (isMobile && id) {
|
||||
setMobilePanel("properties");
|
||||
}
|
||||
}}
|
||||
onUpdateElement={handleUpdateElementWithoutHistory}
|
||||
onUpdateElementComplete={historyActions.commit}
|
||||
zoom={zoom}
|
||||
@@ -1296,43 +1471,103 @@ export default function ReportEditorPage() {
|
||||
onContextMenu={handleContextMenu}
|
||||
/>
|
||||
|
||||
{/* Properties Panel */}
|
||||
<PropertiesPanel
|
||||
element={selectedElement || null}
|
||||
onUpdateElement={handleUpdateSelectedElement}
|
||||
pageSize={
|
||||
(currentPage?.pageSize as PageSize) || template.meta.pageSize
|
||||
}
|
||||
orientation={
|
||||
(currentPage?.orientation as PageOrientation) ||
|
||||
template.meta.orientation
|
||||
}
|
||||
margins={currentPage?.margins || template.meta.margins}
|
||||
onUpdatePage={handleUpdatePage}
|
||||
fontFamilies={fontFamilies}
|
||||
availableDatasets={availableDatasets}
|
||||
dataSchemas={dataSchemaMap}
|
||||
onOpenImageUpload={() => setImageUploadDialog(true)}
|
||||
// Page-specific props
|
||||
currentPage={currentPage}
|
||||
onUpdateCurrentPage={(updates) => {
|
||||
if (!currentPage) return;
|
||||
historyActions.set((prev) => ({
|
||||
...prev,
|
||||
pages: prev.pages.map((p) =>
|
||||
p.id === currentPage.id ? { ...p, ...updates } : p,
|
||||
),
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{/* Desktop/Tablet: Properties Panel */}
|
||||
{!isMobile && renderPropertiesPanel()}
|
||||
</Box>
|
||||
|
||||
{/* Mobile Bottom Navigation */}
|
||||
{isMobile && (
|
||||
<Paper
|
||||
sx={{
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1100,
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<BottomNavigation
|
||||
value={mobilePanel}
|
||||
onChange={(_, newValue) => {
|
||||
setMobilePanel(newValue === mobilePanel ? null : newValue);
|
||||
}}
|
||||
showLabels
|
||||
>
|
||||
<BottomNavigationAction
|
||||
label="Pagine"
|
||||
value="pages"
|
||||
icon={<PageIcon />}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="Dati"
|
||||
value="data"
|
||||
icon={<DataIcon />}
|
||||
/>
|
||||
<BottomNavigationAction
|
||||
label="Proprietà"
|
||||
value="properties"
|
||||
icon={<SettingsIcon />}
|
||||
/>
|
||||
</BottomNavigation>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Mobile Panel Drawer */}
|
||||
<SwipeableDrawer
|
||||
anchor="bottom"
|
||||
open={isMobile && mobilePanel !== null}
|
||||
onClose={() => setMobilePanel(null)}
|
||||
onOpen={() => {}}
|
||||
disableSwipeToOpen
|
||||
PaperProps={{
|
||||
sx: {
|
||||
height: "70vh",
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{/* Drawer Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
p: 2,
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{getMobilePanelTitle()}</Typography>
|
||||
<IconButton onClick={() => setMobilePanel(null)}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Drawer Content */}
|
||||
<Box sx={{ flex: 1, overflow: "auto" }}>
|
||||
{renderMobileDrawerContent()}
|
||||
</Box>
|
||||
</Box>
|
||||
</SwipeableDrawer>
|
||||
|
||||
{/* Save Dialog for new templates */}
|
||||
<Dialog
|
||||
open={saveDialog}
|
||||
onClose={() => setSaveDialog(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
>
|
||||
<DialogTitle>Salva Template</DialogTitle>
|
||||
<DialogContent>
|
||||
@@ -1379,14 +1614,17 @@ export default function ReportEditorPage() {
|
||||
</FormControl>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setSaveDialog(false)}>Annulla</Button>
|
||||
<DialogActions sx={{ p: { xs: 2, sm: 1 } }}>
|
||||
<Button onClick={() => setSaveDialog(false)} fullWidth={isMobile}>
|
||||
Annulla
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
saveMutation.mutate({ template, info: templateInfo })
|
||||
}
|
||||
disabled={!templateInfo.nome || saveMutation.isPending}
|
||||
fullWidth={isMobile}
|
||||
>
|
||||
{saveMutation.isPending ? "Salvataggio..." : "Salva"}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user