From 2f808ed226e98738a1cf476e1f1dda8a1d9118b0 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sun, 21 Dec 2025 11:14:02 +0100 Subject: [PATCH] Improved command management to reduce the number of instances --- src/myfasthtml/controls/DataGrid.py | 2 +- src/myfasthtml/controls/DataGridsManager.py | 31 +++++++++++--- src/myfasthtml/controls/Panel.py | 2 +- src/myfasthtml/controls/TabsManager.py | 24 ++++++++--- src/myfasthtml/controls/TreeView.py | 6 +++ src/myfasthtml/core/commands.py | 46 ++++++++++++++++++++- src/myfasthtml/core/utils.py | 2 +- tests/controls/test_panel.py | 6 +-- 8 files changed, 100 insertions(+), 19 deletions(-) diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 53aba86..478ab04 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -57,7 +57,7 @@ class DataGrid(MultipleInstance): def render(self): return Div( - NotStr(self._state.html), + NotStr(self._state.html) if self._state.html else "Content lost !", id=self._id ) diff --git a/src/myfasthtml/controls/DataGridsManager.py b/src/myfasthtml/controls/DataGridsManager.py index bdb9ef1..29ce8c5 100644 --- a/src/myfasthtml/controls/DataGridsManager.py +++ b/src/myfasthtml/controls/DataGridsManager.py @@ -1,3 +1,4 @@ +import uuid from dataclasses import dataclass import pandas as pd @@ -10,7 +11,7 @@ from myfasthtml.controls.Panel import Panel from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TreeView import TreeView, TreeNode from myfasthtml.controls.helpers import mk -from myfasthtml.core.commands import Command, LambdaCommand +from myfasthtml.core.commands import Command from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.instances import MultipleInstance, InstancesManager from myfasthtml.icons.fluent_p1 import table_add20_regular @@ -19,6 +20,7 @@ from myfasthtml.icons.fluent_p3 import folder_open20_regular @dataclass class DocumentDefinition: + document_id: str namespace: str name: str type: str @@ -30,7 +32,7 @@ class DataGridsState(DbObject): def __init__(self, owner, name=None): super().__init__(owner, name=name) with self.initializing(): - self.elements: list = [] + self.elements: list = [DocumentDefinition] class Commands(BaseCommands): @@ -61,7 +63,11 @@ class Commands(BaseCommands): self._owner.clear_tree).htmx(target=f"#{self._owner._tree.get_id()}") def show_document(self): - return LambdaCommand(self._owner, lambda: print("show_document")) + return Command("ShowDocument", + "Show document", + self._owner, + self._owner.select_document, + key="SelectNode") class DataGridsManager(MultipleInstance): @@ -89,18 +95,29 @@ class DataGridsManager(MultipleInstance): dg = DataGrid(self._tabs_manager) dg.set_html(html) document = DocumentDefinition( + document_id=str(uuid.uuid4()), namespace=file_upload.get_file_basename(), name=file_upload.get_sheet_name(), type="excel", tab_id=tab_id, datagrid_id=dg.get_id() ) - self._state.elements = self._state.elements + [document] + self._state.elements = self._state.elements + [document] # do not use append() other it won't be saved parent_id = self._tree.ensure_path(document.namespace) tree_node = TreeNode(label=document.name, type="excel", parent=parent_id) self._tree.add_node(tree_node, parent_id=parent_id) return self._mk_tree(), self._tabs_manager.change_tab_content(tab_id, document.name, Panel(self).set_main(dg)) + def select_document(self, node_id): + document_id = self._tree.get_bag(node_id) + try: + document = next(filter(lambda x: x.document_id == document_id, self._state.elements)) + dg = DataGrid(self._tabs_manager, _id=document.datagrid_id) + return self._tabs_manager.show_or_create_tab(document.tab_id, document.name, dg) + except StopIteration: + # the selected node is not a document (it's a folder) + return None + def clear_tree(self): self._state.elements = [] self._tree.clear() @@ -117,7 +134,11 @@ class DataGridsManager(MultipleInstance): tree = TreeView(self, _id="-treeview") for element in self._state.elements: parent_id = tree.ensure_path(element.namespace) - tree.add_node(TreeNode(label=element.name, type=element.type, parent=parent_id, id=element.datagrid_id)) + tree.add_node(TreeNode(id=element.document_id, + label=element.name, + type=element.type, + parent=parent_id, + bag=element.document_id)) return tree def render(self): diff --git a/src/myfasthtml/controls/Panel.py b/src/myfasthtml/controls/Panel.py index d7242b6..d27e46d 100644 --- a/src/myfasthtml/controls/Panel.py +++ b/src/myfasthtml/controls/Panel.py @@ -40,7 +40,7 @@ class PanelIds: @dataclass class PanelConf: - left: bool = True + left: bool = False right: bool = True diff --git a/src/myfasthtml/controls/TabsManager.py b/src/myfasthtml/controls/TabsManager.py index 9935fff..2eb82a9 100644 --- a/src/myfasthtml/controls/TabsManager.py +++ b/src/myfasthtml/controls/TabsManager.py @@ -58,29 +58,34 @@ class TabsManagerState(DbObject): class Commands(BaseCommands): def show_tab(self, tab_id): - return Command(f"{self._prefix}ShowTab", + return Command(f"ShowTab", "Activate or show a specific tab", self._owner, self._owner.show_tab, args=[tab_id, True, - False]).htmx(target=f"#{self._id}-controller", swap="outerHTML") + False], + key=f"{self._owner.get_full_id()}-ShowTab-{tab_id}", + ).htmx(target=f"#{self._id}-controller", swap="outerHTML") def close_tab(self, tab_id): - return Command(f"{self._prefix}CloseTab", + return Command(f"CloseTab", "Close a specific tab", self._owner, self._owner.close_tab, - args=[tab_id]).htmx(target=f"#{self._id}-controller", swap="outerHTML") + kwargs={"tab_id": tab_id}, + ).htmx(target=f"#{self._id}-controller", swap="outerHTML") def add_tab(self, label: str, component: Any, auto_increment=False): - return Command(f"{self._prefix}AddTab", + return Command(f"AddTab", "Add a new tab", self._owner, self._owner.on_new_tab, args=[label, component, - auto_increment]).htmx(target=f"#{self._id}-controller", swap="outerHTML") + auto_increment], + key="#{id-name-args}", + ).htmx(target=f"#{self._id}-controller", swap="outerHTML") class TabsManager(MultipleInstance): @@ -148,6 +153,13 @@ class TabsManager(MultipleInstance): tab_id = self.create_tab(label, component) return self.show_tab(tab_id, oob=False) + def show_or_create_tab(self, tab_id, label, component, activate=True): + logger.debug(f"show_or_create_tab {tab_id=}, {label=}, {component=}, {activate=}") + if tab_id not in self._state.tabs: + self._add_or_update_tab(tab_id, label, component, activate) + + return self.show_tab(tab_id, activate=activate, oob=True) + def create_tab(self, label: str, component: Any, activate: bool = True) -> str: """ Add a new tab or update an existing one with the same component type, ID and label. diff --git a/src/myfasthtml/controls/TreeView.py b/src/myfasthtml/controls/TreeView.py index b5dfea3..0a7aeda 100644 --- a/src/myfasthtml/controls/TreeView.py +++ b/src/myfasthtml/controls/TreeView.py @@ -267,6 +267,12 @@ class TreeView(MultipleInstance): self._state.update(state) return self + def get_bag(self, node_id: str): + try: + return self._state.items[node_id].bag + except KeyError: + return None + def _toggle_node(self, node_id: str): """Toggle expand/collapse state of a node.""" if node_id in self._state.opened: diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py index c24eab5..ff07149 100644 --- a/src/myfasthtml/core/commands.py +++ b/src/myfasthtml/core/commands.py @@ -1,5 +1,6 @@ import inspect import json +import logging import uuid from typing import Optional @@ -8,6 +9,8 @@ from myutils.observable import NotObservableError, ObservableResultCollector from myfasthtml.core.constants import Routes, ROUTE_ROOT from myfasthtml.core.utils import flatten +logger = logging.getLogger("Commands") + class Command: """ @@ -26,6 +29,33 @@ class Command: :type description: str """ + @staticmethod + def process_key(key, name, owner, args, kwargs): + def _compute_from_args(): + res = [] + for arg in args: + if hasattr(arg, "get_full_id"): + res.append(arg.get_full_id()) + else: + res.append(str(arg)) + return "-".join(res) + + # special management when kwargs are provided + # In this situation, + # either there is no parameter (so one single instance of the command is enough) + # or the parameter is a kwargs (so the parameters are provided when the command is called) + if (key is None + and owner is not None + and args is None # args is not provided + ): + key = f"{owner.get_full_id()}-{name}" + + key = key.replace("#{args}", _compute_from_args()) + key = key.replace("#{id}", owner.get_full_id()) + key = key.replace("#{id-name-args}", f"{owner.get_full_id()}-{name}-{_compute_from_args()}") + + return key + def __init__(self, name, description, owner=None, @@ -47,10 +77,20 @@ class Command: self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {} self._key = key + # special management when kwargs are provided + # In this situation, + # either there is no parameter (so one single instance of the command is enough) + # or the parameter is a kwargs (so the parameters are provided when the command is called) + if (self._key is None + and self.owner is not None + and args is None # args is not provided + ): + self._key = f"{owner.get_full_id()}-{name}" + # register the command if auto_register: - if key in CommandsManager.commands_by_key: - self.id = CommandsManager.commands_by_key[key].id + if self._key in CommandsManager.commands_by_key: + self.id = CommandsManager.commands_by_key[self._key].id else: CommandsManager.register(self) @@ -78,6 +118,7 @@ class Command: return res def execute(self, client_response: dict = None): + logger.debug(f"Executing command {self.name}") with ObservableResultCollector(self._bindings) as collector: kwargs = self._create_kwargs(self.default_kwargs, client_response, @@ -87,6 +128,7 @@ class Command: ret_from_bound_commands = [] if self.owner: for command in self.owner.get_bound_commands(self.name): + logger.debug(f" will execute bound command {command.name}...") r = command.execute(client_response) ret_from_bound_commands.append(r) # it will be flatten if needed later diff --git a/src/myfasthtml/core/utils.py b/src/myfasthtml/core/utils.py index addfea4..64bb40c 100644 --- a/src/myfasthtml/core/utils.py +++ b/src/myfasthtml/core/utils.py @@ -12,7 +12,7 @@ from myfasthtml.core.constants import Routes, ROUTE_ROOT from myfasthtml.test.MyFT import MyFT utils_app, utils_rt = fast_app() -logger = logging.getLogger("Commands") +logger = logging.getLogger("Routing") def mount_if_not_exists(app, path: str, sub_app): diff --git a/tests/controls/test_panel.py b/tests/controls/test_panel.py index 4c95872..44b29ce 100644 --- a/tests/controls/test_panel.py +++ b/tests/controls/test_panel.py @@ -24,7 +24,7 @@ class TestPanelBehaviour: panel = Panel(root_instance) assert panel is not None - assert panel.conf.left is True + assert panel.conf.left is False assert panel.conf.right is True def test_i_can_create_panel_with_custom_config(self, root_instance): @@ -157,7 +157,7 @@ class TestPanelBehaviour: """Test that update_side_width() returns a panel element.""" panel = Panel(root_instance) - result = panel.update_side_width("left", 300) + result = panel.update_side_width("right", 300) assert result is not None @@ -196,7 +196,7 @@ class TestPanelRender: @pytest.fixture def panel(self, root_instance): - panel = Panel(root_instance) + panel = Panel(root_instance, PanelConf(True, True)) panel.set_main(Div("Main content")) panel.set_left(Div("Left content")) panel.set_right(Div("Right content"))