Files
MyFastHtml/src/myfasthtml/controls/TabsManager.py

186 lines
5.1 KiB
Python

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