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 { useSceneContext } from "../../modules/scene/sceneContext"; import useModuleStore from "../../store/ui/useModuleStore"; 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"; const DashboardEditor: React.FC = () => { const { simulationDashBoardStore, versionStore } = useSceneContext(); const { selectedVersion } = versionStore(); const { projectId } = useParams(); const { activeModule } = useModuleStore(); const { blocks, setBlocks, updateBlockPosition, updateElementPosition, updateBlockSize, updateElementSize, addBlock, updateBlockStyle, updateBlockPositionType, updateBlockZIndex, updateElementStyle, updateElementPositionType, updateElementZIndex, updateElementData, updateGraphData, updateGraphTitle, updateGraphType, swapElements, subscribe, } = simulationDashBoardStore(); const [selectedBlock, setSelectedBlock] = useState(null); const [selectedElement, setSelectedElement] = useState(null); 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); useEffect(() => { // console.log("blocks: ", blocks); }, [blocks]); useEffect(() => { if (!projectId || !selectedVersion) return; getDashBoardBlocksApi(projectId, selectedVersion.versionId).then((data) => { if (data.data?.blocks) { setBlocks(data.data.blocks); } }); }, [projectId, selectedVersion]); const updateBackend = (blocks: Block[]) => { if (!projectId || !selectedVersion) return; upsetDashBoardBlocksApi({ projectId, versionId: selectedVersion.versionId, blocks }).then((data) => { if (data.data?.blocks) { setBlocks(data.data.blocks); } }); }; useEffect(() => { const unsubscribe = subscribe(() => { if (!projectId || !selectedVersion) return; updateBackend(blocks); }); return () => { unsubscribe(); }; }, [blocks, projectId, selectedVersion]); useCallBackOnKey( () => { if (!projectId || !selectedVersion) return; updateBackend(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 = (): void => { // Update state only on mouse up for elements 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); updateElementPosition(selectedBlock, draggingElement, { x, y }); } } // Update state only on mouse up for blocks 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); updateBlockPosition(draggingBlock, { x, y }); } } // Update state only on mouse up for resizing 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); updateElementSize(selectedBlock, resizingElement, { width, height }); } } 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); updateBlockSize(resizingBlock, { width, height }); } } // 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" && ( addBlock()} showDataModelPanel={showDataModelPanel} setShowDataModelPanel={setShowDataModelPanel} /> )} {/* BlockGrid */}
handleBlockClick(blockId, event, editMode, setSelectedBlock, setSelectedElement, setShowSwapUI)} 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={(elementId, event) => handleSwapTarget(elementId, event, swapSource, selectedBlock, swapElements)} handleBlockDragStart={(blockId, event) => handleBlockDragStart(blockId, event, setDraggingBlock, setBlockDragOffset)} setShowElementDropdown={setShowElementDropdown} showElementDropdown={showElementDropdown} blockRef={blockRef} /> {/* BlockEditor */} {selectedBlock && editMode && !selectedElement && currentBlock && ( updateBlockStyle(blockId, style)} updateBlockSize={(blockId, size) => updateBlockSize(blockId, size)} updateBlockPosition={(blockId, position) => updateBlockPosition(blockId, position)} updateBlockPositionType={(blockId, positionType) => updateBlockPositionType(blockId, positionType)} updateBlockZIndex={(blockId, zIndex) => updateBlockZIndex(blockId, zIndex)} /> )} {selectedElement && editMode && selectedBlock && currentElement && ( updateElementStyle(blockId, elementId, style)} updateElementSize={(blockId, elementId, size) => updateElementSize(blockId, elementId, size)} updateElementPosition={(blockId, elementId, position) => updateElementPosition(blockId, elementId, position)} updateElementPositionType={(blockId, elementId, positionType) => updateElementPositionType(blockId, elementId, positionType)} updateElementZIndex={(blockId, elementId, zIndex) => updateElementZIndex(blockId, elementId, zIndex)} updateElementData={(blockId, elementId, updates) => updateElementData(blockId, elementId, updates)} updateGraphData={(blockId, elementId, newData) => updateGraphData(blockId, elementId, newData)} updateGraphTitle={(blockId, elementId, title) => updateGraphTitle(blockId, elementId, title)} updateGraphType={(blockId, elementId, type) => updateGraphType(blockId, elementId, type)} setSwapSource={setSwapSource} setShowSwapUI={setShowSwapUI} dataModelManager={dataModelManager} /> )}
{showDataModelPanel && editMode && } {showSwapUI && }
); }; export default DashboardEditor;