diff --git a/CLAUDE.md b/CLAUDE.md index b80dd09..f5c4ace 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`): diff --git a/frontend/src/components/reportEditor/EditorCanvas.tsx b/frontend/src/components/reportEditor/EditorCanvas.tsx index 62d5a54..e681d47 100644 --- a/frontend/src/components/reportEditor/EditorCanvas.tsx +++ b/frontend/src/components/reportEditor/EditorCanvas.tsx @@ -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) => void; /** Called when a drag/resize operation completes - use to commit to history */ onUpdateElementComplete?: () => void; @@ -78,7 +78,7 @@ const EditorCanvas = forwardRef( ( { template, - selectedElementId, + selectedElementIds, onSelectElement, onUpdateElement, onUpdateElementComplete, @@ -106,6 +106,28 @@ const EditorCanvas = forwardRef( const loadingImagesRef = useRef>(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(null); + + // Track selected objects for multi-move + const multiSelectedObjectsRef = useRef([]); + 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( // 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 & { target?: fabric.FabricObject }) => { @@ -661,10 +714,101 @@ const EditorCanvas = forwardRef( [], ); + // 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( 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( 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( 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( 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( }); }, [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 ( (defaultPage.id); - // Editor state - const [selectedElementId, setSelectedElementId] = useState( - null, - ); + // Editor state - support multiple selection + const [selectedElementIds, setSelectedElementIds] = useState([]); const [zoom, setZoom] = useState(isMobile ? 0.5 : 1); const [showGrid, setShowGrid] = useState(true); const [snapOptions, setSnapOptions] = useState({ @@ -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 diff --git a/src/Apollinare.API/apollinare.db-shm b/src/Apollinare.API/apollinare.db-shm index 792bf50..3584c8c 100644 Binary files a/src/Apollinare.API/apollinare.db-shm and b/src/Apollinare.API/apollinare.db-shm differ diff --git a/src/Apollinare.API/apollinare.db-wal b/src/Apollinare.API/apollinare.db-wal index d39da59..8ca6b33 100644 Binary files a/src/Apollinare.API/apollinare.db-wal and b/src/Apollinare.API/apollinare.db-wal differ