I can drag and drop items into the canvas

I
This commit is contained in:
2025-07-02 18:23:49 +02:00
parent 7f6a19813d
commit f4e8f7a16c
10 changed files with 555 additions and 12 deletions

View File

@@ -1,10 +1,36 @@
from fastcore.basics import NotStr
from fasthtml.components import *
from fasthtml.xtend import Script
from components.BaseComponent import BaseComponent
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID
from components.workflows.db_management import WorkflowsDesignerSettings
from components.workflows.db_management import WorkflowsDesignerSettings, WorkflowsDesignerState, WorkflowComponent, \
Connection
from components_helpers import apply_boundaries, mk_tooltip
from core.utils import get_unique_id
# Component templates
COMPONENT_TYPES = {
"producer": {
"title": "Data Producer",
"description": "Generates or loads data",
"icon": "📊",
"color": "bg-green-100 border-green-300"
},
"filter": {
"title": "Data Filter",
"description": "Filters and transforms data",
"icon": "🔍",
"color": "bg-blue-100 border-blue-300"
},
"presenter": {
"title": "Data Presenter",
"description": "Displays or exports data",
"icon": "📋",
"color": "bg-purple-100 border-purple-300"
}
}
class WorkflowDesigner(BaseComponent):
def __init__(self, session,
@@ -15,13 +41,71 @@ class WorkflowDesigner(BaseComponent):
super().__init__(session, _id)
self._settings_manager = settings_manager
self._designer_settings = designer_settings
self.boundaries = boundaries
self._state = WorkflowsDesignerState()
self._boundaries = boundaries
def set_boundaries(self, boundaries: dict):
self.boundaries = boundaries
self._boundaries = boundaries
def refresh(self, oob=False):
return self._mk_elements()
def add_component(self, component_type, x, y):
self._state.component_counter += 1
component_id = f"comp_{self._state.component_counter}"
info = COMPONENT_TYPES[component_type]
component = WorkflowComponent(
id=component_id,
type=component_type,
x=int(x),
y=int(y),
title=info["title"],
description=info["description"]
)
self._state.components[component_id] = component
return self.refresh()
def move_component(self, component_id, x, y):
if component_id in self._state.components:
self._state.components[component_id].x = int(x)
self._state.components[component_id].y = int(y)
return self.refresh()
def delete_component(self, component_id):
# Remove component
if component_id in self._state.components:
del self._state.components[component_id]
# Remove related connections
self._state.connections = [connection for connection in self._state.connections
if connection.from_id != component_id and connection.to_id != component_id]
return self.refresh()
def add_connection(self, from_id, to_id):
# Check if connection already exists
for connection in self._state.connections:
if connection.from_id == from_id and connection.to_id == to_id:
return self.refresh() # , self.error_message("Connection already exists")
connection_id = f"conn_{len(self._state.connections) + 1}"
connection = Connection(id=connection_id, from_id=from_id, to_id=to_id)
self._state.connections.append(connection)
return self.refresh()
def __ft__(self):
return Div(f"Workflow Designer - {self._designer_settings.workflow_name}")
return Div(
H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"),
P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm mb-6"),
self._mk_designer(),
Script(f"bindWorkflowDesigner('{self._id}');"),
**apply_boundaries(self._boundaries),
id=f"{self._id}",
)
@staticmethod
def create_component_id(session, suffix=None):
@@ -30,3 +114,114 @@ class WorkflowDesigner(BaseComponent):
suffix = get_unique_id()
return f"{prefix}{suffix}"
@staticmethod
def _mk_toolbox_item(component_type: str, info: dict):
return Div(
mk_tooltip(
Div(
Span(info["icon"], cls="text-2xl mb-2"),
H4(info["title"], cls="font-semibold text-sm"),
cls=f"p-3 rounded-lg border-2 {info['color']} text-center"
),
tooltip=info["description"]),
cls="wkf-toolbox-item p-2",
draggable="true",
data_type=component_type
)
@staticmethod
def _mk_workflow_component(component: WorkflowComponent):
info = COMPONENT_TYPES[component.type]
return Div(
# Input connection point
Div(cls="wkf-connection-point wkf-input-point",
data_component_id=component.id,
data_point_type="input"),
# Component content
Div(
Span(info["icon"], cls="text-xl mb-1"),
H4(component.title, cls="font-semibold text-sm"),
P(info["description"], cls="text-xs text-gray-600"),
cls=f"p-3 rounded-lg border-2 {info['color']} bg-white shadow-lg"
),
# Output connection point
Div(cls="wkf-connection-point wkf-output-point",
data_component_id=component.id,
data_point_type="output"),
cls="wkf-workflow-component w-32",
style=f"left: {component.x}px; top: {component.y}px;",
data_component_id=component.id,
draggable="true"
)
def _mk_connection_svg(self, conn: Connection):
if conn.from_id not in self._state.components or conn.to_id not in self._state.components:
return ""
from_comp = self._state.components[conn.from_id]
to_comp = self._state.components[conn.to_id]
# Calculate connection points (approximate)
x1 = from_comp.x + 128 # component width + output point
y1 = from_comp.y + 40 # component height / 2
x2 = to_comp.x
y2 = to_comp.y + 40
# Create curved path
mid_x = (x1 + x2) / 2
path = f"M {x1} {y1} C {mid_x} {y1}, {mid_x} {y2}, {x2} {y2}"
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)"/>
<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"/>
</marker>
</defs>
</svg>
"""
def _mk_elements(self):
return Div(
# Render connections
*[NotStr(self._mk_connection_svg(conn)) for conn in self._state.connections],
# Render components
*[self._mk_workflow_component(comp) for comp in self._state.components.values()],
cls="wkf-canvas bg-white rounded-lg border relative",
),
def _mk_canvas(self, oob=False):
return Div(
self._mk_elements(),
cls="flex-1 ",
id=f"c_{self._id}",
hx_swap_oob='true' if oob else None,
),
def _mk_toolbox(self):
return Div(
H2("Toolbox", cls="text-lg font-semibold mb-4 text-gray-700"),
Div(
*[self._mk_toolbox_item(comp_type, info)
for comp_type, info in COMPONENT_TYPES.items()],
cls="space-y-3"
),
cls="w-32 p-4 bg-gray-50 rounded-lg border"
)
def _mk_designer(self):
return Div(
self._mk_toolbox(), # (Left side)
self._mk_canvas(), # (Right side)
cls="flex gap-4",
id=f"d_{self._id}",
)