This commit is contained in:
2025-11-29 01:36:07 +01:00
parent d932b832c1
commit 4fa9c97189
5 changed files with 427 additions and 59 deletions

View File

@@ -46,12 +46,25 @@ XX. **Nome Problema (FIX/IMPLEMENTATO DATA):** - **Problema:** Descrizione breve
## Quick Start - Session Recovery
**Ultima sessione:** 28 Novembre 2025 (notte)
**Ultima sessione:** 29 Novembre 2025
**Stato progetto:** Migrazione Oracle APEX → .NET + React TypeScript in corso
**Lavoro completato nell'ultima sessione:**
- **NUOVA FEATURE: Selezione Multipla nel Report Editor** - COMPLETATO
- Implementato sistema di selezione multipla personalizzato (senza usare ActiveSelection di Fabric.js che causava riposizionamento oggetti)
- **Selezione con rettangolo di trascinamento**: trascinando sul canvas vuoto appare rettangolo blu tratteggiato, al rilascio seleziona tutti gli oggetti che intersecano
- **Shift+click**: aggiunge/rimuove oggetti dalla selezione
- **Spostamento multiplo**: quando più oggetti sono selezionati, trascinandone uno si spostano tutti insieme
- **Feedback visivo**: oggetti selezionati mostrano bordo blu (#1976d2) e ombra
- **Gestione corretta degli eventi**: i ref (`selectedElementIdsRef`, `onSelectElementRef`, etc.) evitano stale closures negli event handler
- **File modificati:**
- `EditorCanvas.tsx` - Nuovi handler `handleMouseDown`, `handleMouseUp`, logica selezione multipla, refs per valori correnti
- `ReportEditorPage.tsx` - Cambiato `selectedElementId: string | null``selectedElementIds: string[]`, aggiornati tutti i riferimenti
**Lavoro completato nelle sessioni precedenti (28 Novembre 2025 notte):**
- **NUOVA FEATURE: Sincronizzazione Real-Time Efficiente** - COMPLETATO
- **Prima:** Al salvataggio veniva inviata solo una notifica `DataSaved`, l'altra sessione ricaricava il template dal server (lento)
- **Dopo:** Al salvataggio viene inviato l'intero template via SignalR (`BroadcastTemplateSync`), l'altra sessione lo applica direttamente (istantaneo)
@@ -1141,6 +1154,26 @@ frontend/src/
- Check `isPending` spostato dentro il callback del setTimeout
- **File:** `ReportEditorPage.tsx`
26. **Selezione Multipla Fabric.js - Riposizionamento Oggetti (FIX 29/11/2025):**
- **Problema:** Usando `ActiveSelection` di Fabric.js per la selezione multipla, gli oggetti venivano riposizionati/spostati quando selezionati
- **Causa:** `ActiveSelection` raggruppa gli oggetti e le loro coordinate diventano relative al centro del gruppo. Inoltre, ricreando l'`ActiveSelection` nell'effect quando cambiava `selectedElementIds`, gli oggetti venivano spostati
- **Soluzione:** Sistema di selezione multipla completamente personalizzato:
- Disabilitata selezione nativa di Fabric.js (`selection: false` nel canvas)
- Implementato `handleMouseDown` per:
- Click su oggetto → selezione singola
- Shift+click → aggiunge/rimuove dalla selezione
- Click su canvas vuoto → inizio rettangolo di selezione
- Click su oggetto già selezionato (multi) → inizio drag multiplo
- Implementato `handleMouseMove` per:
- Disegno rettangolo di selezione (Rect blu tratteggiato)
- Spostamento multiplo oggetti (aggiorna `left`/`top` di ogni oggetto)
- Implementato `handleMouseUp` per:
- Fine rettangolo → calcola intersezione e seleziona oggetti
- Fine drag multiplo → aggiorna template con nuove posizioni
- Feedback visivo: bordo blu e ombra sugli oggetti selezionati (invece di ActiveSelection)
- Usati refs (`selectedElementIdsRef`, `onSelectElementRef`, etc.) per evitare stale closures negli event handler registrati una sola volta
- **File:** `EditorCanvas.tsx`, `ReportEditorPage.tsx`
### Schema Database Report System
Le tabelle sono già nel DbContext (`AppollinareDbContext.cs`):

View File

@@ -43,8 +43,8 @@ export interface RemoteCursor {
interface EditorCanvasProps {
template: AprtTemplate;
selectedElementId: string | null;
onSelectElement: (id: string | null) => void;
selectedElementIds: string[];
onSelectElement: (ids: string[]) => void;
onUpdateElement: (id: string, updates: Partial<AprtElement>) => void;
/** Called when a drag/resize operation completes - use to commit to history */
onUpdateElementComplete?: () => void;
@@ -78,7 +78,7 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
(
{
template,
selectedElementId,
selectedElementIds,
onSelectElement,
onUpdateElement,
onUpdateElementComplete,
@@ -106,6 +106,28 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
const loadingImagesRef = useRef<Set<string>>(new Set()); // elementIds currently being loaded
const templateElementsRef = useRef(template.elements); // Ref for accessing current elements in callbacks
// Custom multi-selection state
const isDrawingSelectionRef = useRef(false);
const selectionStartRef = useRef<{ x: number; y: number } | null>(null);
const selectionRectRef = useRef<fabric.Rect | null>(null);
// Track selected objects for multi-move
const multiSelectedObjectsRef = useRef<FabricObjectWithData[]>([]);
const isDraggingMultiRef = useRef(false);
const dragStartPosRef = useRef<{ x: number; y: number } | null>(null);
// Refs to access current values in event handlers (avoids stale closures)
const selectedElementIdsRef = useRef(selectedElementIds);
selectedElementIdsRef.current = selectedElementIds;
const onSelectElementRef = useRef(onSelectElement);
onSelectElementRef.current = onSelectElement;
const onUpdateElementRef = useRef(onUpdateElement);
onUpdateElementRef.current = onUpdateElement;
const onUpdateElementCompleteRef = useRef(onUpdateElementComplete);
onUpdateElementCompleteRef.current = onUpdateElementComplete;
const zoomRef = useRef(zoom);
zoomRef.current = zoom;
// Keep templateElementsRef in sync
templateElementsRef.current = template.elements;
@@ -167,18 +189,49 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
// Event handlers
const handleSelectionCreated = useCallback(
(e: { selected: fabric.FabricObject[] }) => {
const selected = e.selected?.[0] as FabricObjectWithData | undefined;
if (selected?.data?.id) {
onSelectElement(selected.data.id as string);
// Ignore if we're handling multi-selection manually
if (isDraggingMultiRef.current || isDrawingSelectionRef.current) {
return;
}
// Ignore if we already have multi-selection (don't let Fabric override it)
if (selectedElementIdsRef.current.length > 1) {
return;
}
const selectedIds: string[] = [];
if (e.selected) {
for (const obj of e.selected) {
const objData = (obj as FabricObjectWithData).data;
if (
objData?.id &&
!objData.isGrid &&
!objData.isMargin &&
!objData.isGuide
) {
selectedIds.push(objData.id as string);
}
}
}
if (selectedIds.length > 0) {
onSelectElementRef.current(selectedIds);
}
},
[onSelectElement],
[],
);
const handleSelectionCleared = useCallback(() => {
onSelectElement(null);
// Ignore if we're handling multi-selection manually
if (isDraggingMultiRef.current || isDrawingSelectionRef.current) {
return;
}
// Ignore if we have multi-selection (don't let Fabric clear it)
if (selectedElementIdsRef.current.length > 1) {
return;
}
onSelectElementRef.current([]);
clearGuideLines();
}, [onSelectElement, clearGuideLines]);
}, [clearGuideLines]);
const handleObjectMoving = useCallback(
(e: fabric.TEvent<MouseEvent> & { target?: fabric.FabricObject }) => {
@@ -661,10 +714,101 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
[],
);
// Custom multi-selection: mouse down handler
const handleMouseDown = useCallback(
(e: { e: MouseEvent; target?: fabric.FabricObject }) => {
if (!fabricRef.current) return;
const canvas = fabricRef.current;
const pointer = canvas.getScenePoint(e.e);
// If clicking on an object, handle normal selection or multi-drag
if (e.target) {
const objData = (e.target as FabricObjectWithData).data;
if (
objData?.id &&
!objData.isGrid &&
!objData.isMargin &&
!objData.isGuide
) {
const currentSelectedIds = selectedElementIdsRef.current;
// Check if this object is already in multi-selection
if (
currentSelectedIds.length > 1 &&
currentSelectedIds.includes(objData.id as string)
) {
// Start dragging all selected objects
isDraggingMultiRef.current = true;
dragStartPosRef.current = { x: pointer.x, y: pointer.y };
// Store references to all selected objects
multiSelectedObjectsRef.current = [];
for (const id of currentSelectedIds) {
const obj = elementsMapRef.current.get(id);
if (obj) {
multiSelectedObjectsRef.current.push(obj);
}
}
return;
}
// Shift+click to add/remove from selection
if (e.e.shiftKey) {
if (currentSelectedIds.includes(objData.id as string)) {
// Remove from selection
onSelectElementRef.current(
currentSelectedIds.filter((id) => id !== objData.id),
);
} else {
// Add to selection
onSelectElementRef.current([
...currentSelectedIds,
objData.id as string,
]);
}
return;
}
// Normal click on object - select just this one
onSelectElementRef.current([objData.id as string]);
}
return;
}
// Clicking on empty canvas - clear selection and start drawing selection rectangle
onSelectElementRef.current([]);
isDrawingSelectionRef.current = true;
selectionStartRef.current = { x: pointer.x, y: pointer.y };
// Create selection rectangle
const selectionRect = new fabric.Rect({
left: pointer.x,
top: pointer.y,
width: 0,
height: 0,
fill: "rgba(25, 118, 210, 0.1)",
stroke: "#1976d2",
strokeWidth: 1,
strokeDashArray: [4, 4],
selectable: false,
evented: false,
excludeFromExport: true,
});
(selectionRect as FabricObjectWithData).data = {
isSelectionRect: true,
};
selectionRectRef.current = selectionRect;
canvas.add(selectionRect);
canvas.renderAll();
},
[], // No dependencies - uses refs
);
const handleMouseMove = useCallback(
(e: { e: MouseEvent }) => {
if (!fabricRef.current) return;
const pointer = fabricRef.current.getScenePoint(e.e);
const canvas = fabricRef.current;
const pointer = canvas.getScenePoint(e.e);
const xMm = Math.round((pxToMm(pointer.x) / zoom) * 10) / 10;
const yMm = Math.round((pxToMm(pointer.y) / zoom) * 10) / 10;
setCursorPosition({ x: xMm, y: yMm });
@@ -673,10 +817,139 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
if (onCursorMove) {
onCursorMove(xMm, yMm);
}
// Handle multi-drag
if (isDraggingMultiRef.current && dragStartPosRef.current) {
const dx = pointer.x - dragStartPosRef.current.x;
const dy = pointer.y - dragStartPosRef.current.y;
for (const obj of multiSelectedObjectsRef.current) {
obj.set({
left: (obj.left || 0) + dx,
top: (obj.top || 0) + dy,
});
obj.setCoords();
}
dragStartPosRef.current = { x: pointer.x, y: pointer.y };
canvas.renderAll();
return;
}
// Handle selection rectangle drawing
if (
isDrawingSelectionRef.current &&
selectionStartRef.current &&
selectionRectRef.current
) {
const startX = selectionStartRef.current.x;
const startY = selectionStartRef.current.y;
const left = Math.min(startX, pointer.x);
const top = Math.min(startY, pointer.y);
const width = Math.abs(pointer.x - startX);
const height = Math.abs(pointer.y - startY);
selectionRectRef.current.set({
left,
top,
width,
height,
});
canvas.renderAll();
}
},
[zoom, onCursorMove],
);
// Custom multi-selection: mouse up handler
const handleMouseUp = useCallback(() => {
if (!fabricRef.current) return;
const canvas = fabricRef.current;
// Handle end of multi-drag
if (isDraggingMultiRef.current) {
isDraggingMultiRef.current = false;
dragStartPosRef.current = null;
// Update all moved objects in the template
for (const obj of multiSelectedObjectsRef.current) {
const objData = obj.data;
if (objData?.id) {
const scaleX = obj.scaleX || 1;
const scaleY = obj.scaleY || 1;
const finalWidth = (obj.width || 0) * scaleX;
const finalHeight = (obj.height || 0) * scaleY;
onUpdateElementRef.current(objData.id as string, {
position: {
x: pxToMm(obj.left || 0) / zoomRef.current,
y: pxToMm(obj.top || 0) / zoomRef.current,
width: pxToMm(finalWidth) / zoomRef.current,
height: pxToMm(finalHeight) / zoomRef.current,
rotation: obj.angle || 0,
},
});
}
}
// Commit to history
onUpdateElementCompleteRef.current?.();
multiSelectedObjectsRef.current = [];
return;
}
// Handle end of selection rectangle
if (isDrawingSelectionRef.current && selectionRectRef.current) {
const selRect = selectionRectRef.current;
const rectLeft = selRect.left || 0;
const rectTop = selRect.top || 0;
const rectRight = rectLeft + (selRect.width || 0);
const rectBottom = rectTop + (selRect.height || 0);
// Find all objects within the selection rectangle
const selectedIds: string[] = [];
const objects = canvas.getObjects();
for (const obj of objects) {
const objData = (obj as FabricObjectWithData).data;
if (
objData?.id &&
!objData.isGrid &&
!objData.isMargin &&
!objData.isGuide &&
!objData.isSelectionRect
) {
const objLeft = obj.left || 0;
const objTop = obj.top || 0;
const objRight = objLeft + (obj.width || 0) * (obj.scaleX || 1);
const objBottom = objTop + (obj.height || 0) * (obj.scaleY || 1);
// Check if object intersects with selection rectangle
if (
objLeft < rectRight &&
objRight > rectLeft &&
objTop < rectBottom &&
objBottom > rectTop
) {
selectedIds.push(objData.id as string);
}
}
}
// Remove selection rectangle
canvas.remove(selRect);
selectionRectRef.current = null;
isDrawingSelectionRef.current = false;
selectionStartRef.current = null;
// Update selection
onSelectElementRef.current(selectedIds);
canvas.renderAll();
}
}, []); // No dependencies - uses refs
// Keyboard navigation
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@@ -741,7 +1014,7 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
width: canvasWidth * zoom,
height: canvasHeight * zoom,
backgroundColor: "#ffffff",
selection: true,
selection: false, // Disable native multi-selection (causes object repositioning issues)
preserveObjectStacking: true,
uniformScaling: false, // Allow free resize from corners (not locked to aspect ratio)
});
@@ -770,6 +1043,8 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
canvas.on("mouse:over", handleMouseOver as any);
canvas.on("mouse:out", handleMouseOut as any);
canvas.on("mouse:move", handleMouseMove as any);
canvas.on("mouse:down", handleMouseDown as any);
canvas.on("mouse:up", handleMouseUp as any);
canvas.on("text:changed", handleTextChanged as any);
window.addEventListener("keydown", handleKeyDown);
@@ -784,6 +1059,8 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
canvas.off("mouse:over");
canvas.off("mouse:out");
canvas.off("mouse:move");
canvas.off("mouse:down");
canvas.off("mouse:up");
canvas.off("text:changed");
window.removeEventListener("keydown", handleKeyDown);
canvas.dispose();
@@ -1158,21 +1435,79 @@ const EditorCanvas = forwardRef<EditorCanvasRef, EditorCanvasProps>(
});
}, [template.elements, zoom]);
// Update selection when selectedElementId changes externally
// Update visual selection feedback when selectedElementIds changes
useEffect(() => {
if (!fabricRef.current) return;
if (selectedElementId) {
const obj = elementsMapRef.current.get(selectedElementId);
if (obj && fabricRef.current.getActiveObject() !== obj) {
fabricRef.current.setActiveObject(obj);
fabricRef.current.renderAll();
const canvas = fabricRef.current;
// Clear all selection styling first
const allObjects = canvas.getObjects();
for (const obj of allObjects) {
const objData = (obj as FabricObjectWithData).data;
if (
objData?.id &&
!objData.isGrid &&
!objData.isMargin &&
!objData.isGuide &&
!objData.isSelectionRect
) {
// Reset to default styling
obj.set({
strokeWidth:
obj.type === "line"
? obj.strokeWidth || 1
: (objData.originalStrokeWidth ?? 0),
stroke:
objData.originalStroke ?? (obj.type === "line" ? obj.stroke : ""),
shadow: undefined,
});
}
}
if (selectedElementIds.length === 0) {
canvas.discardActiveObject();
canvas.renderAll();
return;
}
if (selectedElementIds.length === 1) {
// Single selection - use Fabric's native selection (with controls)
const obj = elementsMapRef.current.get(selectedElementIds[0]);
if (obj && canvas.getActiveObject() !== obj) {
canvas.setActiveObject(obj);
}
} else {
fabricRef.current.discardActiveObject();
fabricRef.current.renderAll();
// Multi-selection - show visual feedback on each selected object
canvas.discardActiveObject(); // Clear native selection
for (const id of selectedElementIds) {
const obj = elementsMapRef.current.get(id);
if (obj) {
const objData = obj.data;
// Store original values if not already stored
if (objData && objData.originalStrokeWidth === undefined) {
objData.originalStrokeWidth = obj.strokeWidth || 0;
objData.originalStroke = obj.stroke || "";
}
// Apply selection styling - blue border and shadow
obj.set({
stroke: "#1976d2",
strokeWidth: 2,
shadow: new fabric.Shadow({
color: "rgba(25, 118, 210, 0.4)",
blur: 10,
offsetX: 0,
offsetY: 0,
}),
});
}
}
}
}, [selectedElementId]);
canvas.renderAll();
}, [selectedElementIds]);
return (
<Box

View File

@@ -127,10 +127,8 @@ export default function ReportEditorPage() {
// Page state - track which page is currently being edited
const [currentPageId, setCurrentPageId] = useState<string>(defaultPage.id);
// Editor state
const [selectedElementId, setSelectedElementId] = useState<string | null>(
null,
);
// Editor state - support multiple selection
const [selectedElementIds, setSelectedElementIds] = useState<string[]>([]);
const [zoom, setZoom] = useState(isMobile ? 0.5 : 1);
const [showGrid, setShowGrid] = useState(true);
const [snapOptions, setSnapOptions] = useState<SnapOptions>({
@@ -256,8 +254,10 @@ export default function ReportEditorPage() {
elements: prev.elements.filter((el) => el.id !== message.itemId),
}));
// Clear selection if deleted element was selected
if (selectedElementId === message.itemId) {
setSelectedElementId(null);
if (selectedElementIds.includes(message.itemId)) {
setSelectedElementIds((prev) =>
prev.filter((id) => id !== message.itemId),
);
}
setTimeout(() => {
isApplyingRemoteChange.current = false;
@@ -396,18 +396,18 @@ export default function ReportEditorPage() {
}, [
collaboration,
historyActions,
selectedElementId,
selectedElementIds,
template,
queryClient,
id,
]);
// Send selection changes to collaborators
// Send selection changes to collaborators (send first selected element for compatibility)
useEffect(() => {
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
collaboration.sendSelectionChanged(selectedElementId);
collaboration.sendSelectionChanged(selectedElementIds[0] || null);
}
}, [collaboration, selectedElementId]);
}, [collaboration, selectedElementIds]);
// Send view/page navigation to collaborators
useEffect(() => {
@@ -629,10 +629,14 @@ export default function ReportEditorPage() {
historyActions.redo();
}, [historyActions]);
// Get selected element
// Get selected element(s) - for single selection compatibility, use first selected
const selectedElementId = selectedElementIds[0] || null;
const selectedElement = selectedElementId
? template.elements.find((e) => e.id === selectedElementId)
: null;
const selectedElements = selectedElementIds
.map((id) => template.elements.find((e) => e.id === id))
.filter((e): e is AprtElement => e !== undefined);
// Dataset management
const handleAddDataset = useCallback((dataset: DatasetTypeDto) => {
@@ -682,7 +686,7 @@ export default function ReportEditorPage() {
// Switch to the new page
setCurrentPageId(newPageId);
setSelectedElementId(null);
setSelectedElementIds([]);
}, [template.pages.length, historyActions, collaboration]);
// Duplicate page with all its elements
@@ -717,7 +721,7 @@ export default function ReportEditorPage() {
// Switch to the new page
setCurrentPageId(newPageId);
setSelectedElementId(null);
setSelectedElementIds([]);
},
[template.pages, template.elements, historyActions],
);
@@ -749,7 +753,7 @@ export default function ReportEditorPage() {
if (newCurrentPage) {
setCurrentPageId(newCurrentPage.id);
}
setSelectedElementId(null);
setSelectedElementIds([]);
},
[template.pages, historyActions, collaboration],
);
@@ -796,7 +800,7 @@ export default function ReportEditorPage() {
// Select page
const handleSelectPage = useCallback((pageId: string) => {
setCurrentPageId(pageId);
setSelectedElementId(null); // Clear selection when switching pages
setSelectedElementIds([]); // Clear selection when switching pages
}, []);
// ============ END PAGE MANAGEMENT HANDLERS ============
@@ -851,7 +855,7 @@ export default function ReportEditorPage() {
...prev,
elements: [...prev.elements, newElement],
}));
setSelectedElementId(newElement.id);
setSelectedElementIds([newElement.id]);
// Send to collaborators
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
@@ -958,7 +962,7 @@ export default function ReportEditorPage() {
...prev,
elements: [...prev.elements, newElement],
}));
setSelectedElementId(newElement.id);
setSelectedElementIds([newElement.id]);
},
[
selectedElementId,
@@ -991,7 +995,7 @@ export default function ReportEditorPage() {
...prev,
elements: prev.elements.filter((el) => el.id !== selectedElementId),
}));
setSelectedElementId(null);
setSelectedElementIds([]);
}, [selectedElementId, historyActions, collaboration]);
// Copy element
@@ -1011,7 +1015,7 @@ export default function ReportEditorPage() {
...prev,
elements: [...prev.elements, copy],
}));
setSelectedElementId(copy.id);
setSelectedElementIds([copy.id]);
}, [selectedElement, historyActions]);
// Toggle lock
@@ -1066,7 +1070,7 @@ export default function ReportEditorPage() {
// Handle context menu event from canvas
const handleContextMenu = useCallback((event: ContextMenuEvent) => {
if (event.elementId) {
setSelectedElementId(event.elementId);
setSelectedElementIds(event.elementId ? [event.elementId] : []);
}
setContextMenu({
open: true,
@@ -1113,7 +1117,7 @@ export default function ReportEditorPage() {
...prev,
elements: [...prev.elements, pastedElement],
}));
setSelectedElementId(pastedElement.id);
setSelectedElementIds([pastedElement.id]);
}, [clipboard, historyActions]);
// Duplicate element
@@ -1366,19 +1370,15 @@ export default function ReportEditorPage() {
// Selection operations
const handleSelectAll = useCallback(() => {
// For now, just show a message - multi-selection requires more work
if (template.elements.length > 0) {
setSelectedElementId(template.elements[template.elements.length - 1].id);
setSnackbar({
open: true,
message: "Selezione multipla non ancora implementata",
severity: "error",
});
// Select all elements on the current page
const pageElementIds = currentPageElements.map((e) => e.id);
if (pageElementIds.length > 0) {
setSelectedElementIds(pageElementIds);
}
}, [template.elements]);
}, [currentPageElements]);
const handleDeselectAll = useCallback(() => {
setSelectedElementId(null);
setSelectedElementIds([]);
}, []);
// Toggle visibility
@@ -1704,13 +1704,13 @@ export default function ReportEditorPage() {
onPrevPage={() => {
if (currentPageIndex > 0) {
setCurrentPageId(template.pages[currentPageIndex - 1].id);
setSelectedElementId(null);
setSelectedElementIds([]);
}
}}
onNextPage={() => {
if (currentPageIndex < template.pages.length - 1) {
setCurrentPageId(template.pages[currentPageIndex + 1].id);
setSelectedElementId(null);
setSelectedElementIds([]);
}
}}
// New props for enhanced toolbar
@@ -1764,11 +1764,11 @@ export default function ReportEditorPage() {
margins: currentPage?.margins || template.meta.margins,
},
}}
selectedElementId={selectedElementId}
onSelectElement={(id) => {
setSelectedElementId(id);
selectedElementIds={selectedElementIds}
onSelectElement={(ids) => {
setSelectedElementIds(ids);
// On mobile, auto-open properties when selecting element
if (isMobile && id) {
if (isMobile && ids.length > 0) {
setMobilePanel("properties");
}
}}
@@ -1973,7 +1973,7 @@ export default function ReportEditorPage() {
open={contextMenu.open}
position={contextMenu.position}
selectedElement={selectedElement || null}
selectedElements={selectedElement ? [selectedElement] : []}
selectedElements={selectedElements}
hasClipboard={clipboard !== null}
onClose={closeContextMenu}
// Clipboard actions

Binary file not shown.

Binary file not shown.