I can delete a connection

This commit is contained in:
2025-07-05 22:19:58 +02:00
parent aed1022be3
commit 9df32e3b5f
5 changed files with 513 additions and 264 deletions

View File

@@ -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):

View File

@@ -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;;
}

View File

@@ -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
@@ -358,7 +572,6 @@ function bindWorkflowDesignerSplitter(elementId) {
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;
}

View File

@@ -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"""
<svg class="wkf-connection-line" style="left: 0; top: 0; width: 100%; height: 100%;"
data-from-id="{conn.from_id}" data-to-id="{conn.to_id}">
<path d="{path}" stroke="#3b82f6" stroke-width="2" fill="none" marker-end="url(#arrowhead)"/>
<path d="{path}" class="wkf-connection-path-thick"/>
<path d="{path}" class="wkf-connection-path" marker-end="url(#arrowhead)"/>
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#3b82f6"/>
<polygon points="0 0, 10 3.5, 0 7" class="wkf-connection-path-arrowhead"/>
</marker>
</defs>
</svg>

View File

@@ -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"