diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py index 83ab679..6cb9a9e 100644 --- a/src/components/workflows/WorkflowsApp.py +++ b/src/components/workflows/WorkflowsApp.py @@ -65,6 +65,12 @@ def post(session, _id: str, from_id: str, to_id: str): instance = InstanceManager.get(session, _id) return instance.add_connection(from_id, to_id) +@rt(Routes.DeleteConnection) +def post(session, _id: str, from_id: str, to_id: str): + logger.debug( + f"Entering {Routes.DeleteConnection} with args {debug_session(session)}, {_id=}, {from_id=}, {to_id=}") + instance = InstanceManager.get(session, _id) + return instance.delete_connection(from_id, to_id) @rt(Routes.ResizeDesigner) def post(session, _id: str, designer_height: int): diff --git a/src/components/workflows/assets/Workflows.css b/src/components/workflows/assets/Workflows.css index 06bf17a..42b6234 100644 --- a/src/components/workflows/assets/Workflows.css +++ b/src/components/workflows/assets/Workflows.css @@ -138,3 +138,33 @@ 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } } +.wkf-connection-path { + stroke: #3b82f6; + stroke-width: 2; + fill: none; + cursor: pointer; + pointer-events: none; + transition: stroke 0.2s ease, stroke-width 0.2s ease; +} + +.wkf-connection-path-thick { + stroke: transparent; + stroke-width: 10; + fill: none; + cursor: pointer; + pointer-events: stroke; +} + +.wkf-connection-path-arrowhead { + fill:#3b82f6; +} + +.wkf-connection-selected { + stroke: #ef4444 !important; +} + +.wkf-connection-path-arrowhead-selected { + fill:#ef4444 !important;; +} + + diff --git a/src/components/workflows/assets/Workflows.js b/src/components/workflows/assets/Workflows.js index 4e21283..9be6702 100644 --- a/src/components/workflows/assets/Workflows.js +++ b/src/components/workflows/assets/Workflows.js @@ -4,308 +4,523 @@ function bindWorkflowDesigner(elementId) { } function bindWorkflowDesignerToolbox(elementId) { - // Store state for this specific designer instance - const designer = { - draggedType: null, - draggedComponent: null, - selectedComponent: null, - connectionStart: null, - potentialConnectionStart: null, + // Constants for configuration + const CONFIG = { + COMPONENT_WIDTH: 128, + COMPONENT_HEIGHT: 64, + DRAG_OFFSET: { x: 64, y: 40 }, + CONNECTION_POINT_RADIUS: 6, + INVISIBLE_DRAG_IMAGE: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', }; - // Get the designer container and canvas + // Designer state with better organization + const designer = { + // Drag state + draggedType: null, + draggedComponent: null, + + // Selection state + selectedComponent: null, + selectedConnection: null, + + // Connection state + connectionStart: null, + potentialConnectionStart: null, + + // Performance optimization + lastUpdateTime: 0, + animationFrame: null, + + // Cleanup tracking + eventListeners: new Map(), + + // State methods + reset() { + this.draggedType = null; + this.draggedComponent = null; + this.connectionStart = null; + this.potentialConnectionStart = null; + this.cancelAnimationFrame(); + }, + + cancelAnimationFrame() { + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + } + }; + + // Get DOM elements with error handling const designerContainer = document.getElementById(elementId); - //const canvas = designerContainer.querySelector('.wkf-canvas'); - const canvas = document.getElementById("c_" + elementId); + const canvas = document.getElementById(`c_${elementId}`); - // === Event delegation for components === + if (!designerContainer || !canvas) { + console.error(`Workflow designer elements not found for ID: ${elementId}`); + return null; + } - // Use event delegation for all component-related events - designerContainer.addEventListener('dragstart', (event) => { - // Handle toolbox items - if (event.target.closest('.wkf-toolbox-item')) { - designer.draggedType = event.target.closest('.wkf-toolbox-item').dataset.type; - event.dataTransfer.effectAllowed = 'copy'; - } + // Utility functions + const utils = { - // Handle components - if (event.target.closest('.wkf-workflow-component')) { - const component = event.target.closest('.wkf-workflow-component'); - component.classList.add('dragging'); - designer.draggedComponent = component.dataset.componentId; - event.dataTransfer.effectAllowed = 'move'; + // Check if two rectangles overlap + isOverlapping(rect1, circle) { + // Find the closest point on the rectangle to the circle's center + const closestX = Math.max(rect1.x, Math.min(circle.x, rect1.x + rect1.width)); + const closestY = Math.max(rect1.y, Math.min(circle.y, rect1.y + rect1.height)); - // Create an invisible image to use as the drag image - const invisibleImg = new Image(); - invisibleImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // 1px transparent GIF - event.dataTransfer.setDragImage(invisibleImg, 0, 0); + // Calculate the distance between the circle's center and the closest point + const deltaX = circle.x - closestX; + const deltaY = circle.y - closestY; + const distanceSquared = deltaX * deltaX + deltaY * deltaY; - // Highlight all valid output points on other components - designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => { - if (point.dataset.pointType === 'output' && - point.dataset.componentId !== designer.draggedComponent) { - point.classList.add('potential-connection'); - } - }); - - } - }); - - designerContainer.addEventListener('drag', (event) => { - if (!event.target.closest('.wkf-workflow-component')) return; - if (event.clientX === 0 && event.clientY === 0) return; // Ignore invalid drag events - - const component = event.target.closest('.wkf-workflow-component'); - const rect = canvas.getBoundingClientRect(); - const x = event.clientX - rect.left - 64; - const y = event.clientY - rect.top - 40; - component.style.left = Math.max(0, x) + 'px'; - component.style.top = Math.max(0, y) + 'px'; - - const componentRect = component.getBoundingClientRect(); - const componentId = component.dataset.componentId; - const outputPoints = designerContainer.querySelectorAll('.wkf-connection-point[data-point-type="output"]'); - - outputPoints.forEach(point => { - if (point.dataset.componentId === componentId) return; // Skip points from the same component - - const pointRect = point.getBoundingClientRect(); - const pointCircle = { - x: pointRect.left + pointRect.width / 2, - y: pointRect.top + pointRect.height / 2, - radius: 6 - }; - - if (point != designer.potentialConnectionStart && _isOverlapping(componentRect, pointCircle)) { - console.debug("overlapping !") - outputPoints.forEach(other_point => { - other_point.classList.remove("potential-start") - }); - designer.potentialConnectionStart = point.dataset.componentId; - point.classList.add('potential-start'); - } - - }); - - // Update connections in real-time - updateConnections(designerContainer); - }); - - designerContainer.addEventListener('dragend', (event) => { - if (!event.target.closest('.wkf-workflow-component')) return; - - if (designer.draggedComponent) { - const component = event.target.closest('.wkf-workflow-component'); - const draggedComponentId = component.dataset.componentId; - component.classList.remove('dragging'); + // Check if the distance is less than or equal to the circle's radius + return distanceSquared <= circle.radius * circle.radius; + }, + // Get mouse position relative to canvas + getCanvasPosition(event) { const rect = canvas.getBoundingClientRect(); - const x = event.clientX - rect.left - 64; - const y = event.clientY - rect.top - 40; - htmx.ajax('POST', '/workflows/move-component', { - target: `#c_${elementId}`, - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "innerHTML", - values: { - _id: elementId, - component_id: designer.draggedComponent, - x: Math.max(0, x), - y: Math.max(0, y), - } - }); + return { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; + }, - // Create connection if we ended the drag over a connection point - if (designer.potentialConnectionStart) { + // Constrain position within canvas bounds + constrainPosition(x, y) { + const canvasRect = canvas.getBoundingClientRect(); + return { + x: Math.max(0, Math.min(x, canvasRect.width - CONFIG.COMPONENT_WIDTH)), + y: Math.max(0, Math.min(y, canvasRect.height - CONFIG.COMPONENT_HEIGHT)) + }; + }, - htmx.ajax('POST', '/workflows/add-connection', { - target: `#c_${elementId}`, - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "innerHTML", - values: { - _id: elementId, - from_id: designer.potentialConnectionStart, // The output point we're hovering over - to_id: draggedComponentId, // The component we're dragging + // Create HTMX request with error handling + async makeRequest(url, values, targetId = `#c_${elementId}`, swap="innerHTML") { + try { + return htmx.ajax('POST', url, { + target: targetId, + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + swap: swap, + values: { _id: elementId, ...values } + }); + } catch (error) { + console.error('HTMX request failed:', error); + throw error; + } + } + }; + + // Connection management + const connectionManager = { + // Update all connections with performance optimization + updateAll() { + designer.cancelAnimationFrame(); + designer.animationFrame = requestAnimationFrame(() => { + const connectionLines = designerContainer.querySelectorAll('.wkf-connection-line'); + connectionLines.forEach(svg => { + const { fromId, toId } = svg.dataset; + if (fromId && toId) { + this.updateLine(svg, fromId, toId); } }); - } + }); + }, - // Remove highlighting from all connection points + // Update a specific connection line + updateLine(svg, fromId, toId) { + const fromComp = designerContainer.querySelector(`[data-component-id="${fromId}"]`); + const toComp = designerContainer.querySelector(`[data-component-id="${toId}"]`); + + if (!fromComp || !toComp) return; + + // Calculate connection points + const fromX = parseInt(fromComp.style.left) + CONFIG.COMPONENT_WIDTH; + const fromY = parseInt(fromComp.style.top) + CONFIG.COMPONENT_HEIGHT / 2; + const toX = parseInt(toComp.style.left); + const toY = parseInt(toComp.style.top) + CONFIG.COMPONENT_HEIGHT / 2; + + // Create smooth curved path + const midX = (fromX + toX) / 2; + const path = `M ${fromX} ${fromY} C ${midX} ${fromY}, ${midX} ${toY}, ${toX} ${toY}`; + + // Update the path element + const pathElement = svg.querySelector('.wkf-connection-path'); + if (pathElement) { + pathElement.setAttribute('d', path); + } + }, + + // Clear all connection highlighting + clearHighlighting() { designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => { - point.classList.remove('potential-connection'); + point.classList.remove('potential-connection', 'potential-start'); + point.style.background = '#3b82f6'; + }); + }, + + // Select a connection + select(connectionPath) { + // Deselect all other connections + designerContainer.querySelectorAll('.wkf-connection-line path').forEach(path => { + path.classList.remove('wkf-connection-selected'); }); - designer.draggedComponent = null; - designer.potentialConnectionStart = null; - // Update connections after drag ends - updateConnections(designerContainer); + // Select the clicked connection + connectionPath.classList.add('wkf-connection-selected'); + + // Store connection data + const connectionSvg = connectionPath.closest('.wkf-connection-line'); + designer.selectedConnection = { + fromId: connectionSvg.dataset.fromId, + toId: connectionSvg.dataset.toId + }; + }, + + // Deselect all connections + deselectAll() { + designerContainer.querySelectorAll('.wkf-connection-line path').forEach(path => { + path.classList.remove('wkf-connection-selected'); + }); + designer.selectedConnection = null; } - }); + }; - // Handle component clicks using event delegation - designerContainer.addEventListener('click', (event) => { - // Handle canvas clicks to reset connection mode and deselect components - if (event.target === canvas || event.target.classList.contains('wkf-canvas')) { - if (designer.connectionStart) { - designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => { - point.style.background = '#3b82f6'; - }); - designer.connectionStart = null; - } - // Deselect components + // Component management + const componentManager = { + // Select a component + select(component) { + // Deselect all other components + designerContainer.querySelectorAll('.wkf-workflow-component').forEach(comp => { + comp.classList.remove('selected'); + }); + + // Select the clicked component + component.classList.add('selected'); + designer.selectedComponent = component.dataset.componentId; + + // Also trigger server-side selection + utils.makeRequest('/workflows/select-component', { + component_id: designer.selectedComponent + }, `#p_${elementId}`, "outerHTML"); + }, + + // Deselect all components + deselectAll() { designerContainer.querySelectorAll('.wkf-workflow-component').forEach(comp => { comp.classList.remove('selected'); }); designer.selectedComponent = null; - return; - } + }, - // Handle component selection - const component = event.target.closest('.wkf-workflow-component'); - if (component) { - event.stopPropagation(); - // Remove previous selection - designerContainer.querySelectorAll('.wkf-workflow-component').forEach(comp => { - comp.classList.remove('selected'); + // Update component position with constraints + updatePosition(component, x, y) { + const constrained = utils.constrainPosition(x, y); + component.style.left = constrained.x + 'px'; + component.style.top = constrained.y + 'px'; + } + }; + + // Event handlers with improved organization + const eventHandlers = { + // Handle drag start for both toolbox items and components + onDragStart(event) { + const toolboxItem = event.target.closest('.wkf-toolbox-item'); + const component = event.target.closest('.wkf-workflow-component'); + + if (toolboxItem) { + designer.draggedType = toolboxItem.dataset.type; + event.dataTransfer.effectAllowed = 'copy'; + return; + } + + if (component) { + component.classList.add('dragging'); + designer.draggedComponent = component.dataset.componentId; + event.dataTransfer.effectAllowed = 'move'; + + // Use invisible drag image + const invisibleImg = new Image(); + invisibleImg.src = CONFIG.INVISIBLE_DRAG_IMAGE; + event.dataTransfer.setDragImage(invisibleImg, 0, 0); + + // Highlight potential connection points + designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => { + if (point.dataset.pointType === 'output' && + point.dataset.componentId !== designer.draggedComponent) { + point.classList.add('potential-connection'); + } + }); + } + }, + + // Handle drag with immediate updates + onDrag(event) { + if (!event.target.closest('.wkf-workflow-component')) return; + if (event.clientX === 0 && event.clientY === 0) return; + + const component = event.target.closest('.wkf-workflow-component'); + const position = utils.getCanvasPosition(event); + const x = position.x - CONFIG.DRAG_OFFSET.x; + const y = position.y - CONFIG.DRAG_OFFSET.y; + + // Update position immediately for responsive feel + componentManager.updatePosition(component, x, y); + + // Check for potential connections + eventHandlers.checkPotentialConnections(component); + + // Update connections with requestAnimationFrame for smooth rendering + connectionManager.updateAll(); + }, + + // Check for potential connections during drag + checkPotentialConnections(component) { + const componentRect = component.getBoundingClientRect(); + const componentId = component.dataset.componentId; + const outputPoints = designerContainer.querySelectorAll('.wkf-connection-point[data-point-type="output"]'); + + outputPoints.forEach(point => { + if (point.dataset.componentId === componentId) return; + + const pointRect = point.getBoundingClientRect(); + const pointCircle = { + x: pointRect.left + pointRect.width / 2, + y: pointRect.top + pointRect.height / 2, + radius: CONFIG.CONNECTION_POINT_RADIUS + }; + + if (point !== designer.potentialConnectionStart && + utils.isOverlapping(componentRect, pointCircle)) { + + // Clear previous potential starts + outputPoints.forEach(otherPoint => { + otherPoint.classList.remove('potential-start'); + }); + + designer.potentialConnectionStart = point.dataset.componentId; + point.classList.add('potential-start'); + } }); - // Select current component - component.classList.add('selected'); - designer.selectedComponent = component.dataset.componentId; - } + }, - // Handle connection points - const connectionPoint = event.target.closest('.wkf-connection-point'); - if (connectionPoint) { - event.stopPropagation(); + // Handle drag end with cleanup + async onDragEnd(event) { + if (!event.target.closest('.wkf-workflow-component')) return; + + if (designer.draggedComponent) { + const component = event.target.closest('.wkf-workflow-component'); + const draggedComponentId = component.dataset.componentId; + + component.classList.remove('dragging'); + + const position = utils.getCanvasPosition(event); + const x = position.x - CONFIG.DRAG_OFFSET.x; + const y = position.y - CONFIG.DRAG_OFFSET.y; + const constrained = utils.constrainPosition(x, y); + + try { + // Move component + await utils.makeRequest('/workflows/move-component', { + component_id: designer.draggedComponent, + x: constrained.x, + y: constrained.y + }); + + // Create connection if applicable + if (designer.potentialConnectionStart) { + await utils.makeRequest('/workflows/add-connection', { + from_id: designer.potentialConnectionStart, + to_id: draggedComponentId + }); + } + } catch (error) { + console.error('Failed to update component:', error); + } + + // Cleanup + connectionManager.clearHighlighting(); + designer.reset(); + connectionManager.updateAll(); + } + }, + + // Handle clicks with improved event delegation + onClick(event) { + + // Connection point handling + const connectionPoint = event.target.closest('.wkf-connection-point'); + if (connectionPoint) { + event.stopPropagation(); + eventHandlers.handleConnectionPoint(connectionPoint); + } + + // Connection selection + const connectionPath = event.target.closest('.wkf-connection-line path'); + if (connectionPath) { + event.stopPropagation(); + componentManager.deselectAll(); + // get the visible connection path + const visibleConnectionPath = connectionPath.parentElement.querySelector('.wkf-connection-path'); + connectionManager.select(visibleConnectionPath); + return; + } + + // Canvas click - reset everything + if (event.target === canvas || event.target.classList.contains('wkf-canvas')) { + designer.reset(); + connectionManager.clearHighlighting(); + connectionManager.deselectAll(); + componentManager.deselectAll(); + return; + } + + // Component selection + const component = event.target.closest('.wkf-workflow-component'); + if (component) { + event.stopPropagation(); + connectionManager.deselectAll(); + componentManager.select(component); + return; + } + + + }, + + // Handle connection point interactions + async handleConnectionPoint(connectionPoint) { const componentId = connectionPoint.dataset.componentId; const pointType = connectionPoint.dataset.pointType; if (!designer.connectionStart) { // Start connection from output point if (pointType === 'output') { - designer.connectionStart = { - componentId: componentId, - pointType: pointType - }; + designer.connectionStart = { componentId, pointType }; connectionPoint.style.background = '#ef4444'; - console.log('Connection started from:', componentId); } } else { // Complete connection to input point if (pointType === 'input' && componentId !== designer.connectionStart.componentId) { - htmx.ajax('POST', '/workflows/add-connection', { - target: `#c_${elementId}`, - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "innerHTML", - values: { - _id: elementId, - from_id: designer.connectionStart.componentId, - to_id: componentId, - } - }); + try { + await utils.makeRequest('/workflows/add-connection', { + from_id: designer.connectionStart.componentId, + to_id: componentId + }); + } catch (error) { + console.error('Failed to create connection:', error); + } } + // Reset connection mode - designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => { - point.style.background = '#3b82f6'; - }); + connectionManager.clearHighlighting(); designer.connectionStart = null; } + }, + + // Handle canvas drop for new components + async onCanvasDrop(event) { + event.preventDefault(); + + if (designer.draggedType) { + const position = utils.getCanvasPosition(event); + const x = position.x - CONFIG.DRAG_OFFSET.x; + const y = position.y - CONFIG.DRAG_OFFSET.y; + const constrained = utils.constrainPosition(x, y); + + try { + await utils.makeRequest('/workflows/add-component', { + component_type: designer.draggedType, + x: constrained.x, + y: constrained.y + }); + } catch (error) { + console.error('Failed to add component:', error); + } + + designer.draggedType = null; + } + }, + + // Handle keyboard shortcuts + async onKeyDown(event) { + if (event.key === 'Delete' || event.key === 'Suppr') { + try { + if (designer.selectedComponent) { + await utils.makeRequest('/workflows/delete-component', { + component_id: designer.selectedComponent + }); + designer.selectedComponent = null; + } else if (designer.selectedConnection) { + await utils.makeRequest('/workflows/delete-connection', { + from_id: designer.selectedConnection.fromId, + to_id: designer.selectedConnection.toId + }); + designer.selectedConnection = null; + } + } catch (error) { + console.error('Failed to delete:', error); + } + } } + }; - else { - const component = event.target.closest('.wkf-workflow-component'); - if (!component) return; + // Event registration with cleanup tracking + function registerEventListener(element, event, handler, options = {}) { + const key = `${element.id || 'global'}-${event}`; + element.addEventListener(event, handler, options); + designer.eventListeners.set(key, () => element.removeEventListener(event, handler, options)); + } - componentId = component.dataset.componentId - htmx.ajax('POST', '/workflows/select-component', { - target: `#p_${elementId}`, - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "outerHTML", - values: { - _id: elementId, - component_id: componentId - } - }); - } - }); - - // Canvas drag over event (for dropping new components) - canvas.addEventListener('dragover', (event) => { + // Register all event listeners + registerEventListener(designerContainer, 'dragstart', eventHandlers.onDragStart); + registerEventListener(designerContainer, 'drag', eventHandlers.onDrag); + registerEventListener(designerContainer, 'dragend', eventHandlers.onDragEnd); + registerEventListener(designerContainer, 'click', eventHandlers.onClick); + registerEventListener(canvas, 'dragover', (event) => { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; }); + registerEventListener(canvas, 'drop', eventHandlers.onCanvasDrop); + registerEventListener(document, 'keydown', eventHandlers.onKeyDown); - // Canvas drop event (for creating new components) - canvas.addEventListener('drop', (event) => { - event.preventDefault(); - if (designer.draggedType) { - const rect = event.currentTarget.getBoundingClientRect(); - const x = event.clientX - rect.left - 64; // Center the component - const y = event.clientY - rect.top - 40; - htmx.ajax('POST', '/workflows/add-component', { - target: `#c_${elementId}`, - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "innerHTML", - values: { - _id: elementId, - component_type: designer.draggedType, - x: Math.max(0, x), - y: Math.max(0, y), - } - }); + // Public API + const api = { + // Cleanup function for proper disposal + destroy() { + designer.cancelAnimationFrame(); + designer.eventListeners.forEach(cleanup => cleanup()); + designer.eventListeners.clear(); + }, - designer.draggedType = null; - } - }); + // Get current designer state + getState() { + return { + selectedComponent: designer.selectedComponent, + selectedConnection: designer.selectedConnection, + connectionStart: designer.connectionStart + }; + }, - // Delete selected component with Delete key - document.addEventListener('keydown', (event) => { - if (event.key === 'Delete' && designer.selectedComponent) { - htmx.ajax('POST', '/workflows/delete-component', { - target: `#c_${elementId}`, - headers: {"Content-Type": "application/x-www-form-urlencoded"}, - swap: "innerHTML", - values: { - _id: elementId, - component_id: designer.selectedComponent, - } - }); - } - }); + // Force update all connections + updateConnections() { + connectionManager.updateAll(); + }, - // Function to update all connection lines for this designer - function updateConnections(container) { - const connectionLines = container.querySelectorAll('.wkf-connection-line'); - connectionLines.forEach(svg => { - const connectionData = svg.dataset; - if (connectionData.fromId && connectionData.toId) { - updateConnectionLine(svg, connectionData.fromId, connectionData.toId, container); + // Select component programmatically + selectComponent(componentId) { + const component = designerContainer.querySelector(`[data-component-id="${componentId}"]`); + if (component) { + componentManager.select(component); } - }); - } - - // Function to update a single connection line - function updateConnectionLine(svg, fromId, toId, container) { - const fromComp = container.querySelector(`[data-component-id="${fromId}"]`); - const toComp = container.querySelector(`[data-component-id="${toId}"]`); - if (!fromComp || !toComp) return; - // Get current positions - const fromX = parseInt(fromComp.style.left) + 128; // component width + output point - const fromY = parseInt(fromComp.style.top) + 32; // component height / 2 - const toX = parseInt(toComp.style.left); - const toY = parseInt(toComp.style.top) + 32; - // Create curved path - const midX = (fromX + toX) / 2; - const path = `M ${fromX} ${fromY} C ${midX} ${fromY}, ${midX} ${toY}, ${toX} ${toY}`; - // Update the path - const pathElement = svg.querySelector('path'); - if (pathElement) { - pathElement.setAttribute('d', path); } - } + }; - // Return the designer object for possible external use - return designer; + // Initialize connections on load + setTimeout(() => connectionManager.updateAll(), 100); + + return api; } /** @@ -335,7 +550,6 @@ function bindWorkflowDesignerSplitter(elementId) { isResizing = true; startY = e.clientY; startDesignerHeight = parseInt(designer.style.height, 10) || designer.parentNode.getBoundingClientRect().height; - console.debug("startDesignerHeight", startDesignerHeight); document.body.style.userSelect = 'none'; // Disable text selection document.body.style.cursor = "row-resize"; // Change cursor style globally for horizontal splitter @@ -357,8 +571,7 @@ function bindWorkflowDesignerSplitter(elementId) { const propertiesHeight = Math.max(50, containerHeight - newDesignerHeight - splitter.offsetHeight); properties.style.height = `${propertiesHeight}px`; } - - console.debug("newDesignerHeight", newDesignerHeight); + }); // Mouse up event - stop dragging @@ -399,16 +612,3 @@ function bindWorkflowDesignerSplitter(elementId) { } } -function _isOverlapping(rect, circle) { - // Find the closest point on the rectangle to the circle's center - const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width)); - const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height)); - - // Calculate the distance between the circle's center and the closest point - const deltaX = circle.x - closestX; - const deltaY = circle.y - closestY; - const distanceSquared = deltaX * deltaX + deltaY * deltaY; - - // Check if the distance is less than or equal to the circle's radius - return distanceSquared <= circle.radius * circle.radius; -} \ No newline at end of file diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py index 8901cd7..22cb545 100644 --- a/src/components/workflows/components/WorkflowDesigner.py +++ b/src/components/workflows/components/WorkflowDesigner.py @@ -124,6 +124,16 @@ class WorkflowDesigner(BaseComponent): return self.refresh_designer() + def delete_connection(self, from_id, to_id): + for connection in self._state.connections: + if connection.from_id == from_id and connection.to_id == to_id: + self._state.connections.remove(connection) + + # update db + self._db.save_state(self._key, self._state) + + return self.refresh_designer() + def set_designer_height(self, height): self._state.designer_height = height self._db.save_state(self._key, self._state) @@ -201,10 +211,12 @@ class WorkflowDesigner(BaseComponent): return f""" - + + + - + diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py index 4f0a910..4eafd49 100644 --- a/src/components/workflows/constants.py +++ b/src/components/workflows/constants.py @@ -17,6 +17,7 @@ class Routes: MoveComponent = "/move-component" DeleteComponent = "/delete-component" AddConnection = "/add-connection" + DeleteConnection = "/delete-connection" ResizeDesigner = "/resize-designer" SaveProperties = "/save-properties" CancelProperties = "/cancel-properties"