import { useParams } from "react-router-dom"; import React, { useState, useRef, useEffect } from "react"; import { dataModelManager } from "./data/dataModel"; import ControlPanel from "./ControlPanel"; import SwapModal from "./SwapModal"; import { Block } from "../../types/exportedTypes"; import DataModelPanel from "./components/models/DataModelPanel"; import AnalyzerManager from "./AnalyzerManager"; import { useSceneContext } from "../../modules/scene/sceneContext"; import useModuleStore from "../../store/ui/useModuleStore"; import { usePlayButtonStore } from "../../store/ui/usePlayButtonStore"; import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize"; import { handleElementDragStart, handleElementResizeStart, handleBlockResizeStart, handleSwapStart, handleSwapTarget, handleBlockClick, handleElementClick } from "./functions/eventHandlers"; import BlockGrid from "./components/block/BlockGrid"; import BlockEditor from "./components/block/BlockEditor"; import ElementEditor from "./components/element/ElementEditor"; import useCallBackOnKey from "../../utils/hooks/useCallBackOnKey"; import { handleBlockDragStart } from "./functions/block/handleBlockDragStart"; import { getDashBoardBlocksApi } from "../../services/visulization/dashBoard/getDashBoardBlocks"; import { upsetDashBoardBlocksApi } from "../../services/visulization/dashBoard/upsertDashBoardBlocks"; import { deleteDashBoardBlocksApi } from "../../services/visulization/dashBoard/deleteDashBoardBlocks"; import { deleteDashBoardElementsApi } from "../../services/visulization/dashBoard/deleteDashBoardElements"; const DashboardEditor: React.FC = () => { const { simulationDashBoardStore, versionStore } = useSceneContext(); const { selectedVersion } = versionStore(); const { projectId } = useParams(); const { activeModule } = useModuleStore(); const { isPlaying } = usePlayButtonStore(); const { blocks, selectedBlock, setSelectedBlock, selectedElement, setSelectedElement, addBlock, setBlocks, removeBlock, removeElement, getBlockById, updateBlock, // Peek methods peekAddBlock, peekRemoveBlock, peekAddElement, peekRemoveElement, peekUpdateBlockStyle, peekUpdateBlockSize, peekUpdateBlockPosition, peekUpdateBlockPositionType, peekUpdateBlockZIndex, peekUpdateElementStyle, peekUpdateElementSize, peekUpdateElementPosition, peekUpdateElementPositionType, peekUpdateElementZIndex, peekUpdateElementData, peekUpdateGraphData, peekUpdateGraphTitle, peekUpdateGraphType, peekUpdateDataType, peekUpdateCommonValue, peekUpdateDataValue, peekUpdateDataSource, peekSwapElements, } = simulationDashBoardStore(); const [editMode, setEditMode] = useState(false); const [draggingElement, setDraggingElement] = useState(null); const [resizingElement, setResizingElement] = useState(null); const [resizingBlock, setResizingBlock] = useState(null); const [resizeStart, setResizeStart] = useState<{ x: number; y: number; width: number; height: number; } | null>(null); const [showSwapUI, setShowSwapUI] = useState(false); const [swapSource, setSwapSource] = useState(null); const [dataModel, setDataModel] = useState(dataModelManager.getDataSnapshot()); const [showDataModelPanel, setShowDataModelPanel] = useState(false); const [showElementDropdown, setShowElementDropdown] = useState(null); const [draggingBlock, setDraggingBlock] = useState(null); const [elementDragOffset, setElementDragOffset] = useState({ x: 0, y: 0 }); const [blockDragOffset, setBlockDragOffset] = useState({ x: 0, y: 0 }); const editorRef = useRef(null); const blockEditorRef = useRef(null); const elementEditorRef = useRef(null); const dropdownRef = useRef(null); const blockRef = useRef(null); const currentBlock = blocks.find((b) => b.blockUuid === selectedBlock); const currentElement = currentBlock?.elements.find((el) => el.elementUuid === selectedElement); // Helper function to send updates to backend - only sends the specific block that changed const updateBackend = async (updatedBlock: Block) => { if (!projectId || !selectedVersion) return; try { const response = await upsetDashBoardBlocksApi({ projectId, versionId: selectedVersion.versionId, blocks: [updatedBlock], // Send only the updated block }); if (response.data?.blocks) { // Update only the blocks that have success messages response.data.blocks.forEach((responseBlock: any) => { if (responseBlock.message === "Block updated successfully") { // Update the specific block in the store const { message, elements, ...blockData } = responseBlock; // Process elements to remove their messages const cleanedElements = elements?.map((el: any) => { const { message: elementMessage, ...elementData } = el; return elementData; }) || []; updateBlock(responseBlock.blockUuid, { ...blockData, elements: cleanedElements, }); } else if (responseBlock.message === "Block created successfully") { addBlock(responseBlock); } }); } } catch (error) { console.error("Failed to update backend:", error); } }; // Helper function to get a specific block from peeked blocks const getBlockFromPeekedBlocks = (peekedBlocks: Block[], blockId: string): Block | undefined => { return peekedBlocks.find((b) => b.blockUuid === blockId); }; useEffect(() => { if (!projectId || !selectedVersion) return; getDashBoardBlocksApi(projectId, selectedVersion.versionId).then((data) => { if (data.data?.blocks) { setBlocks(data.data.blocks); } }); }, [projectId, selectedVersion]); useCallBackOnKey( () => { if (!projectId || !selectedVersion) return; // For Ctrl+S, send all blocks upsetDashBoardBlocksApi({ projectId, versionId: selectedVersion.versionId, blocks: blocks, }); }, "Ctrl+S", { dependencies: [blocks, projectId, selectedVersion], noRepeat: true, allowOnInput: true } ); // Subscribe to data model changes useEffect(() => { const handleDataChange = (): void => { setDataModel(dataModelManager.getDataSnapshot()); }; const keys = dataModelManager.getAvailableKeys(); const subscriptions: Array<[string, () => void]> = []; for (const key of keys) { const callback = () => handleDataChange(); dataModelManager.subscribe(key, callback); subscriptions.push([key, callback]); } const interval = setInterval(() => { const currentKeys = dataModelManager.getAvailableKeys(); const newKeys = currentKeys.filter((key) => !keys.includes(key)); for (const key of newKeys) { const callback = () => handleDataChange(); dataModelManager.subscribe(key, callback); subscriptions.push([key, callback]); } }, 1000); return () => { for (const [key, callback] of subscriptions) { dataModelManager.unsubscribe(key, callback); } clearInterval(interval); }; }, []); // Click outside handler useEffect(() => { const handleClickOutside = (event: MouseEvent): void => { const target = event.target as Node; const isInsideBlockEditor = blockEditorRef.current?.contains(target); const isInsideElementEditor = elementEditorRef.current?.contains(target); const isInsideDropdown = dropdownRef.current?.contains(target); const isInsideEditor = editorRef.current?.contains(target); if (!isInsideEditor) { setSelectedBlock(null); setSelectedElement(null); setShowSwapUI(false); return; } if (isInsideEditor && !isInsideBlockEditor && !isInsideElementEditor && !isInsideDropdown) { const clickedElement = event.target as HTMLElement; const isBlock = clickedElement.closest("[data-block-id]"); const isElement = clickedElement.closest("[data-element-id]"); const isButton = clickedElement.closest("button"); const isResizeHandle = clickedElement.closest(".resize-handle"); if (!isBlock && !isElement && !isButton && !isResizeHandle) { setSelectedBlock(null); setSelectedElement(null); setShowSwapUI(false); setShowElementDropdown(null); } } }; // document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [selectedBlock, selectedElement]); // Drag and drop handler useEffect(() => { const handleMouseMove = (e: MouseEvent): void => { // Element dragging - direct DOM manipulation if (draggingElement && selectedBlock && currentElement?.positionType === "absolute") { const blockElement = document.querySelector(`[data-block-id="${selectedBlock}"]`) as HTMLElement; const elementToDrag = document.querySelector(`[data-element-id="${draggingElement}"]`) as HTMLElement; if (blockElement && elementToDrag) { const blockRect = blockElement.getBoundingClientRect(); const newX = e.clientX - blockRect.left - elementDragOffset.x; const newY = e.clientY - blockRect.top - elementDragOffset.y; // Direct DOM manipulation elementToDrag.style.left = `${Math.max(0, Math.min(blockRect.width - 50, newX))}px`; elementToDrag.style.top = `${Math.max(0, Math.min(blockRect.height - 30, newY))}px`; } } // Block dragging - direct DOM manipulation if (draggingBlock && currentBlock?.positionType && (currentBlock.positionType === "absolute" || currentBlock.positionType === "fixed")) { const editorElement = editorRef.current; const blockToDrag = document.querySelector(`[data-block-id="${draggingBlock}"]`) as HTMLElement; if (editorElement && blockToDrag) { const editorRect = editorElement.getBoundingClientRect(); const newX = e.clientX - editorRect.left - blockDragOffset.x; const newY = e.clientY - editorRect.top - blockDragOffset.y; // Direct DOM manipulation blockToDrag.style.left = `${Math.max(0, Math.min(editorRect.width - (currentBlock.size?.width || 400), newX))}px`; blockToDrag.style.top = `${Math.max(0, Math.min(editorRect.height - (currentBlock.size?.height || 300), newY))}px`; } } // Resizing - direct DOM manipulation if ((resizingElement || resizingBlock) && resizeStart) { if (resizingElement && selectedBlock) { const elementToResize = document.querySelector(`[data-element-id="${resizingElement}"]`) as HTMLElement; if (elementToResize) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; const newWidth = Math.max(100, resizeStart.width + deltaX); const newHeight = Math.max(50, resizeStart.height + deltaY); // Direct DOM manipulation elementToResize.style.width = `${newWidth}px`; elementToResize.style.height = `${newHeight}px`; } } else if (resizingBlock) { const blockToResize = document.querySelector(`[data-block-id="${resizingBlock}"]`) as HTMLElement; if (blockToResize) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; const currentBlock = blocks.find((b) => b.blockUuid === resizingBlock); const minSize = currentBlock ? calculateMinBlockSize(currentBlock) : { width: 100, height: 50 }; const newWidth = Math.max(minSize.width, resizeStart.width + deltaX); const newHeight = Math.max(minSize.height, resizeStart.height + deltaY); // Direct DOM manipulation blockToResize.style.width = `${newWidth}px`; blockToResize.style.height = `${newHeight}px`; } } } }; const handleMouseUp = async (): Promise => { // Update backend using peek methods, then update state from response if (draggingElement && selectedBlock && currentElement?.positionType === "absolute") { const blockElement = document.querySelector(`[data-block-id="${selectedBlock}"]`) as HTMLElement; const elementToDrag = document.querySelector(`[data-element-id="${draggingElement}"]`) as HTMLElement; if (blockElement && elementToDrag) { const computedStyle = window.getComputedStyle(elementToDrag); const x = parseFloat(computedStyle.left); const y = parseFloat(computedStyle.top); // Use peek to get updated blocks and send only the affected block to backend const updatedBlocks = peekUpdateElementPosition(selectedBlock, draggingElement, { x, y }); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, selectedBlock); if (updatedBlock) { await updateBackend(updatedBlock); } } } // Update backend for block drag if (draggingBlock && currentBlock?.positionType && (currentBlock.positionType === "absolute" || currentBlock.positionType === "fixed")) { const blockToDrag = document.querySelector(`[data-block-id="${draggingBlock}"]`) as HTMLElement; if (blockToDrag) { const computedStyle = window.getComputedStyle(blockToDrag); const x = parseFloat(computedStyle.left); const y = parseFloat(computedStyle.top); // Use peek to get updated blocks and send only the affected block to backend const updatedBlocks = peekUpdateBlockPosition(draggingBlock, { x, y }); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, draggingBlock); if (updatedBlock) { await updateBackend(updatedBlock); } } } // Update backend for element resize if (resizingElement && selectedBlock) { const elementToResize = document.querySelector(`[data-element-id="${resizingElement}"]`) as HTMLElement; if (elementToResize) { const computedStyle = window.getComputedStyle(elementToResize); const width = parseFloat(computedStyle.width); const height = parseFloat(computedStyle.height); // Use peek to get updated blocks and send only the affected block to backend const updatedBlocks = peekUpdateElementSize(selectedBlock, resizingElement, { width, height }); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, selectedBlock); if (updatedBlock) { await updateBackend(updatedBlock); } } } // Update backend for block resize if (resizingBlock) { const blockToResize = document.querySelector(`[data-block-id="${resizingBlock}"]`) as HTMLElement; if (blockToResize) { const computedStyle = window.getComputedStyle(blockToResize); const width = parseFloat(computedStyle.width); const height = parseFloat(computedStyle.height); // Use peek to get updated blocks and send only the affected block to backend const updatedBlocks = peekUpdateBlockSize(resizingBlock, { width, height }); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, resizingBlock); if (updatedBlock) { await updateBackend(updatedBlock); } } } // Reset all dragging states setDraggingElement(null); setResizingElement(null); setDraggingBlock(null); setResizingBlock(null); setResizeStart(null); }; if (draggingElement || draggingBlock || resizingElement || resizingBlock) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [draggingElement, resizingElement, draggingBlock, resizingBlock, elementDragOffset, blockDragOffset, selectedBlock, currentElement, resizeStart, currentBlock, blocks]); return (
{activeModule === "visualization" && ( { const updatedBlocks = peekAddBlock(); const newBlock = updatedBlocks[updatedBlocks.length - 1]; // Get the newly added block if (newBlock) { await updateBackend(newBlock); } }} showDataModelPanel={showDataModelPanel} setShowDataModelPanel={setShowDataModelPanel} /> )} {activeModule === "simulation" && isPlaying && } {/* BlockGrid */}
{ const updatedBlocks = peekAddElement(blockId, type, graphType); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} editMode={editMode} selectedBlock={selectedBlock} selectedElement={selectedElement} showSwapUI={showSwapUI} swapSource={swapSource} calculateMinBlockSize={calculateMinBlockSize} handleBlockClick={(blockId, event) => handleBlockClick(blockId, event, editMode, setSelectedBlock, setSelectedElement, setShowSwapUI, setShowElementDropdown, showElementDropdown)} handleElementClick={(blockId, elementId, event) => handleElementClick(blockId, elementId, event, editMode, setSelectedElement, setSelectedBlock, setShowSwapUI, setShowElementDropdown) } handleElementDragStart={(elementId, event) => handleElementDragStart(elementId, event, currentElement, setDraggingElement, setElementDragOffset)} handleElementResizeStart={(elementId, event) => handleElementResizeStart(elementId, event, setResizingElement, setResizeStart)} handleBlockResizeStart={(blockId, event) => handleBlockResizeStart(blockId, event, setResizingBlock, setResizeStart)} handleSwapStart={(elementId, event) => handleSwapStart(elementId, event, setSwapSource, setShowSwapUI)} handleSwapTarget={async (elementId, event) => { if (!selectedBlock) return; handleSwapTarget(elementId, event, swapSource, selectedBlock, async (blockId, el1, el2) => { const updatedBlocks = peekSwapElements(blockId, el1, el2); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }); }} handleBlockDragStart={(blockId, event) => handleBlockDragStart(blockId, event, setDraggingBlock, setBlockDragOffset)} setShowElementDropdown={setShowElementDropdown} showElementDropdown={showElementDropdown} blockRef={blockRef} /> {/* BlockEditor */} {selectedBlock && editMode && !selectedElement && currentBlock && ( { const updatedBlocks = peekUpdateBlockStyle(blockId, style); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateBlockSize={async (blockId, size) => { const updatedBlocks = peekUpdateBlockSize(blockId, size); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateBlockPosition={async (blockId, position) => { const updatedBlocks = peekUpdateBlockPosition(blockId, position); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateBlockPositionType={async (blockId, positionType) => { const updatedBlocks = peekUpdateBlockPositionType(blockId, positionType); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateBlockZIndex={async (blockId, zIndex) => { const updatedBlocks = peekUpdateBlockZIndex(blockId, zIndex); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} handleRemoveBlock={async (blockId) => { const block = getBlockById(blockId); if (!block) return; try { const data = await deleteDashBoardBlocksApi({ projectId: projectId!, versionId: selectedVersion!.versionId, blocks: [block], }); if (data.blocks.length > 0) { data.blocks.forEach((updatedBlock: any) => { if (updatedBlock.message === "Block deleted successfully") { removeBlock(updatedBlock.blockUuid); } }); } } catch (error) { console.error("Failed to delete block:", error); } }} /> )} {selectedElement && editMode && selectedBlock && currentElement && ( { const updatedBlocks = peekUpdateElementStyle(blockId, elementId, style); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateElementSize={async (blockId, elementId, size) => { const updatedBlocks = peekUpdateElementSize(blockId, elementId, size); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateElementPosition={async (blockId, elementId, position) => { const updatedBlocks = peekUpdateElementPosition(blockId, elementId, position); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateElementPositionType={async (blockId, elementId, positionType) => { const updatedBlocks = peekUpdateElementPositionType(blockId, elementId, positionType); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateElementZIndex={async (blockId, elementId, zIndex) => { const updatedBlocks = peekUpdateElementZIndex(blockId, elementId, zIndex); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateElementData={async (blockId, elementId, updates) => { const updatedBlocks = peekUpdateElementData(blockId, elementId, updates); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateGraphData={async (blockId, elementId, newData) => { const updatedBlocks = peekUpdateGraphData(blockId, elementId, newData); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateGraphTitle={async (blockId, elementId, title) => { const updatedBlocks = peekUpdateGraphTitle(blockId, elementId, title); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateGraphType={async (blockId, elementId, type) => { const updatedBlocks = peekUpdateGraphType(blockId, elementId, type); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateDataType={async (blockId, elementId, dataType) => { const updatedBlocks = peekUpdateDataType(blockId, elementId, dataType); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateCommonValue={async (blockId, elementId, commonValue) => { const updatedBlocks = peekUpdateCommonValue(blockId, elementId, commonValue); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateDataValue={async (blockId, elementId, dataValue) => { const updatedBlocks = peekUpdateDataValue(blockId, elementId, dataValue); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} updateDataSource={async (blockId, elementId, dataSource) => { const updatedBlocks = peekUpdateDataSource(blockId, elementId, dataSource); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); if (updatedBlock) { await updateBackend(updatedBlock); } }} handleRemoveElement={async (blockId, elementId) => { try { const data = await deleteDashBoardElementsApi({ projectId: projectId!, versionId: selectedVersion!.versionId, elementDatas: [{ blockUuid: blockId, elementUuid: elementId }], }); if (data.elements.length > 0) { data.elements.forEach((element: any) => { if (element.message === "Element deleted successfully") { removeElement(element.blockUuid, element.elementUuid); } }); } } catch (error) { console.error("Failed to delete element:", error); } }} setSwapSource={setSwapSource} setShowSwapUI={setShowSwapUI} /> )}
{showDataModelPanel && editMode && } {showSwapUI && }
); }; export default DashboardEditor;