diff --git a/app/src/components/SimulationDashboard/DashboardEditor.tsx b/app/src/components/SimulationDashboard/DashboardEditor.tsx index 35fb2a4..582bece 100644 --- a/app/src/components/SimulationDashboard/DashboardEditor.tsx +++ b/app/src/components/SimulationDashboard/DashboardEditor.tsx @@ -33,26 +33,30 @@ const DashboardEditor: React.FC = () => { setSelectedElement, addBlock, setBlocks, - updateBlockPosition, - updateElementPosition, - updateBlockSize, - updateElementSize, - updateBlockStyle, - updateBlockPositionType, - updateBlockZIndex, - addElement, - updateElementStyle, - updateElementPositionType, - updateElementZIndex, - updateElementData, - updateGraphData, - updateGraphTitle, - updateGraphType, - swapElements, removeBlock, removeElement, getBlockById, - subscribe, + updateBlock, + // Peek methods + peekAddBlock, + peekRemoveBlock, + peekAddElement, + peekRemoveElement, + peekUpdateBlockStyle, + peekUpdateBlockSize, + peekUpdateBlockPosition, + peekUpdateBlockPositionType, + peekUpdateBlockZIndex, + peekUpdateElementStyle, + peekUpdateElementSize, + peekUpdateElementPosition, + peekUpdateElementPositionType, + peekUpdateElementZIndex, + peekUpdateElementData, + peekUpdateGraphData, + peekUpdateGraphTitle, + peekUpdateGraphType, + peekSwapElements, } = simulationDashBoardStore(); const [editMode, setEditMode] = useState(false); @@ -83,9 +87,49 @@ const DashboardEditor: React.FC = () => { const currentBlock = blocks.find((b) => b.blockUuid === selectedBlock); const currentElement = currentBlock?.elements.find((el) => el.elementUuid === selectedElement); - useEffect(() => { - // console.log("blocks: ", blocks); - }, [blocks]); + // 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; @@ -96,31 +140,15 @@ const DashboardEditor: React.FC = () => { }); }, [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); + // For Ctrl+S, send all blocks + upsetDashBoardBlocksApi({ + projectId, + versionId: selectedVersion.versionId, + blocks: blocks, + }); }, "Ctrl+S", { dependencies: [blocks, projectId, selectedVersion], noRepeat: true, allowOnInput: true } @@ -269,8 +297,8 @@ const DashboardEditor: React.FC = () => { } }; - const handleMouseUp = (): void => { - // Update state only on mouse up for elements + 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; @@ -280,11 +308,16 @@ const DashboardEditor: React.FC = () => { const x = parseFloat(computedStyle.left); const y = parseFloat(computedStyle.top); - updateElementPosition(selectedBlock, draggingElement, { x, y }); + // 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 state only on mouse up for blocks + // 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; @@ -293,11 +326,16 @@ const DashboardEditor: React.FC = () => { const x = parseFloat(computedStyle.left); const y = parseFloat(computedStyle.top); - updateBlockPosition(draggingBlock, { x, y }); + // 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 state only on mouse up for resizing + // Update backend for element resize if (resizingElement && selectedBlock) { const elementToResize = document.querySelector(`[data-element-id="${resizingElement}"]`) as HTMLElement; if (elementToResize) { @@ -305,10 +343,16 @@ const DashboardEditor: React.FC = () => { const width = parseFloat(computedStyle.width); const height = parseFloat(computedStyle.height); - updateElementSize(selectedBlock, resizingElement, { width, 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) { @@ -316,7 +360,12 @@ const DashboardEditor: React.FC = () => { const width = parseFloat(computedStyle.width); const height = parseFloat(computedStyle.height); - updateBlockSize(resizingBlock, { width, 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); + } } } @@ -342,7 +391,19 @@ const DashboardEditor: React.FC = () => { return (
{activeModule === "visualization" && ( - addBlock()} showDataModelPanel={showDataModelPanel} setShowDataModelPanel={setShowDataModelPanel} /> + { + const updatedBlocks = peekAddBlock(); + const newBlock = updatedBlocks[updatedBlocks.length - 1]; // Get the newly added block + if (newBlock) { + await updateBackend(newBlock); + } + }} + showDataModelPanel={showDataModelPanel} + setShowDataModelPanel={setShowDataModelPanel} + /> )} {/* BlockGrid */} @@ -350,8 +411,12 @@ const DashboardEditor: React.FC = () => {
{ - addElement(blockId, type, graphType); + handleAddElement={async (blockId, type, graphType) => { + const updatedBlocks = peekAddElement(blockId, type, graphType); + const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); + if (updatedBlock) { + await updateBackend(updatedBlock); + } }} editMode={editMode} selectedBlock={selectedBlock} @@ -367,7 +432,16 @@ const DashboardEditor: React.FC = () => { 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)} + 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} @@ -381,15 +455,52 @@ const DashboardEditor: React.FC = () => { blockEditorRef={blockEditorRef} currentBlock={currentBlock} selectedBlock={selectedBlock} - updateBlockStyle={(blockId, style) => 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)} - handleRemoveBlock={(blockId) => { + updateBlockStyle={async (blockId, style) => { + 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; - deleteDashBoardBlocksApi({ projectId: projectId!, versionId: selectedVersion!.versionId, blocks: [block] }).then((data) => { + + 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") { @@ -397,7 +508,9 @@ const DashboardEditor: React.FC = () => { } }); } - }); + } catch (error) { + console.error("Failed to delete block:", error); + } }} /> )} @@ -408,16 +521,76 @@ const DashboardEditor: React.FC = () => { currentElement={currentElement} selectedBlock={selectedBlock} selectedElement={selectedElement} - updateElementStyle={(blockId, elementId, style) => 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)} - handleRemoveElement={(blockId, elementId) => removeElement(blockId, elementId)} + updateElementStyle={async (blockId, elementId, style) => { + 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); + } + }} + handleRemoveElement={async (blockId, elementId) => { + const updatedBlocks = peekRemoveElement(blockId, elementId); + const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); + if (updatedBlock) { + await updateBackend(updatedBlock); + } + }} setSwapSource={setSwapSource} setShowSwapUI={setShowSwapUI} dataModelManager={dataModelManager} diff --git a/app/src/components/SimulationDashboard/functions/eventHandlers/index.ts b/app/src/components/SimulationDashboard/functions/eventHandlers/index.ts index 8c7b7ec..3836dac 100644 --- a/app/src/components/SimulationDashboard/functions/eventHandlers/index.ts +++ b/app/src/components/SimulationDashboard/functions/eventHandlers/index.ts @@ -115,7 +115,6 @@ export const handleBlockClick = ( setSelectedBlock(blockId); setSelectedElement(null); setShowSwapUI(false); - console.log('showElementDropdown: ', showElementDropdown); if (showElementDropdown && showElementDropdown !== blockId) { setShowElementDropdown(blockId); } @@ -135,8 +134,8 @@ export const handleElementClick = ( event.stopPropagation(); if (editMode) { setSelectedElement(elementId); - setSelectedBlock(blockId); - setShowSwapUI(false); + // setSelectedBlock(blockId); + // setShowSwapUI(false); // setShowElementDropdown(null); } }; diff --git a/app/src/store/simulation/useSimulationDashBoardStore.ts b/app/src/store/simulation/useSimulationDashBoardStore.ts index 1c03c52..b67eeda 100644 --- a/app/src/store/simulation/useSimulationDashBoardStore.ts +++ b/app/src/store/simulation/useSimulationDashBoardStore.ts @@ -19,7 +19,7 @@ interface SimulationDashboardStore { setSelectedElement: (elementId: string | null) => void; // Block operations - addBlock: () => void; + addBlock: (newBlock : Block) => void; removeBlock: (blockId: string) => void; updateBlock: (blockId: string, updates: Partial) => void; clearBlocks: () => void; @@ -53,6 +53,29 @@ interface SimulationDashboardStore { // Element swapping swapElements: (blockId: string, elementId1: string, elementId2: string) => void; + // Peek operations (get updated value without setting state) + peekAddBlock: () => Block[]; + peekRemoveBlock: (blockId: string) => Block[]; + peekUpdateBlock: (blockId: string, updates: Partial) => Block[]; + peekUpdateBlockStyle: (blockId: string, newStyle: React.CSSProperties) => Block[]; + peekUpdateBlockSize: (blockId: string, size: Size) => Block[]; + peekUpdateBlockPosition: (blockId: string, position: Position) => Block[]; + peekUpdateBlockPositionType: (blockId: string, positionType: "relative" | "absolute" | "fixed") => Block[]; + peekUpdateBlockZIndex: (blockId: string, zIndex: number) => Block[]; + peekAddElement: (blockId: string, type: UIType, graphType?: GraphTypes, dataType?: DataType) => Block[]; + peekRemoveElement: (blockId: string, elementId: string) => Block[]; + peekUpdateElement: (blockId: string, elementId: string, updates: Partial) => Block[]; + peekUpdateElementStyle: (blockId: string, elementId: string, newStyle: ExtendedCSSProperties) => Block[]; + peekUpdateElementSize: (blockId: string, elementId: string, size: Size) => Block[]; + peekUpdateElementPosition: (blockId: string, elementId: string, position: Position) => Block[]; + peekUpdateElementPositionType: (blockId: string, elementId: string, positionType: "relative" | "absolute" | "fixed") => Block[]; + peekUpdateElementZIndex: (blockId: string, elementId: string, zIndex: number) => Block[]; + peekUpdateElementData: (blockId: string, elementId: string, updates: Partial) => Block[]; + peekUpdateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => Block[]; + peekUpdateGraphTitle: (blockId: string, elementId: string, title: string) => Block[]; + peekUpdateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => Block[]; + peekSwapElements: (blockId: string, elementId1: string, elementId2: string) => Block[]; + // Helper functions getBlockById: (blockId: string) => Block | undefined; getElementById: (blockId: string, elementId: string) => UIElement | undefined; @@ -64,6 +87,11 @@ interface SimulationDashboardStore { const subscribers = new Set<() => void>(); +// Helper function to deep clone blocks +const cloneBlocks = (blocks: Block[]): Block[] => { + return JSON.parse(JSON.stringify(blocks)); +}; + export const createSimulationDashboardStore = () => { return create()( immer((set, get) => ({ @@ -96,7 +124,6 @@ export const createSimulationDashboardStore = () => { setSelectedBlock: (blockId) => { set((state) => { state.selectedBlock = blockId; - // Clear element selection when selecting a block if (blockId) { state.selectedElement = null; } @@ -110,26 +137,8 @@ export const createSimulationDashboardStore = () => { }, // Block operations - addBlock: () => { + addBlock: (newBlock) => { set((state) => { - const newBlock: Block = { - blockUuid: MathUtils.generateUUID(), - style: { - backgroundColor: "rgba(50, 50, 50, 0.8)", - backdropFilter: "blur(10px)", - padding: 10, - borderRadius: 8, - border: "1px solid rgba(255, 255, 255, 0.1)", - position: "relative", - minHeight: "200px", - minWidth: "300px", - }, - elements: [], - zIndex: 1, - size: { width: 400, height: 300 }, - position: { x: 0, y: 0 }, - positionType: "relative", - }; state.blocks.push(newBlock); }); }, @@ -292,7 +301,6 @@ export const createSimulationDashboardStore = () => { commonValue: "metric-1", }; } else { - // Default to single-machine newElement = { ...baseGraphProps, dataType: "single-machine" as const, @@ -334,7 +342,7 @@ export const createSimulationDashboardStore = () => { break; default: - return; // Should not happen + return; } block.elements.push(newElement); @@ -471,7 +479,6 @@ export const createSimulationDashboardStore = () => { if (element && element.type === "graph") { element.graphType = graphType; element.graphTitle = `${graphType.charAt(0).toUpperCase() + graphType.slice(1)} Chart`; - element.graphData = defaultGraphData; } } @@ -498,6 +505,367 @@ export const createSimulationDashboardStore = () => { }); }, + // Peek operations - return updated blocks without setting state + peekAddBlock: () => { + const blocks = cloneBlocks(get().blocks); + const newBlock: Block = { + blockUuid: MathUtils.generateUUID(), + style: { + backgroundColor: "rgba(50, 50, 50, 0.8)", + backdropFilter: "blur(10px)", + padding: 10, + borderRadius: 8, + border: "1px solid rgba(255, 255, 255, 0.1)", + position: "relative", + minHeight: "200px", + minWidth: "300px", + }, + elements: [], + zIndex: 1, + size: { width: 400, height: 300 }, + position: { x: 0, y: 0 }, + positionType: "relative", + }; + blocks.push(newBlock); + return blocks; + }, + + peekRemoveBlock: (blockId) => { + const blocks = cloneBlocks(get().blocks); + return blocks.filter((block) => block.blockUuid !== blockId); + }, + + peekUpdateBlock: (blockId, updates) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + Object.assign(block, updates); + } + return blocks; + }, + + peekUpdateBlockStyle: (blockId, newStyle) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.style = { ...block.style, ...newStyle }; + } + return blocks; + }, + + peekUpdateBlockSize: (blockId, size) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.size = size; + } + return blocks; + }, + + peekUpdateBlockPosition: (blockId, position) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.position = position; + } + return blocks; + }, + + peekUpdateBlockPositionType: (blockId, positionType) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.positionType = positionType; + } + return blocks; + }, + + peekUpdateBlockZIndex: (blockId, zIndex) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.zIndex = zIndex; + } + return blocks; + }, + + peekAddElement: (blockId, type, graphType, dataType?: DataType) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (!block) return blocks; + + let newElement: UIElement; + + const commonProps = { + elementUuid: MathUtils.generateUUID(), + positionType: "relative" as const, + position: { x: 0, y: 0 }, + zIndex: 1, + data: { + key: MathUtils.generateUUID(), + dataSource: "static" as const, + staticValue: "", + label: undefined, + }, + }; + + switch (type) { + case "label-value": + newElement = { + ...commonProps, + type: "label-value", + title: "Label Value", + dataSource: "machine-1", + dataValue: "metric-1", + style: { + color: "#ffffff", + fontSize: 14, + textAlign: "left" as const, + display: "flex", + flexDirection: "column", + gap: "4px", + alignItems: "flex-start", + justifyContent: "center", + labelColor: "#ffffff", + valueColor: "#ffffff", + }, + data: { + ...commonProps.data, + staticValue: "Value", + label: "Label", + }, + size: { width: 200, height: 60 }, + }; + break; + + case "graph": + const baseGraphProps = { + ...commonProps, + type: "graph" as const, + graphType: graphType, + graphTitle: "Graph Title", + style: { + width: "100%", + height: "100%", + minHeight: "120px", + color: "#ffffff", + fontSize: 14, + textAlign: "left" as const, + backgroundColor: "rgba(0, 0, 0, 0.2)", + borderRadius: "4px", + padding: "8px", + }, + graphData: defaultGraphData, + size: { width: 400, height: 200 }, + }; + + if (dataType === "multiple-machine") { + newElement = { + ...baseGraphProps, + dataType: "multiple-machine" as const, + title: "Multi Machine Chart", + dataSource: ["machine-1", "machine-2", "machine-3"], + commonValue: "metric-1", + }; + } else { + newElement = { + ...baseGraphProps, + dataType: "single-machine" as const, + title: "Single Machine Chart", + dataSource: "machine-1", + dataValue: ["metric-1", "metric-2", "metric-3"], + }; + } + break; + + case "text": + newElement = { + ...commonProps, + type: "text", + style: { + color: "#ffffff", + fontSize: 14, + textAlign: "left" as const, + }, + data: { + ...commonProps.data, + staticValue: "Text", + }, + size: { width: 200, height: 40 }, + }; + break; + + case "icon": + newElement = { + ...commonProps, + type: "icon", + style: { + color: "#ffffff", + fontSize: 14, + textAlign: "center" as const, + }, + size: { width: 40, height: 40 }, + }; + break; + + default: + return blocks; + } + + block.elements.push(newElement); + return blocks; + }, + + peekRemoveElement: (blockId, elementId) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.elements = block.elements.filter((el) => el.elementUuid !== elementId); + } + return blocks; + }, + + peekUpdateElement: (blockId, elementId, updates) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + Object.assign(element, updates); + } + } + return blocks; + }, + + peekUpdateElementStyle: (blockId, elementId, newStyle) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.style = { ...element.style, ...newStyle }; + } + } + return blocks; + }, + + peekUpdateElementSize: (blockId, elementId, size) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.size = size; + } + } + return blocks; + }, + + peekUpdateElementPosition: (blockId, elementId, position) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.position = position; + } + } + return blocks; + }, + + peekUpdateElementPositionType: (blockId, elementId, positionType) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.positionType = positionType; + } + } + return blocks; + }, + + peekUpdateElementZIndex: (blockId, elementId, zIndex) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.zIndex = zIndex; + } + } + return blocks; + }, + + peekUpdateElementData: (blockId, elementId, updates) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element?.data) { + element.data = { ...element.data, ...updates }; + } + } + return blocks; + }, + + peekUpdateGraphData: (blockId, elementId, newData) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.graphData = newData; + } + } + return blocks; + }, + + peekUpdateGraphTitle: (blockId, elementId, title) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element) { + element.graphTitle = title; + } + } + return blocks; + }, + + peekUpdateGraphType: (blockId, elementId, graphType) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + const element = block.elements.find((el) => el.elementUuid === elementId); + if (element && element.type === "graph") { + element.graphType = graphType; + element.graphTitle = `${graphType.charAt(0).toUpperCase() + graphType.slice(1)} Chart`; + element.graphData = defaultGraphData; + } + } + return blocks; + }, + + peekSwapElements: (blockId, elementId1, elementId2) => { + const blocks = cloneBlocks(get().blocks); + const block = blocks.find((b) => b.blockUuid === blockId); + if (block) { + block.elements = block.elements.map((el) => { + if (el.elementUuid === elementId1) { + const targetElement = block.elements.find((e) => e.elementUuid === elementId2); + return targetElement ? { ...targetElement, elementUuid: elementId1 } : el; + } + if (el.elementUuid === elementId2) { + const sourceElement = block.elements.find((e) => e.elementUuid === elementId1); + return sourceElement ? { ...sourceElement, elementUuid: elementId2 } : el; + } + return el; + }); + } + return blocks; + }, + // Helper functions getBlockById: (blockId) => { return get().blocks.find((b) => b.blockUuid === blockId);