2 Commits

Author SHA1 Message Date
b26abc4257 Working on Datagrid interaction 2025-12-08 22:39:26 +01:00
045f01b48a Refactored Command to add owner 2025-12-08 21:07:34 +01:00
22 changed files with 174 additions and 118 deletions

View File

@@ -17,6 +17,7 @@ class Commands(BaseCommands):
def update_boundaries(self): def update_boundaries(self):
return Command(f"{self._prefix}UpdateBoundaries", return Command(f"{self._prefix}UpdateBoundaries",
"Update component boundaries", "Update component boundaries",
self._owner,
self._owner.update_boundaries).htmx(target=f"{self._owner.get_id()}") self._owner.update_boundaries).htmx(target=f"{self._owner.get_id()}")

View File

@@ -1,9 +1,10 @@
from typing import Optional from typing import Optional
from fastcore.basics import NotStr
from fasthtml.components import Div from fasthtml.components import Div
from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, DataGridFooterConf, \ from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance from myfasthtml.core.instances import MultipleInstance
@@ -24,6 +25,7 @@ class DatagridState(DbObject):
self.filtered: dict = {} self.filtered: dict = {}
self.edition: DatagridEditionState = DatagridEditionState() self.edition: DatagridEditionState = DatagridEditionState()
self.selection: DatagridSelectionState = DatagridSelectionState() self.selection: DatagridSelectionState = DatagridSelectionState()
self.html = None
class DatagridSettings(DbObject): class DatagridSettings(DbObject):
@@ -46,13 +48,17 @@ class Commands(BaseCommands):
class DataGrid(MultipleInstance): class DataGrid(MultipleInstance):
def __init__(self, parent, settings=None, _id=None): def __init__(self, parent, settings=None, _id=None):
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self._settings = DatagridSettings(self).update(settings) self._settings = DatagridSettings(self)
self._state = DatagridState(self) self._state = DatagridState(self)
self.commands = Commands(self) self.commands = Commands(self)
def set_html(self, html):
self._state.html = html
def render(self): def render(self):
return Div( return Div(
self._id NotStr(self._state.html),
id=self._id
) )
def __ft__(self): def __ft__(self):

View File

@@ -1,11 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
import pandas as pd import pandas as pd
from fastcore.basics import NotStr
from fasthtml.components import Div from fasthtml.components import Div
from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.DataGrid import DataGrid
from myfasthtml.controls.FileUpload import FileUpload from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.Panel import Panel
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.TreeView import TreeView, TreeNode from myfasthtml.controls.TreeView import TreeView, TreeNode
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
@@ -36,16 +37,19 @@ class Commands(BaseCommands):
def upload_from_source(self): def upload_from_source(self):
return Command("UploadFromSource", return Command("UploadFromSource",
"Upload from source", "Upload from source",
self._owner,
self._owner.upload_from_source).htmx(target=None) self._owner.upload_from_source).htmx(target=None)
def new_grid(self): def new_grid(self):
return Command("NewGrid", return Command("NewGrid",
"New grid", "New grid",
self._owner,
self._owner.new_grid) self._owner.new_grid)
def open_from_excel(self, tab_id, file_upload): def open_from_excel(self, tab_id, file_upload):
return Command("OpenFromExcel", return Command("OpenFromExcel",
"Open from Excel", "Open from Excel",
self._owner,
self._owner.open_from_excel, self._owner.open_from_excel,
tab_id, tab_id,
file_upload).htmx(target=f"#{self._owner._tree.get_id()}") file_upload).htmx(target=f"#{self._owner._tree.get_id()}")
@@ -53,6 +57,7 @@ class Commands(BaseCommands):
def clear_tree(self): def clear_tree(self):
return Command("ClearTree", return Command("ClearTree",
"Clear tree", "Clear tree",
self._owner,
self._owner.clear_tree).htmx(target=f"#{self._owner._tree.get_id()}") self._owner.clear_tree).htmx(target=f"#{self._owner._tree.get_id()}")
@@ -72,8 +77,10 @@ class DataGridsManager(MultipleInstance):
def open_from_excel(self, tab_id, file_upload: FileUpload): def open_from_excel(self, tab_id, file_upload: FileUpload):
excel_content = file_upload.get_content() excel_content = file_upload.get_content()
df = pd.read_excel(excel_content) df = pd.read_excel(excel_content, file_upload.get_sheet_name())
content = df.to_html(index=False) html = df.to_html(index=False)
dg = DataGrid(self._tabs_manager)
dg.set_html(html)
document = DocumentDefinition( document = DocumentDefinition(
namespace=file_upload.get_file_basename(), namespace=file_upload.get_file_basename(),
name=file_upload.get_sheet_name(), name=file_upload.get_sheet_name(),
@@ -85,7 +92,7 @@ class DataGridsManager(MultipleInstance):
parent_id = self._tree.ensure_path(document.namespace) parent_id = self._tree.ensure_path(document.namespace)
tree_node = TreeNode(label=document.name, type="excel", parent=parent_id) tree_node = TreeNode(label=document.name, type="excel", parent=parent_id)
self._tree.add_node(tree_node, parent_id=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, NotStr(content)) return self._mk_tree(), self._tabs_manager.change_tab_content(tab_id, document.name, Panel(self).set_main(dg))
def clear_tree(self): def clear_tree(self):
self._state.elements = [] self._state.elements = []

View File

@@ -10,10 +10,16 @@ from myfasthtml.core.instances import MultipleInstance
class Commands(BaseCommands): class Commands(BaseCommands):
def close(self): def close(self):
return Command("Close", "Close Dropdown", self._owner.close).htmx(target=f"#{self._owner.get_id()}-content") return Command("Close",
"Close Dropdown",
self._owner,
self._owner.close).htmx(target=f"#{self._owner.get_id()}-content")
def click(self): def click(self):
return Command("Click", "Click on Dropdown", self._owner.on_click).htmx(target=f"#{self._owner.get_id()}-content") return Command("Click",
"Click on Dropdown",
self._owner,
self._owner.on_click).htmx(target=f"#{self._owner.get_id()}-content")
class DropdownState: class DropdownState:

View File

@@ -34,10 +34,16 @@ class Commands(BaseCommands):
super().__init__(owner) super().__init__(owner)
def on_file_uploaded(self): def on_file_uploaded(self):
return Command("UploadFile", "Upload file", self._owner.upload_file).htmx(target=f"#sn_{self._id}") return Command("UploadFile",
"Upload file",
self._owner,
self._owner.upload_file).htmx(target=f"#sn_{self._id}")
def on_sheet_selected(self): def on_sheet_selected(self):
return Command("SheetSelected", "Sheet selected", self._owner.select_sheet).htmx(target=f"#sn_{self._id}") return Command("SheetSelected",
"Sheet selected",
self._owner,
self._owner.select_sheet).htmx(target=f"#sn_{self._id}")
class FileUpload(MultipleInstance): class FileUpload(MultipleInstance):

View File

@@ -12,6 +12,7 @@ class InstancesDebugger(SingleInstance):
self._panel = Panel(self, _id="-panel") self._panel = Panel(self, _id="-panel")
self._command = Command("ShowInstance", self._command = Command("ShowInstance",
"Display selected Instance", "Display selected Instance",
self,
self.on_network_event).htmx(target=f"#{self._panel.get_id()}_r") self.on_network_event).htmx(target=f"#{self._panel.get_id()}_r")
def render(self): def render(self):

View File

@@ -37,7 +37,10 @@ class LayoutState(DbObject):
class Commands(BaseCommands): class Commands(BaseCommands):
def toggle_drawer(self, side: Literal["left", "right"]): def toggle_drawer(self, side: Literal["left", "right"]):
return Command("ToggleDrawer", f"Toggle {side} layout drawer", self._owner.toggle_drawer, side) return Command("ToggleDrawer",
f"Toggle {side} layout drawer",
self._owner,
self._owner.toggle_drawer, side)
def update_drawer_width(self, side: Literal["left", "right"], width: int = None): def update_drawer_width(self, side: Literal["left", "right"], width: int = None):
""" """
@@ -50,12 +53,11 @@ class Commands(BaseCommands):
Returns: Returns:
Command: Command object for updating drawer width Command: Command object for updating drawer width
""" """
return Command( return Command(f"UpdateDrawerWidth_{side}",
f"UpdateDrawerWidth_{side}", f"Update {side} drawer width",
f"Update {side} drawer width", self._owner,
self._owner.update_drawer_width, self._owner.update_drawer_width,
side side)
)
class Layout(SingleInstance): class Layout(SingleInstance):

View File

@@ -17,7 +17,11 @@ class PanelConf:
class Commands(BaseCommands): class Commands(BaseCommands):
def toggle_side(self, side: Literal["left", "right"]): def toggle_side(self, side: Literal["left", "right"]):
return Command("TogglePanelSide", f"Toggle {side} side panel", self._owner.toggle_side, side) return Command("TogglePanelSide",
f"Toggle {side} side panel",
self._owner,
self._owner.toggle_side,
side)
def update_side_width(self, side: Literal["left", "right"]): def update_side_width(self, side: Literal["left", "right"]):
""" """
@@ -29,12 +33,11 @@ class Commands(BaseCommands):
Returns: Returns:
Command: Command object for updating panel's side width Command: Command object for updating panel's side width
""" """
return Command( return Command(f"UpdatePanelSideWidth_{side}",
f"UpdatePanelSideWidth_{side}", f"Update {side} side panel width",
f"Update {side} side panel width", self._owner,
self._owner.update_side_width, self._owner.update_side_width,
side side)
)
class Panel(MultipleInstance): class Panel(MultipleInstance):

View File

@@ -14,10 +14,12 @@ logger = logging.getLogger("Search")
class Commands(BaseCommands): class Commands(BaseCommands):
def search(self): def search(self):
return (Command("Search", f"Search {self._owner.items_names}", self._owner.on_search). return (Command("Search",
htmx(target=f"#{self._owner.get_id()}-results", f"Search {self._owner.items_names}",
trigger="keyup changed delay:300ms", self._owner,
swap="innerHTML")) self._owner.on_search).htmx(target=f"#{self._owner.get_id()}-results",
trigger="keyup changed delay:300ms",
swap="innerHTML"))
class Search(MultipleInstance): class Search(MultipleInstance):

View File

@@ -60,6 +60,7 @@ class Commands(BaseCommands):
def show_tab(self, tab_id): def show_tab(self, tab_id):
return Command(f"{self._prefix}ShowTab", return Command(f"{self._prefix}ShowTab",
"Activate or show a specific tab", "Activate or show a specific tab",
self._owner,
self._owner.show_tab, self._owner.show_tab,
tab_id, tab_id,
True, True,
@@ -68,12 +69,14 @@ class Commands(BaseCommands):
def close_tab(self, tab_id): def close_tab(self, tab_id):
return Command(f"{self._prefix}CloseTab", return Command(f"{self._prefix}CloseTab",
"Close a specific tab", "Close a specific tab",
self._owner,
self._owner.close_tab, self._owner.close_tab,
tab_id).htmx(target=f"#{self._id}-controller", swap="outerHTML") tab_id).htmx(target=f"#{self._id}-controller", swap="outerHTML")
def add_tab(self, label: str, component: Any, auto_increment=False): def add_tab(self, label: str, component: Any, auto_increment=False):
return Command(f"{self._prefix}AddTab", return Command(f"{self._prefix}AddTab",
"Add a new tab", "Add a new tab",
self._owner,
self._owner.on_new_tab, self._owner.on_new_tab,
label, label,
component, component,
@@ -108,10 +111,10 @@ class TabsManager(MultipleInstance):
if tab_id not in self._state.tabs: if tab_id not in self._state.tabs:
return Div("Tab not found.") return Div("Tab not found.")
tab_config = self._state.tabs[tab_id] tab_config = self._state.tabs[tab_id]
if tab_config["component_type"] is None: if tab_config["component"] is None:
return Div("Tab content does not support serialization.") return Div("Tab content does not support serialization.")
try: try:
return InstancesManager.get(self._session, tab_config["component_id"]) return InstancesManager.get(self._session, tab_config["component"][1])
except Exception as e: except Exception as e:
logger.error(f"Error while retrieving tab content: {e}") logger.error(f"Error while retrieving tab content: {e}")
return Div("Failed to retrieve tab content.") return Div("Failed to retrieve tab content.")
@@ -381,8 +384,7 @@ class TabsManager(MultipleInstance):
if component_id is not None: if component_id is not None:
for tab_id, tab_data in self._state.tabs.items(): for tab_id, tab_data in self._state.tabs.items():
if (tab_data.get('component_type') == component_type and if (tab_data.get('component') == (component_type, component_id) and
tab_data.get('component_id') == component_id and
tab_data.get('label') == label): tab_data.get('label') == label):
return tab_id return tab_id
@@ -396,16 +398,21 @@ class TabsManager(MultipleInstance):
# Extract component ID if the component has a get_id() method # Extract component ID if the component has a get_id() method
component_type, component_id = None, None component_type, component_id = None, None
parent_type, parent_id = None, None
if isinstance(component, BaseInstance): if isinstance(component, BaseInstance):
component_type = component.get_prefix() component_type = component.get_prefix()
component_id = component.get_id() component_id = component.get_id()
parent = component.get_parent()
if parent:
parent_type = parent.get_prefix()
parent_id = parent.get_id()
# Add tab metadata to state # Add tab metadata to state
state.tabs[tab_id] = { state.tabs[tab_id] = {
'id': tab_id, 'id': tab_id,
'label': label, 'label': label,
'component_type': component_type, 'component': (component_type, component_id) if component_type else None,
'component_id': component_id 'component_parent': (parent_type, parent_id) if parent_type else None
} }
# Add tab to order list # Add tab to order list

View File

@@ -37,6 +37,7 @@ class TreeNode:
type: str = "default" type: str = "default"
parent: Optional[str] = None parent: Optional[str] = None
children: list[str] = field(default_factory=list) children: list[str] = field(default_factory=list)
bag: Optional[dict] = None # to keep extra info
class TreeViewState(DbObject): class TreeViewState(DbObject):
@@ -66,74 +67,67 @@ class Commands(BaseCommands):
def toggle_node(self, node_id: str): def toggle_node(self, node_id: str):
"""Create command to expand/collapse a node.""" """Create command to expand/collapse a node."""
return Command( return Command("ToggleNode",
"ToggleNode", f"Toggle node {node_id}",
f"Toggle node {node_id}", self._owner,
self._owner._toggle_node, self._owner._toggle_node,
node_id node_id).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
def add_child(self, parent_id: str): def add_child(self, parent_id: str):
"""Create command to add a child node.""" """Create command to add a child node."""
return Command( return Command("AddChild",
"AddChild", f"Add child to {parent_id}",
f"Add child to {parent_id}", self._owner,
self._owner._add_child, self._owner._add_child,
parent_id parent_id).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
def add_sibling(self, node_id: str): def add_sibling(self, node_id: str):
"""Create command to add a sibling node.""" """Create command to add a sibling node."""
return Command( return Command("AddSibling",
"AddSibling", f"Add sibling to {node_id}",
f"Add sibling to {node_id}", self._owner,
self._owner._add_sibling, self._owner._add_sibling,
node_id node_id
).htmx(target=f"#{self._owner.get_id()}") ).htmx(target=f"#{self._owner.get_id()}")
def start_rename(self, node_id: str): def start_rename(self, node_id: str):
"""Create command to start renaming a node.""" """Create command to start renaming a node."""
return Command( return Command("StartRename",
"StartRename", f"Start renaming {node_id}",
f"Start renaming {node_id}", self._owner,
self._owner._start_rename, self._owner._start_rename,
node_id node_id).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
def save_rename(self, node_id: str): def save_rename(self, node_id: str):
"""Create command to save renamed node.""" """Create command to save renamed node."""
return Command( return Command("SaveRename",
"SaveRename", f"Save rename for {node_id}",
f"Save rename for {node_id}", self._owner,
self._owner._save_rename, self._owner._save_rename,
node_id node_id).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
def cancel_rename(self): def cancel_rename(self):
"""Create command to cancel renaming.""" """Create command to cancel renaming."""
return Command( return Command("CancelRename",
"CancelRename", "Cancel rename",
"Cancel rename", self._owner,
self._owner._cancel_rename self._owner._cancel_rename).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
def delete_node(self, node_id: str): def delete_node(self, node_id: str):
"""Create command to delete a node.""" """Create command to delete a node."""
return Command( return Command("DeleteNode",
"DeleteNode", f"Delete node {node_id}",
f"Delete node {node_id}", self._owner,
self._owner._delete_node, self._owner._delete_node,
node_id node_id).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
def select_node(self, node_id: str): def select_node(self, node_id: str):
"""Create command to select a node.""" """Create command to select a node."""
return Command( return Command("SelectNode",
"SelectNode", f"Select node {node_id}",
f"Select node {node_id}", self._owner,
self._owner._select_node, self._owner._select_node,
node_id node_id).htmx(target=f"#{self._owner.get_id()}")
).htmx(target=f"#{self._owner.get_id()}")
class TreeView(MultipleInstance): class TreeView(MultipleInstance):
@@ -187,7 +181,7 @@ class TreeView(MultipleInstance):
self._state.items[node.id] = node self._state.items[node.id] = node
if parent_id is None and node.parent is not None: if parent_id is None and node.parent is not None:
parent_id = node.parent parent_id = node.parent
node.parent = parent_id node.parent = parent_id
if parent_id and parent_id in self._state.items: if parent_id and parent_id in self._state.items:

View File

@@ -33,7 +33,10 @@ class UserProfileState:
class Commands(BaseCommands): class Commands(BaseCommands):
def update_dark_mode(self): def update_dark_mode(self):
return Command("UpdateDarkMode", "Set the dark mode", self._owner.update_dark_mode).htmx(target=None) return Command("UpdateDarkMode",
"Set the dark mode",
self._owner,
self._owner.update_dark_mode).htmx(target=None)
class UserProfile(SingleInstance): class UserProfile(SingleInstance):

View File

@@ -25,10 +25,11 @@ class BaseCommand:
:type description: str :type description: str
""" """
def __init__(self, name, description, auto_register=True): def __init__(self, name, description, owner=None, auto_register=True):
self.id = uuid.uuid4() self.id = uuid.uuid4()
self.name = name self.name = name
self.description = description self.description = description
self.owner = owner
self._htmx_extra = {} self._htmx_extra = {}
self._bindings = [] self._bindings = []
self._ft = None self._ft = None
@@ -133,8 +134,8 @@ class Command(BaseCommand):
:type kwargs: dict :type kwargs: dict
""" """
def __init__(self, name, description, callback, *args, **kwargs): def __init__(self, name, description, owner, callback, *args, **kwargs):
super().__init__(name, description) super().__init__(name, description, owner=owner)
self.callback = callback self.callback = callback
self.callback_parameters = dict(inspect.signature(callback).parameters) if callback else {} self.callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
self.args = args self.args = args
@@ -202,8 +203,8 @@ class Command(BaseCommand):
class LambdaCommand(Command): class LambdaCommand(Command):
def __init__(self, delegate, name="LambdaCommand", description="Lambda Command"): def __init__(self, owner, delegate, name="LambdaCommand", description="Lambda Command"):
super().__init__(name, description, delegate) super().__init__(name, description, owner, delegate)
self.htmx(target=None) self.htmx(target=None)
def execute(self, client_response: dict = None): def execute(self, client_response: dict = None):

View File

@@ -98,6 +98,8 @@ class DbObject:
properties = {} properties = {}
if args: if args:
arg = args[0] arg = args[0]
if arg is None:
return self
if not isinstance(arg, (dict, SimpleNamespace)): if not isinstance(arg, (dict, SimpleNamespace)):
raise ValueError("Only dict or Expando are allowed as argument") raise ValueError("Only dict or Expando are allowed as argument")
properties |= vars(arg) if isinstance(arg, SimpleNamespace) else arg properties |= vars(arg) if isinstance(arg, SimpleNamespace) else arg

View File

@@ -49,8 +49,14 @@ def get():
mk.manage_binding(datalist, Binding(data)) mk.manage_binding(datalist, Binding(data))
mk.manage_binding(label_elt, Binding(data)) mk.manage_binding(label_elt, Binding(data))
add_button = mk.button("Add", command=Command("Add", "Add a suggestion", add_suggestion).bind(data)) add_button = mk.button("Add", command=Command("Add",
remove_button = mk.button("Remove", command=Command("Remove", "Remove a suggestion", remove_suggestion).bind(data)) "Add a suggestion",
None,
add_suggestion).bind(data))
remove_button = mk.button("Remove", command=Command("Remove",
"Remove a suggestion",
None,
remove_suggestion).bind(data))
return Div( return Div(
add_button, add_button,

View File

@@ -11,7 +11,10 @@ def say_hello():
# Create the command # Create the command
hello_command = Command("say_hello", "Responds with a greeting", say_hello) hello_command = Command("say_hello",
"Responds with a greeting",
None,
say_hello)
# Create the app # Create the app
app, rt = create_app(protect_routes=False) app, rt = create_app(protect_routes=False)

View File

@@ -13,7 +13,10 @@ def change_text():
return "New text" return "New text"
command = Command("change_text", "change the text", change_text).htmx(target="#text") command = Command("change_text",
"change the text",
None,
change_text).htmx(target="#text")
@rt("/") @rt("/")

View File

@@ -45,7 +45,7 @@ def test_i_can_mk_button_with_attrs():
def test_i_can_mk_button_with_command(user, rt): def test_i_can_mk_button_with_command(user, rt):
def new_value(value): return value def new_value(value): return value
command = Command('test', 'TestingCommand', new_value, "this is my new value") command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
@rt('/') @rt('/')
def get(): return mk.button('button', command) def get(): return mk.button('button', command)

View File

@@ -51,8 +51,8 @@ class TestTabsManagerBehaviour:
assert tab_id in tabs_manager.get_state().tabs assert tab_id in tabs_manager.get_state().tabs
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Tab1" assert tabs_manager.get_state().tabs[tab_id]["label"] == "Tab1"
assert tabs_manager.get_state().tabs[tab_id]["id"] == tab_id assert tabs_manager.get_state().tabs[tab_id]["id"] == tab_id
assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None assert tabs_manager.get_state().tabs[tab_id]["component"] is None
assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None assert tabs_manager.get_state().tabs[tab_id]["component_parent"] is None
assert tabs_manager.get_state().tabs_order == [tab_id] assert tabs_manager.get_state().tabs_order == [tab_id]
assert tabs_manager.get_state().active_tab == tab_id assert tabs_manager.get_state().active_tab == tab_id
@@ -61,9 +61,12 @@ class TestTabsManagerBehaviour:
vis_network = VisNetwork(tabs_manager, nodes=[], edges=[]) vis_network = VisNetwork(tabs_manager, nodes=[], edges=[])
tab_id = tabs_manager.create_tab("Network", vis_network) tab_id = tabs_manager.create_tab("Network", vis_network)
component_type, component_id = vis_network.get_prefix(), vis_network.get_id()
parent_type, parent_id = tabs_manager.get_prefix(), tabs_manager.get_id()
assert tab_id is not None assert tab_id is not None
assert tabs_manager.get_state().tabs[tab_id]["component_type"] == vis_network.get_prefix() assert tabs_manager.get_state().tabs[tab_id]["component"] == (component_type, component_id)
assert tabs_manager.get_state().tabs[tab_id]["component_id"] == vis_network.get_id() assert tabs_manager.get_state().tabs[tab_id]["component_parent"] == (parent_type, parent_id)
def test_i_can_create_multiple_tabs(self, tabs_manager): def test_i_can_create_multiple_tabs(self, tabs_manager):
"""Test creating multiple tabs maintains correct order and activation.""" """Test creating multiple tabs maintains correct order and activation."""

View File

@@ -27,21 +27,21 @@ def reset_command_manager():
class TestCommandDefault: class TestCommandDefault:
def test_i_can_create_a_command_with_no_params(self): def test_i_can_create_a_command_with_no_params(self):
command = Command('test', 'Command description', callback) command = Command('test', 'Command description', None, callback)
assert command.id is not None assert command.id is not None
assert command.name == 'test' assert command.name == 'test'
assert command.description == 'Command description' assert command.description == 'Command description'
assert command.execute() == "Hello World" assert command.execute() == "Hello World"
def test_command_are_registered(self): def test_command_are_registered(self):
command = Command('test', 'Command description', callback) command = Command('test', 'Command description', None, callback)
assert CommandsManager.commands.get(str(command.id)) is command assert CommandsManager.commands.get(str(command.id)) is command
class TestCommandBind: class TestCommandBind:
def test_i_can_bind_a_command_to_an_element(self): def test_i_can_bind_a_command_to_an_element(self):
command = Command('test', 'Command description', callback) command = Command('test', 'Command description', None, callback)
elt = Button() elt = Button()
updated = command.bind_ft(elt) updated = command.bind_ft(elt)
@@ -50,7 +50,7 @@ class TestCommandBind:
assert matches(updated, expected) assert matches(updated, expected)
def test_i_can_suppress_swapping_with_target_attr(self): def test_i_can_suppress_swapping_with_target_attr(self):
command = Command('test', 'Command description', callback).htmx(target=None) command = Command('test', 'Command description', None, callback).htmx(target=None)
elt = Button() elt = Button()
updated = command.bind_ft(elt) updated = command.bind_ft(elt)
@@ -70,7 +70,7 @@ class TestCommandBind:
make_observable(data) make_observable(data)
bind(data, "value", on_data_change) bind(data, "value", on_data_change)
command = Command('test', 'Command description', another_callback).bind(data) command = Command('test', 'Command description', None, another_callback).bind(data)
res = command.execute() res = command.execute()
@@ -88,14 +88,14 @@ class TestCommandBind:
make_observable(data) make_observable(data)
bind(data, "value", on_data_change) bind(data, "value", on_data_change)
command = Command('test', 'Command description', another_callback).bind(data) command = Command('test', 'Command description', None, another_callback).bind(data)
res = command.execute() res = command.execute()
assert res == ["another 1", "another 2", ("hello", "new value")] assert res == ["another 1", "another 2", ("hello", "new value")]
def test_by_default_swap_is_set_to_outer_html(self): def test_by_default_swap_is_set_to_outer_html(self):
command = Command('test', 'Command description', callback) command = Command('test', 'Command description', None, callback)
elt = Button() elt = Button()
updated = command.bind_ft(elt) updated = command.bind_ft(elt)
@@ -113,7 +113,7 @@ class TestCommandBind:
def another_callback(): def another_callback():
return return_values return return_values
command = Command('test', 'Command description', another_callback) command = Command('test', 'Command description', None, another_callback)
res = command.execute() res = command.execute()
@@ -125,7 +125,7 @@ class TestCommandBind:
class TestCommandExecute: class TestCommandExecute:
def test_i_can_create_a_command_with_no_params(self): def test_i_can_create_a_command_with_no_params(self):
command = Command('test', 'Command description', callback) command = Command('test', 'Command description', None, callback)
assert command.id is not None assert command.id is not None
assert command.name == 'test' assert command.name == 'test'
assert command.description == 'Command description' assert command.description == 'Command description'
@@ -137,7 +137,7 @@ class TestCommandExecute:
def callback_with_param(param): def callback_with_param(param):
return f"Hello {param}" return f"Hello {param}"
command = Command('test', 'Command description', callback_with_param, "world") command = Command('test', 'Command description', None, callback_with_param, "world")
assert command.execute() == "Hello world" assert command.execute() == "Hello world"
def test_i_can_execute_a_command_with_open_parameter(self): def test_i_can_execute_a_command_with_open_parameter(self):
@@ -146,7 +146,7 @@ class TestCommandExecute:
def callback_with_param(name): def callback_with_param(name):
return f"Hello {name}" return f"Hello {name}"
command = Command('test', 'Command description', callback_with_param) command = Command('test', 'Command description', None, callback_with_param)
assert command.execute(client_response={"name": "world"}) == "Hello world" assert command.execute(client_response={"name": "world"}) == "Hello world"
def test_i_can_convert_arg_in_execute(self): def test_i_can_convert_arg_in_execute(self):
@@ -155,7 +155,7 @@ class TestCommandExecute:
def callback_with_param(number: int): def callback_with_param(number: int):
assert isinstance(number, int) assert isinstance(number, int)
command = Command('test', 'Command description', callback_with_param) command = Command('test', 'Command description', None, callback_with_param)
command.execute(client_response={"number": "10"}) command.execute(client_response={"number": "10"})
def test_swap_oob_is_added_when_multiple_elements_are_returned(self): def test_swap_oob_is_added_when_multiple_elements_are_returned(self):
@@ -164,7 +164,7 @@ class TestCommandExecute:
def another_callback(): def another_callback():
return Div(id="first"), Div(id="second"), "hello", Div(id="third") return Div(id="first"), Div(id="second"), "hello", Div(id="third")
command = Command('test', 'Command description', another_callback) command = Command('test', 'Command description', None, another_callback)
res = command.execute() res = command.execute()
assert "hx-swap-oob" not in res[0].attrs assert "hx-swap-oob" not in res[0].attrs
@@ -177,7 +177,7 @@ class TestCommandExecute:
def another_callback(): def another_callback():
return Div(id="first"), Div(), "hello", Div() return Div(id="first"), Div(), "hello", Div()
command = Command('test', 'Command description', another_callback) command = Command('test', 'Command description', None, another_callback)
res = command.execute() res = command.execute()
assert "hx-swap-oob" not in res[0].attrs assert "hx-swap-oob" not in res[0].attrs
@@ -188,9 +188,9 @@ class TestCommandExecute:
class TestLambaCommand: class TestLambaCommand:
def test_i_can_create_a_command_from_lambda(self): def test_i_can_create_a_command_from_lambda(self):
command = LambdaCommand(lambda resp: "Hello World") command = LambdaCommand(None, lambda resp: "Hello World")
assert command.execute() == "Hello World" assert command.execute() == "Hello World"
def test_by_default_target_is_none(self): def test_by_default_target_is_none(self):
command = LambdaCommand(lambda resp: "Hello World") command = LambdaCommand(None, lambda resp: "Hello World")
assert command.get_htmx_params()["hx-swap"] == "none" assert command.get_htmx_params()["hx-swap"] == "none"

View File

@@ -34,13 +34,13 @@ def rt(user):
class TestingCommand: class TestingCommand:
def test_i_can_trigger_a_command(self, user): def test_i_can_trigger_a_command(self, user):
command = Command('test', 'TestingCommand', new_value, "this is my new value") command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
testable = TestableElement(user, mk.button('button', command)) testable = TestableElement(user, mk.button('button', command))
testable.click() testable.click()
assert user.get_content() == "this is my new value" assert user.get_content() == "this is my new value"
def test_error_is_raised_when_command_is_not_found(self, user): def test_error_is_raised_when_command_is_not_found(self, user):
command = Command('test', 'TestingCommand', new_value, "this is my new value") command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
CommandsManager.reset() CommandsManager.reset()
testable = TestableElement(user, mk.button('button', command)) testable = TestableElement(user, mk.button('button', command))
@@ -50,7 +50,7 @@ class TestingCommand:
assert "not found." in str(exc_info.value) assert "not found." in str(exc_info.value)
def test_i_can_play_a_complex_scenario(self, user, rt): def test_i_can_play_a_complex_scenario(self, user, rt):
command = Command('test', 'TestingCommand', new_value, "this is my new value") command = Command('test', 'TestingCommand', None, new_value, "this is my new value")
@rt('/') @rt('/')
def get(): return mk.button('button', command) def get(): return mk.button('button', command)

View File

@@ -463,7 +463,7 @@ class TestPredicates:
div = Div(hx_post="/url") div = Div(hx_post="/url")
assert HasHtmx(hx_post="/url").validate(div) assert HasHtmx(hx_post="/url").validate(div)
c = Command("c", "testing has_htmx", None) c = Command("c", "testing has_htmx", None, None)
c.bind_ft(div) c.bind_ft(div)
assert HasHtmx(command=c).validate(div) assert HasHtmx(command=c).validate(div)