import uuid from dataclasses import dataclass from typing import Any from fasthtml.common import Div, Button, Span from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.helpers import Ids from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.instances import MultipleInstance, BaseInstance from myfasthtml.icons.fluent_p3 import dismiss_circle16_regular # Structure de tabs: # { # "tab-uuid-1": { # "label": "Users", # "component_type": "UsersPanel", # "component_id": "UsersPanel-abc123", # }, # "tab-uuid-2": { ... } # } class TabsManagerState(DbObject): def __init__(self, owner): super().__init__(owner.get_session(), owner.get_id()) with self.initializing(): # persisted in DB self.tabs: dict[str, Any] = {} self.tabs_order: list[str] = [] self.active_tab: str | None = None # must not be persisted in DB self._tabs_content: dict[str, Any] = {} class Commands(BaseCommands): pass class TabsManager(MultipleInstance): def __init__(self, session): super().__init__(session, Ids.TabsManager) self._state = TabsManagerState(self) self._commands = Commands(self) def get_state(self): return self._state def add_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. Args: label: Display label for the tab component: Component instance to display in the tab activate: Whether to activate the new/updated tab immediately (default: True) Returns: tab_id: The UUID of the tab (new or existing) """ # copy the state to avoid multiple database call state = self._state.copy() # Extract component ID if the component has a get_id() method component_type, component_id = None, None if isinstance(component, BaseInstance): component_type = type(component).__name__ component_id = component.get_id() # Check if a tab with the same component_type, component_id AND label already exists existing_tab_id = None if component_id is not None: for tab_id, tab_data in state.tabs.items(): if (tab_data.get('component_type') == component_type and tab_data.get('component_id') == component_id and tab_data.get('label') == label): existing_tab_id = tab_id break if existing_tab_id: # Update existing tab (only the component instance in memory) tab_id = existing_tab_id state._tabs_content[tab_id] = component else: # Create new tab tab_id = str(uuid.uuid4()) # Add tab metadata to state state.tabs[tab_id] = { 'label': label, 'component_type': component_type, 'component_id': component_id } # Add tab to order state.tabs_order.append(tab_id) # Store component in memory state._tabs_content[tab_id] = component # Activate tab if requested if activate: state.active_tab = tab_id # finally, update the state self._state.update(state) return tab_id def _mk_tab_button(self, tab_id: str, tab_data: dict): """ Create a single tab button with its label and close button. Args: tab_id: Unique identifier for the tab tab_data: Dictionary containing tab information (label, component_type, etc.) Returns: Button element representing the tab """ is_active = tab_id == self._state.active_tab return Button( Span(tab_data.get("label", "Untitled"), cls="mf-tab-label"), Span(dismiss_circle16_regular, cls="mf-tab-close-btn"), cls=f"mf-tab-button {'mf-tab-active' if is_active else ''}", data_tab_id=tab_id, data_manager_id=self._id ) def _mk_tabs_header(self): """ Create the tabs header containing all tab buttons. Returns: Div element containing all tab buttons """ tab_buttons = [ self._mk_tab_button(tab_id, self._state.tabs[tab_id]) for tab_id in self._state.tabs_order if tab_id in self._state.tabs ] return Div( *tab_buttons, cls="mf-tabs-header", id=f"{self._id}-header" ) def _mk_tab_content(self): """ Create the active tab content area. Returns: Div element containing the active tab content or empty container """ content = None if self._state.active_tab and self._state.active_tab in self._state._tabs_content: component = self._state._tabs_content[self._state.active_tab] content = component return Div( content if content else Div("No active tab", cls="mf-empty-content"), cls="mf-tab-content", id=f"{self._id}-content" ) def render(self): """ Render the complete TabsManager component. Returns: Div element containing tabs header and content area """ return Div( self._mk_tabs_header(), self._mk_tab_content(), cls="mf-tabs-manager", id=self._id ) def __ft__(self): return self.render()