I can drag and drop items into the canvas
I
This commit is contained in:
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components_helpers import set_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters
|
||||
from components_helpers import apply_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters
|
||||
from core.utils import get_unique_id
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ class AdminForm(BaseComponent):
|
||||
for item in self.form_fields
|
||||
],
|
||||
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
|
||||
**set_boundaries(self._boundaries),
|
||||
**apply_boundaries(self._boundaries),
|
||||
cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.datagrid_new.settings import DataGridSettings
|
||||
from components.hoildays.helpers.nibelisparser import NibelisParser
|
||||
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
|
||||
from components_helpers import mk_dialog_buttons, set_boundaries
|
||||
from components_helpers import mk_dialog_buttons, apply_boundaries
|
||||
from core.instance_manager import InstanceManager
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ImportHolidays(BaseComponent):
|
||||
mk_dialog_buttons(ok_title="Import", cls="mt-2", on_ok=self.commands.import_holidays()),
|
||||
id=self._id,
|
||||
cls="m-2",
|
||||
**set_boundaries(self._boundaries, other=26),
|
||||
**apply_boundaries(self._boundaries, other=26),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -9,7 +9,7 @@ from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.debugger.assets.icons import icon_expanded, icon_collapsed, icon_class
|
||||
from components.debugger.commands import JsonViewerCommands
|
||||
from components.debugger.constants import INDENT_SIZE, MAX_TEXT_LENGTH, NODE_OBJECT, NODES_KEYS_TO_NOT_EXPAND
|
||||
from components_helpers import set_boundaries
|
||||
from components_helpers import apply_boundaries
|
||||
from core.serializer import TAG_OBJECT
|
||||
from core.utils import get_unique_id
|
||||
|
||||
@@ -299,7 +299,7 @@ class JsonViewer(BaseComponent):
|
||||
style="margin-left: 0px;"),
|
||||
cls="mmt-jsonviewer",
|
||||
id=f"{self._id}",
|
||||
**set_boundaries(self._boundaries),
|
||||
**apply_boundaries(self._boundaries),
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
@@ -32,3 +32,33 @@ def post(session, _id: str, name: str, tab_boundaries: str):
|
||||
f"Entering {Routes.AddWorkflow} with args {debug_session(session)}, {_id=}, {name=}, {tab_boundaries=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.show_workflow(name, json.loads(tab_boundaries))
|
||||
|
||||
|
||||
@rt(Routes.AddComponent)
|
||||
def post(session, _id: str, component_type: str, x: int, y: int):
|
||||
logger.debug(
|
||||
f"Entering {Routes.AddComponent} with args {debug_session(session)}, {_id=}, {component_type=}, {x=}, {y=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.add_component(component_type, x, y)
|
||||
|
||||
@rt(Routes.MoveComponent)
|
||||
def post(session, _id: str, component_id: str, x: int, y: int):
|
||||
logger.debug(
|
||||
f"Entering {Routes.MoveComponent} with args {debug_session(session)}, {_id=}, {component_id=}, {x=}, {y=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.move_component(component_id, x, y)
|
||||
|
||||
|
||||
@rt(Routes.DeleteComponent)
|
||||
def post(session, _id: str, component_id: str):
|
||||
logger.debug(
|
||||
f"Entering {Routes.DeleteComponent} with args {debug_session(session)}, {_id=}, {component_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.delete_component(component_id)
|
||||
|
||||
@rt(Routes.AddConnection)
|
||||
def post(session, _id: str, from_id: str, to_id: str):
|
||||
logger.debug(
|
||||
f"Entering {Routes.AddConnection} with args {debug_session(session)}, {_id=}, {from_id=}, {to_id=}")
|
||||
instance = InstanceManager.get(session, _id)
|
||||
return instance.add_connection(from_id, to_id)
|
||||
67
src/components/workflows/assets/Workflows.css
Normal file
67
src/components/workflows/assets/Workflows.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.wkf-toolbox-item {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.wkf-toolbox-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.wkf-canvas {
|
||||
position: relative;
|
||||
min-height: 600px;
|
||||
background-image:
|
||||
linear-gradient(rgba(0,0,0,.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,0,0,.1) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.wkf-workflow-component {
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wkf-workflow-component:hover {
|
||||
border-color: #3b82f6;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.wkf-workflow-component.selected {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.wkf-connection-line {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wkf-connection-point {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #3b82f6;
|
||||
border-radius: 50%;
|
||||
cursor: crosshair;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.wkf-output-point {
|
||||
right: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wkf-input-point {
|
||||
left: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wkf-connection-point:hover {
|
||||
background: #ef4444;
|
||||
transform: translateY(-50%) scale(1.2);
|
||||
}
|
||||
226
src/components/workflows/assets/Workflows.js
Normal file
226
src/components/workflows/assets/Workflows.js
Normal file
@@ -0,0 +1,226 @@
|
||||
function bindWorkflowDesigner(elementId) {
|
||||
// Store state for this specific designer instance
|
||||
const designer = {
|
||||
draggedType: null,
|
||||
draggedComponent: null,
|
||||
selectedComponent: null,
|
||||
connectionMode: false,
|
||||
connectionStart: null
|
||||
};
|
||||
|
||||
// Get the designer container and canvas
|
||||
const designerContainer = document.getElementById(elementId);
|
||||
//const canvas = designerContainer.querySelector('.wkf-canvas');
|
||||
const canvas = document.getElementById("c_" + elementId);
|
||||
|
||||
// === Event delegation for components ===
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Handle components
|
||||
if (event.target.closest('.wkf-workflow-component')) {
|
||||
designer.draggedComponent = event.target.closest('.wkf-workflow-component').dataset.componentId;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
});
|
||||
|
||||
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';
|
||||
// Update connections in real-time
|
||||
updateConnections(designerContainer);
|
||||
});
|
||||
|
||||
designerContainer.addEventListener('dragend', (event) => {
|
||||
if (!event.target.closest('.wkf-workflow-component')) return;
|
||||
if (designer.draggedComponent) {
|
||||
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),
|
||||
}
|
||||
});
|
||||
designer.draggedComponent = null;
|
||||
// Update connections after drag ends
|
||||
updateConnections(designerContainer);
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
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');
|
||||
});
|
||||
// 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();
|
||||
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
|
||||
};
|
||||
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,
|
||||
}
|
||||
});
|
||||
}
|
||||
// Reset connection mode
|
||||
designerContainer.querySelectorAll('.wkf-connection-point').forEach(point => {
|
||||
point.style.background = '#3b82f6';
|
||||
});
|
||||
designer.connectionStart = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Canvas drag over event (for dropping new components)
|
||||
canvas.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
|
||||
// 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),
|
||||
}
|
||||
});
|
||||
|
||||
designer.draggedType = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 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,
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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) + 40; // component height / 2
|
||||
const toX = parseInt(toComp.style.left);
|
||||
const toY = parseInt(toComp.style.top) + 40;
|
||||
// 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 all workflow designers on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Find all workflow designer containers and bind them
|
||||
document.querySelectorAll('[id^="wkf-designer-"]').forEach(designer => {
|
||||
bindDesigner(designer.id);
|
||||
});
|
||||
});
|
||||
@@ -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}",
|
||||
)
|
||||
|
||||
@@ -8,3 +8,7 @@ class Routes:
|
||||
AddWorkflow = "/add-workflow"
|
||||
SelectWorkflow = "/select-workflow"
|
||||
ShowWorkflow = "/show-workflow"
|
||||
AddComponent = "/add-component"
|
||||
MoveComponent = "/move-component"
|
||||
DeleteComponent = "/delete-component"
|
||||
AddConnection = "/add-connection"
|
||||
|
||||
@@ -6,11 +6,32 @@ from core.settings_management import SettingsManager
|
||||
|
||||
logger = logging.getLogger("WorkflowsSettings")
|
||||
|
||||
# Data structures
|
||||
@dataclass
|
||||
class WorkflowComponent:
|
||||
id: str
|
||||
type: str
|
||||
x: int
|
||||
y: int
|
||||
title: str
|
||||
description: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Connection:
|
||||
id: str
|
||||
from_id: str
|
||||
to_id: str
|
||||
|
||||
@dataclass
|
||||
class WorkflowsDesignerSettings:
|
||||
workflow_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowsDesignerState:
|
||||
components: dict[str, WorkflowComponent] = field(default_factory=dict)
|
||||
connections: list[Connection] = field(default_factory=list)
|
||||
component_counter = 0
|
||||
|
||||
@dataclass
|
||||
class WorkflowsSettings:
|
||||
|
||||
@@ -81,7 +81,7 @@ def mk_accordion_section(component_id, title, icon, content, selected=False):
|
||||
)
|
||||
|
||||
|
||||
def set_boundaries(boundaries, remove_margin=True, other=0):
|
||||
def apply_boundaries(boundaries, remove_margin=True, other=0):
|
||||
if isinstance(boundaries, int):
|
||||
max_height = boundaries
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user