-
This commit is contained in:
35
CLAUDE.md
35
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`):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user