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 fasthtml.components import *
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponent
|
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
|
from core.utils import get_unique_id
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ class AdminForm(BaseComponent):
|
|||||||
for item in self.form_fields
|
for item in self.form_fields
|
||||||
],
|
],
|
||||||
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
|
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"
|
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.datagrid_new.settings import DataGridSettings
|
||||||
from components.hoildays.helpers.nibelisparser import NibelisParser
|
from components.hoildays.helpers.nibelisparser import NibelisParser
|
||||||
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
|
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
|
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()),
|
mk_dialog_buttons(ok_title="Import", cls="mt-2", on_ok=self.commands.import_holidays()),
|
||||||
id=self._id,
|
id=self._id,
|
||||||
cls="m-2",
|
cls="m-2",
|
||||||
**set_boundaries(self._boundaries, other=26),
|
**apply_boundaries(self._boundaries, other=26),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@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.assets.icons import icon_expanded, icon_collapsed, icon_class
|
||||||
from components.debugger.commands import JsonViewerCommands
|
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.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.serializer import TAG_OBJECT
|
||||||
from core.utils import get_unique_id
|
from core.utils import get_unique_id
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ class JsonViewer(BaseComponent):
|
|||||||
style="margin-left: 0px;"),
|
style="margin-left: 0px;"),
|
||||||
cls="mmt-jsonviewer",
|
cls="mmt-jsonviewer",
|
||||||
id=f"{self._id}",
|
id=f"{self._id}",
|
||||||
**set_boundaries(self._boundaries),
|
**apply_boundaries(self._boundaries),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __eq__(self, other):
|
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=}")
|
f"Entering {Routes.AddWorkflow} with args {debug_session(session)}, {_id=}, {name=}, {tab_boundaries=}")
|
||||||
instance = InstanceManager.get(session, _id)
|
instance = InstanceManager.get(session, _id)
|
||||||
return instance.show_workflow(name, json.loads(tab_boundaries))
|
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.components import *
|
||||||
|
from fasthtml.xtend import Script
|
||||||
|
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID
|
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
|
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):
|
class WorkflowDesigner(BaseComponent):
|
||||||
def __init__(self, session,
|
def __init__(self, session,
|
||||||
@@ -15,13 +41,71 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
super().__init__(session, _id)
|
super().__init__(session, _id)
|
||||||
self._settings_manager = settings_manager
|
self._settings_manager = settings_manager
|
||||||
self._designer_settings = designer_settings
|
self._designer_settings = designer_settings
|
||||||
self.boundaries = boundaries
|
self._state = WorkflowsDesignerState()
|
||||||
|
self._boundaries = boundaries
|
||||||
|
|
||||||
def set_boundaries(self, boundaries: dict):
|
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):
|
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
|
@staticmethod
|
||||||
def create_component_id(session, suffix=None):
|
def create_component_id(session, suffix=None):
|
||||||
@@ -30,3 +114,114 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
suffix = get_unique_id()
|
suffix = get_unique_id()
|
||||||
|
|
||||||
return f"{prefix}{suffix}"
|
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"
|
AddWorkflow = "/add-workflow"
|
||||||
SelectWorkflow = "/select-workflow"
|
SelectWorkflow = "/select-workflow"
|
||||||
ShowWorkflow = "/show-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")
|
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
|
@dataclass
|
||||||
class WorkflowsDesignerSettings:
|
class WorkflowsDesignerSettings:
|
||||||
workflow_name: str
|
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
|
@dataclass
|
||||||
class WorkflowsSettings:
|
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):
|
if isinstance(boundaries, int):
|
||||||
max_height = boundaries
|
max_height = boundaries
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user