diff --git a/backend/__pycache__/database.cpython-39.pyc b/backend/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000..45b6547 Binary files /dev/null and b/backend/__pycache__/database.cpython-39.pyc differ diff --git a/frontend/canvas.html b/frontend/canvas.html index 7431059..540d9eb 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -637,6 +637,104 @@ const [gridSnap, setGridSnap] = useState(false); + // --- CLIPBOARD & ACTIONS --- + const [clipboard, setClipboard] = useState({ nodes: [] }); + + const handleCopy = useCallback(() => { + const currentNodes = reactFlowInstance.getNodes(); + const selectedNodes = currentNodes.filter(n => n.selected); + if (selectedNodes.length > 0) { + // Deep clone the selected nodes to prevent reference issues + setClipboard({ nodes: JSON.parse(JSON.stringify(selectedNodes)) }); + } + }, [reactFlowInstance]); + + const handleCut = useCallback(() => { + const currentNodes = reactFlowInstance.getNodes(); + const selectedNodes = currentNodes.filter(n => n.selected); + + if (selectedNodes.length > 0) { + setClipboard({ nodes: JSON.parse(JSON.stringify(selectedNodes)) }); + + const selectedNodeIds = new Set(selectedNodes.map(n => n.id)); + + // Remove nodes and any edges connected to them + setNodes(nds => nds.filter(n => !selectedNodeIds.has(n.id))); + setEdges(eds => eds.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target))); + } + }, [reactFlowInstance, setNodes, setEdges]); + + const handlePaste = useCallback(() => { + if (clipboard.nodes.length > 0) { + const newNodes = clipboard.nodes.map(node => { + // Give the new node a unique ID and a slight positional offset + // so it doesn't overlap perfectly with the original + return { + ...node, + id: Date.now().toString() + Math.random().toString(36).substr(2, 5), + position: { + x: node.position.x + 20, + y: node.position.y + 20 + }, + selected: true, // Automatically select the newly pasted node + data: { + ...node.data, + // Ensure the pasted node gets a fresh display name (e.g., component_5) + componentDisplayName: generateComponentDisplayName() + } + }; + }); + + // Deselect existing nodes and add the new ones + setNodes(nds => nds.map(n => ({...n, selected: false})).concat(newNodes)); + + // Update the clipboard with the new offset nodes so + // rapid pasting cascades them nicely + setClipboard({ nodes: newNodes }); + } + }, [clipboard, setNodes, generateComponentDisplayName]); + + const handleDelete = useCallback(() => { + const currentNodes = reactFlowInstance.getNodes(); + const selectedNodes = currentNodes.filter(n => n.selected); + const selectedNodeIds = new Set(selectedNodes.map(n => n.id)); + + if (selectedNodeIds.size > 0) { + setNodes(nds => nds.filter(n => !selectedNodeIds.has(n.id))); + setEdges(eds => eds.filter(e => !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target))); + } + }, [reactFlowInstance, setNodes, setEdges]); + + // --- KEYBOARD SHORTCUTS --- + useEffect(() => { + const handleKeyDown = (e) => { + // Prevent actions if the user is typing in an input or textarea + if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') { + return; + } + + const cmdOrCtrl = e.ctrlKey || e.metaKey; // Supports Windows/Linux (Ctrl) and Mac (Cmd) + + if (cmdOrCtrl && e.key.toLowerCase() === 'c') { + e.preventDefault(); + handleCopy(); + } else if (cmdOrCtrl && e.key.toLowerCase() === 'x') { + e.preventDefault(); + handleCut(); + } else if (cmdOrCtrl && e.key.toLowerCase() === 'v') { + e.preventDefault(); + handlePaste(); + } else if (e.key === 'Delete' || e.key === 'Backspace') { + // ReactFlow handles this natively if the canvas is focused, + // but this ensures it fires even if focus is outside the canvas wrapper. + handleDelete(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [handleCopy, handleCut, handlePaste, handleDelete]); + const componentCounterRef = useRef(1); const generateComponentDisplayName = useCallback(() => { diff --git a/frontend/canvas_edit.html b/frontend/canvas_edit.html new file mode 100644 index 0000000..7431059 --- /dev/null +++ b/frontend/canvas_edit.html @@ -0,0 +1,829 @@ + + + +{% raw %} + +
+ +