Improved command management to reduce the number of instances

This commit is contained in:
2025-12-21 11:14:02 +01:00
parent 9f69a6bc5b
commit 2f808ed226
8 changed files with 100 additions and 19 deletions

View File

@@ -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
)

View File

@@ -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):

View File

@@ -40,7 +40,7 @@ class PanelIds:
@dataclass
class PanelConf:
left: bool = True
left: bool = False
right: bool = True

View File

@@ -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.

View File

@@ -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:

View File

@@ -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

View File

@@ -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):

View File

@@ -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"))