From 7f6a19813da32e8a3096e34182963059954a7a97 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Wed, 2 Jul 2025 00:05:49 +0200 Subject: [PATCH] I can show WorkflowDesigner tab I --- src/assets/main.css | 4 + src/components/tabs/components/MyTabs.py | 3 + src/components/workflows/WorkflowsApp.py | 8 + src/components/workflows/commands.py | 8 + .../workflows/components/WorkflowDesigner.py | 32 ++++ .../workflows/components/Workflows.py | 38 ++++- src/components/workflows/constants.py | 2 + src/components/workflows/db_management.py | 5 + tests/tests_workflows_db_manager.py | 152 ++++++++++++++++++ 9 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 tests/tests_workflows_db_manager.py diff --git a/src/assets/main.css b/src/assets/main.css index f97bf85..830aacc 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -42,6 +42,10 @@ transition: opacity 0.2s ease; } +.mmt-selected { + background-color: var(--color-base-300); + border-radius: .25rem; +} .icon-32 { width: 32px; diff --git a/src/components/tabs/components/MyTabs.py b/src/components/tabs/components/MyTabs.py index 965cbfb..8ce1de2 100644 --- a/src/components/tabs/components/MyTabs.py +++ b/src/components/tabs/components/MyTabs.py @@ -149,6 +149,9 @@ class MyTabs(BaseComponent): if active is not None: to_modify.active = active + def get_tab_content_by_key(self, key): + return self.tabs_by_key[key].content if key in self.tabs_by_key else None + def refresh(self): return self.render(oob=True) diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py index f8b57ee..8260354 100644 --- a/src/components/workflows/WorkflowsApp.py +++ b/src/components/workflows/WorkflowsApp.py @@ -24,3 +24,11 @@ def post(session, _id: str, tab_id: str, form_id: str, name: str, tab_boundaries f"Entering {Routes.AddWorkflow} with args {debug_session(session)}, {_id=}, {tab_id=}, {form_id=}, {name=}, {tab_boundaries=}") instance = InstanceManager.get(session, _id) return instance.add_new_workflow(tab_id, form_id, name, json.loads(tab_boundaries)) + + +@rt(Routes.ShowWorkflow) +def post(session, _id: str, name: str, tab_boundaries: str): + logger.debug( + 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)) diff --git a/src/components/workflows/commands.py b/src/components/workflows/commands.py index 0e2dc82..0148e92 100644 --- a/src/components/workflows/commands.py +++ b/src/components/workflows/commands.py @@ -20,3 +20,11 @@ class WorkflowsCommandManager(BaseCommandManager): "hx-target": f"#w_{self._id}", "hx-vals": f'js:{{"_id": "{self._id}", "tab_id": "{tab_id}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}', } + + def show_workflow(self, workflow_name): + return { + "hx_post": f"{ROUTE_ROOT}{Routes.ShowWorkflow}", + "hx-target": f"#{self._owner.tabs_manager.get_id()}", + "hx-swap": "outerHTML", + "hx-vals": f'js:{{"_id": "{self._id}", "name": "{workflow_name}", "tab_boundaries": getTabContentBoundaries("{self._owner.tabs_manager.get_id()}")}}', + } diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py index e69de29..54c4b49 100644 --- a/src/components/workflows/components/WorkflowDesigner.py +++ b/src/components/workflows/components/WorkflowDesigner.py @@ -0,0 +1,32 @@ +from fasthtml.components import * + +from components.BaseComponent import BaseComponent +from components.workflows.constants import WORKFLOW_DESIGNER_INSTANCE_ID +from components.workflows.db_management import WorkflowsDesignerSettings +from core.utils import get_unique_id + + +class WorkflowDesigner(BaseComponent): + def __init__(self, session, + _id=None, + settings_manager=None, + designer_settings: WorkflowsDesignerSettings = None, + boundaries: dict = None): + super().__init__(session, _id) + self._settings_manager = settings_manager + self._designer_settings = designer_settings + self.boundaries = boundaries + + def set_boundaries(self, boundaries: dict): + self.boundaries = boundaries + + def __ft__(self): + return Div(f"Workflow Designer - {self._designer_settings.workflow_name}") + + @staticmethod + def create_component_id(session, suffix=None): + prefix = f"{WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}" + if suffix is None: + suffix = get_unique_id() + + return f"{prefix}{suffix}" diff --git a/src/components/workflows/components/Workflows.py b/src/components/workflows/components/Workflows.py index 14e88a2..806ccd0 100644 --- a/src/components/workflows/components/Workflows.py +++ b/src/components/workflows/components/Workflows.py @@ -6,8 +6,9 @@ from assets.icons import icon_add_regular from components.BaseComponent import BaseComponentSingleton from components.form.components.MyForm import MyForm, FormField from components.workflows.commands import WorkflowsCommandManager +from components.workflows.components.WorkflowDesigner import WorkflowDesigner from components.workflows.constants import WORKFLOWS_INSTANCE_ID -from components.workflows.db_management import WorkflowsDbManager +from components.workflows.db_management import WorkflowsDbManager, WorkflowsDesignerSettings from components_helpers import mk_ellipsis, mk_icon from core.instance_manager import InstanceManager @@ -50,7 +51,7 @@ class Workflows(BaseComponentSingleton): self.tabs_manager.set_tab_content(tab_id, self._get_workflow_designer(workflow_name, tab_boundaries), title=workflow_name, - key=workflow_name, + key=f"{self._id}_{workflow_name}", active=True) return self._mk_workflows(), self.tabs_manager.refresh() @@ -62,6 +63,23 @@ class Workflows(BaseComponentSingleton): return self.tabs_manager.refresh() + def show_workflow(self, workflow_name: str, tab_boundaries: dict): + tab_key = f"{self._id}_{workflow_name}" + if tab_key not in self.tabs_manager.tabs: + self.tabs_manager.add_tab(workflow_name, + self._get_workflow_designer(workflow_name, tab_boundaries), + key=tab_key) + else: + workflow_designer = self.tabs_manager.get_tab_content_by_key(tab_key) + workflow_designer.set_boundaries(tab_boundaries) + + self.tabs_manager.select_tab_by_key(tab_key) + self.db.select_workflow(workflow_name) + return self.tabs_manager.refresh(), self.refresh() + + def refresh(self): + return self._mk_workflows(True) + def __ft__(self): return Div( Div(cls="divider"), @@ -79,7 +97,12 @@ class Workflows(BaseComponentSingleton): ) def _get_workflow_designer(self, workflow_name: str, tab_boundaries: dict): - return Div(f"Workflow Designer for {workflow_name}") + return InstanceManager.get(self._session, + WorkflowDesigner.create_component_id(self._session, workflow_name), + WorkflowDesigner, + settings_manager=self._settings_manager, + designer_settings=WorkflowsDesignerSettings(workflow_name=workflow_name), + boundaries=tab_boundaries) def _mk_add_workflow_form(self, tab_id: str): return InstanceManager.get(self._session, MyForm.create_component_id(self._session), MyForm, @@ -89,7 +112,14 @@ class Workflows(BaseComponentSingleton): ) def _mk_workflow(self, workflow_name: str, selected: bool): - return mk_ellipsis(workflow_name, cls="text-sm") + elt = mk_ellipsis(workflow_name, cls="text-sm", **self.commands.show_workflow(workflow_name)) + if selected: + return Div( + elt, + cls="items-center mmt-selected" + ) + else: + return elt def _mk_workflows(self, oob=False): return Div( diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py index 32f06bf..143af84 100644 --- a/src/components/workflows/constants.py +++ b/src/components/workflows/constants.py @@ -1,4 +1,5 @@ WORKFLOWS_INSTANCE_ID = "__Workflows__" +WORKFLOW_DESIGNER_INSTANCE_ID = "__WorkflowDesigner__" WORKFLOWS_SETTINGS_ENTRY = "Workflows" ROUTE_ROOT = "/workflows" @@ -6,3 +7,4 @@ ROUTE_ROOT = "/workflows" class Routes: AddWorkflow = "/add-workflow" SelectWorkflow = "/select-workflow" + ShowWorkflow = "/show-workflow" diff --git a/src/components/workflows/db_management.py b/src/components/workflows/db_management.py index 5cbc6dd..f0ab673 100644 --- a/src/components/workflows/db_management.py +++ b/src/components/workflows/db_management.py @@ -6,6 +6,11 @@ from core.settings_management import SettingsManager logger = logging.getLogger("WorkflowsSettings") +@dataclass +class WorkflowsDesignerSettings: + workflow_name: str + + @dataclass class WorkflowsSettings: diff --git a/tests/tests_workflows_db_manager.py b/tests/tests_workflows_db_manager.py new file mode 100644 index 0000000..965ad19 --- /dev/null +++ b/tests/tests_workflows_db_manager.py @@ -0,0 +1,152 @@ +from unittest.mock import patch + +import pytest + +from components.workflows.constants import WORKFLOWS_SETTINGS_ENTRY +from components.workflows.db_management import WorkflowsDbManager, WorkflowsSettings +from core.settings_management import SettingsManager, MemoryDbEngine + +USER_EMAIL = "test@mail.com" +USER_ID = "test_user" + + +@pytest.fixture +def session(): + return {"user_id": USER_ID, "user_email": USER_EMAIL} + + +@pytest.fixture +def settings_manager(): + return SettingsManager(engine=MemoryDbEngine()) + + +@pytest.fixture +def workflows_db_manager(session, settings_manager): + return WorkflowsDbManager(session=session, settings_manager=settings_manager) + + +def test_add_workflow(workflows_db_manager): + # Test adding a new workflow + assert workflows_db_manager.add_workflow("workflow1") is True + + # Verify workflow was added + workflows = workflows_db_manager.get_workflows() + assert "workflow1" in workflows + assert len(workflows) == 1 + + +def test_add_workflow_empty_name(workflows_db_manager): + # Test adding a workflow with empty name raises ValueError + with pytest.raises(ValueError, match="Workflow name cannot be empty."): + workflows_db_manager.add_workflow("") + + +def test_add_workflow_duplicate(workflows_db_manager): + # Add a workflow + workflows_db_manager.add_workflow("workflow1") + + # Test adding duplicate workflow raises ValueError + with pytest.raises(ValueError, match="Workflow 'workflow1' already exists."): + workflows_db_manager.add_workflow("workflow1") + + +def test_get_workflow(workflows_db_manager): + # Add a workflow + workflows_db_manager.add_workflow("workflow1") + + # Test getting the workflow + workflow = workflows_db_manager.get_workflow("workflow1") + assert workflow == "workflow1" + + +def test_get_workflow_empty_name(workflows_db_manager): + # Test getting a workflow with empty name raises ValueError + with pytest.raises(ValueError, match="Workflow name cannot be empty."): + workflows_db_manager.get_workflow("") + + +def test_get_workflow_nonexistent(workflows_db_manager): + # Test getting a non-existent workflow raises ValueError + with pytest.raises(ValueError, match="Workflow 'nonexistent' does not exist."): + workflows_db_manager.get_workflow("nonexistent") + + +def test_remove_workflow(workflows_db_manager): + # Add a workflow + workflows_db_manager.add_workflow("workflow1") + + # Test removing the workflow + assert workflows_db_manager.remove_workflow("workflow1") is True + + # Verify workflow was removed + assert len(workflows_db_manager.get_workflows()) == 0 + + +def test_remove_workflow_empty_name(workflows_db_manager): + # Test removing a workflow with empty name raises ValueError + with pytest.raises(ValueError, match="Workflow name cannot be empty."): + workflows_db_manager.remove_workflow("") + + +def test_remove_workflow_nonexistent(workflows_db_manager): + # Test removing a non-existent workflow raises ValueError + with pytest.raises(ValueError, match="workflow 'nonexistent' does not exist."): + workflows_db_manager.remove_workflow("nonexistent") + + +def test_exists_workflow(workflows_db_manager): + # Add a workflow + workflows_db_manager.add_workflow("workflow1") + + # Test workflow exists + assert workflows_db_manager.exists_workflow("workflow1") is True + + # Test non-existent workflow + assert workflows_db_manager.exists_workflow("nonexistent") is False + + +def test_exists_workflow_empty_name(workflows_db_manager): + # Test checking existence of workflow with empty name raises ValueError + with pytest.raises(ValueError, match="workflow name cannot be empty."): + workflows_db_manager.exists_workflow("") + + +def test_get_workflows(workflows_db_manager): + # Initially, no workflows + assert len(workflows_db_manager.get_workflows()) == 0 + + # Add workflows + workflows_db_manager.add_workflow("workflow1") + workflows_db_manager.add_workflow("workflow2") + + # Test getting all workflows + workflows = workflows_db_manager.get_workflows() + assert "workflow1" in workflows + assert "workflow2" in workflows + assert len(workflows) == 2 + + +def test_select_workflow(workflows_db_manager): + # Add a workflow + workflows_db_manager.add_workflow("workflow1") + + # Select the workflow + workflows_db_manager.select_workflow("workflow1") + + # Verify workflow was selected + assert workflows_db_manager.get_selected_workflow() == "workflow1" + + +def test_get_selected_workflow_none(workflows_db_manager): + # Initially, no selected workflow + assert workflows_db_manager.get_selected_workflow() is None + + +def test_get_settings_default(workflows_db_manager, session, settings_manager): + # Test _get_settings returns default settings when none exist + with patch.object(settings_manager, 'load', return_value=WorkflowsSettings()) as mock_load: + settings = workflows_db_manager._get_settings() + mock_load.assert_called_once_with(session, WORKFLOWS_SETTINGS_ENTRY, default=WorkflowsSettings()) + assert isinstance(settings, WorkflowsSettings) + assert settings.workflows == [] + assert settings.selected_workflow is None