|
|
|
|
@@ -52,32 +52,39 @@ class TabsManagerState(DbObject):
|
|
|
|
|
self.active_tab: str | None = None
|
|
|
|
|
|
|
|
|
|
# must not be persisted in DB
|
|
|
|
|
self._tabs_content: dict[str, Any] = {}
|
|
|
|
|
self.ns_tabs_content: dict[str, Any] = {} # Cache: always stores raw content (not wrapped)
|
|
|
|
|
self.ns_tabs_sent_to_client: set = set() # for tabs created, but not yet displayed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Commands(BaseCommands):
|
|
|
|
|
def show_tab(self, tab_id):
|
|
|
|
|
return Command(f"{self._prefix}ShowTab",
|
|
|
|
|
"Activate or show a specific tab",
|
|
|
|
|
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}-controller", swap="outerHTML")
|
|
|
|
|
self._owner.show_tab,
|
|
|
|
|
tab_id,
|
|
|
|
|
True,
|
|
|
|
|
False).htmx(target=f"#{self._id}-controller", swap="outerHTML")
|
|
|
|
|
|
|
|
|
|
def close_tab(self, tab_id):
|
|
|
|
|
return Command(f"{self._prefix}CloseTab",
|
|
|
|
|
"Close a specific tab",
|
|
|
|
|
self._owner.close_tab, tab_id).htmx(target=f"#{self._id}", swap="outerHTML")
|
|
|
|
|
self._owner.close_tab,
|
|
|
|
|
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",
|
|
|
|
|
"Add a new tab",
|
|
|
|
|
self._owner.on_new_tab, label, component, auto_increment).
|
|
|
|
|
htmx(target=f"#{self._id}-controller"))
|
|
|
|
|
return Command(f"{self._prefix}AddTab",
|
|
|
|
|
"Add a new tab",
|
|
|
|
|
self._owner.on_new_tab,
|
|
|
|
|
label,
|
|
|
|
|
component,
|
|
|
|
|
auto_increment).htmx(target=f"#{self._id}-controller", swap="outerHTML")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TabsManager(MultipleInstance):
|
|
|
|
|
_tab_count = 0
|
|
|
|
|
|
|
|
|
|
def __init__(self, parent, _id=None):
|
|
|
|
|
super().__init__(parent, _id=_id)
|
|
|
|
|
self._tab_count = 0
|
|
|
|
|
self._state = TabsManagerState(self)
|
|
|
|
|
self.commands = Commands(self)
|
|
|
|
|
self._boundaries = Boundaries()
|
|
|
|
|
@@ -86,6 +93,7 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
get_attr=lambda x: x["label"],
|
|
|
|
|
template=self._mk_tab_button,
|
|
|
|
|
_id="-search")
|
|
|
|
|
|
|
|
|
|
logger.debug(f"TabsManager created with id: {self._id}")
|
|
|
|
|
logger.debug(f" tabs : {self._get_ordered_tabs()}")
|
|
|
|
|
logger.debug(f" active tab : {self._state.active_tab}")
|
|
|
|
|
@@ -96,22 +104,36 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
def _get_ordered_tabs(self):
|
|
|
|
|
return {tab_id: self._state.tabs.get(tab_id, None) for tab_id in self._state.tabs_order}
|
|
|
|
|
|
|
|
|
|
def _get_tab_content(self, tab_id):
|
|
|
|
|
def _dynamic_get_content(self, tab_id):
|
|
|
|
|
if tab_id not in self._state.tabs:
|
|
|
|
|
return None
|
|
|
|
|
return Div("Tab not found.")
|
|
|
|
|
tab_config = self._state.tabs[tab_id]
|
|
|
|
|
if tab_config["component_type"] is None:
|
|
|
|
|
return None
|
|
|
|
|
return Div("Tab content does not support serialization.")
|
|
|
|
|
try:
|
|
|
|
|
return InstancesManager.get(self._session, tab_config["component_id"])
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error while retrieving tab content: {e}")
|
|
|
|
|
return Div("Tab not found.")
|
|
|
|
|
return Div("Failed to retrieve tab content.")
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _get_tab_count():
|
|
|
|
|
res = TabsManager._tab_count
|
|
|
|
|
TabsManager._tab_count += 1
|
|
|
|
|
def _get_or_create_tab_content(self, tab_id):
|
|
|
|
|
"""
|
|
|
|
|
Get tab content from cache or create it.
|
|
|
|
|
This method ensures content is always stored in raw form (not wrapped).
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
tab_id: ID of the tab
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Raw content component (not wrapped in Div)
|
|
|
|
|
"""
|
|
|
|
|
if tab_id not in self._state.ns_tabs_content:
|
|
|
|
|
self._state.ns_tabs_content[tab_id] = self._dynamic_get_content(tab_id)
|
|
|
|
|
return self._state.ns_tabs_content[tab_id]
|
|
|
|
|
|
|
|
|
|
def _get_tab_count(self):
|
|
|
|
|
res = self._tab_count
|
|
|
|
|
self._tab_count += 1
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
def on_new_tab(self, label: str, component: Any, auto_increment=False):
|
|
|
|
|
@@ -120,20 +142,13 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
label = f"{label}_{self._get_tab_count()}"
|
|
|
|
|
component = component or VisNetwork(self, nodes=vis_nodes, edges=vis_edges)
|
|
|
|
|
|
|
|
|
|
tab_id = self._tab_already_exists(label, component)
|
|
|
|
|
if tab_id:
|
|
|
|
|
return self.show_tab(tab_id)
|
|
|
|
|
|
|
|
|
|
tab_id = self.add_tab(label, component)
|
|
|
|
|
return (
|
|
|
|
|
self._mk_tabs_controller(),
|
|
|
|
|
self._wrap_tab_content(self._mk_tab_content(tab_id, component)),
|
|
|
|
|
self._mk_tabs_header_wrapper(True),
|
|
|
|
|
)
|
|
|
|
|
tab_id = self.create_tab(label, component)
|
|
|
|
|
return self.show_tab(tab_id, oob=False)
|
|
|
|
|
|
|
|
|
|
def add_tab(self, label: str, component: Any, activate: bool = True) -> str:
|
|
|
|
|
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.
|
|
|
|
|
The tab is not yet sent to the client.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
label: Display label for the tab
|
|
|
|
|
@@ -144,73 +159,52 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
tab_id: The UUID of the tab (new or existing)
|
|
|
|
|
"""
|
|
|
|
|
logger.debug(f"add_tab {label=}, component={component}, activate={activate}")
|
|
|
|
|
# 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 = component.get_prefix() if isinstance(component, BaseInstance) else 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 = self._tab_already_exists(label, component)
|
|
|
|
|
|
|
|
|
|
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] = {
|
|
|
|
|
'id': 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)
|
|
|
|
|
self._search.set_items(self._get_tab_list())
|
|
|
|
|
|
|
|
|
|
tab_id = self._tab_already_exists(label, component) or str(uuid.uuid4())
|
|
|
|
|
self._add_or_update_tab(tab_id, label, component, activate)
|
|
|
|
|
return tab_id
|
|
|
|
|
|
|
|
|
|
def show_tab(self, tab_id):
|
|
|
|
|
def show_tab(self, tab_id, activate: bool = True, oob=True):
|
|
|
|
|
"""
|
|
|
|
|
Send the tab to the client if needed.
|
|
|
|
|
If the tab was already sent, just update the active tab.
|
|
|
|
|
:param tab_id:
|
|
|
|
|
:param activate:
|
|
|
|
|
:param oob: default=True so other control will not care of the target
|
|
|
|
|
:return:
|
|
|
|
|
"""
|
|
|
|
|
logger.debug(f"show_tab {tab_id=}")
|
|
|
|
|
if tab_id not in self._state.tabs:
|
|
|
|
|
logger.debug(f" Tab not found.")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
logger.debug(f" Tab label is: {self._state.tabs[tab_id]['label']}")
|
|
|
|
|
self._state.active_tab = tab_id
|
|
|
|
|
|
|
|
|
|
if tab_id not in self._state._tabs_content:
|
|
|
|
|
logger.debug(f" Content does not exist. Creating it.")
|
|
|
|
|
content = self._get_tab_content(tab_id)
|
|
|
|
|
if activate:
|
|
|
|
|
self._state.active_tab = tab_id
|
|
|
|
|
|
|
|
|
|
# Get or create content (always stored in raw form)
|
|
|
|
|
content = self._get_or_create_tab_content(tab_id)
|
|
|
|
|
|
|
|
|
|
if tab_id not in self._state.ns_tabs_sent_to_client:
|
|
|
|
|
logger.debug(f" Content not in client memory. Sending it.")
|
|
|
|
|
self._state.ns_tabs_sent_to_client.add(tab_id)
|
|
|
|
|
tab_content = self._mk_tab_content(tab_id, content)
|
|
|
|
|
self._state._tabs_content[tab_id] = tab_content
|
|
|
|
|
return self._mk_tabs_controller(), self._wrap_tab_content(tab_content)
|
|
|
|
|
return self._mk_tabs_controller(oob), self._mk_tabs_header_wrapper(), self._wrap_tab_content(tab_content)
|
|
|
|
|
else:
|
|
|
|
|
logger.debug(f" Content already exists. Just switch.")
|
|
|
|
|
return self._mk_tabs_controller()
|
|
|
|
|
logger.debug(f" Content already in client memory. Just switch.")
|
|
|
|
|
return self._mk_tabs_controller(oob) # no new tab_id => header is already up to date
|
|
|
|
|
|
|
|
|
|
def switch_tab(self, tab_id, label, component, activate=True):
|
|
|
|
|
def change_tab_content(self, tab_id, label, component, activate=True):
|
|
|
|
|
logger.debug(f"switch_tab {label=}, component={component}, activate={activate}")
|
|
|
|
|
|
|
|
|
|
if tab_id not in self._state.tabs:
|
|
|
|
|
logger.error(f" Tab {tab_id} not found. Cannot change its content.")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
self._add_or_update_tab(tab_id, label, component, activate)
|
|
|
|
|
return self.show_tab(tab_id) #
|
|
|
|
|
self._state.ns_tabs_sent_to_client.discard(tab_id) # to make sure that the new content will be sent to the client
|
|
|
|
|
return self.show_tab(tab_id, activate=activate, oob=True)
|
|
|
|
|
|
|
|
|
|
def close_tab(self, tab_id: str):
|
|
|
|
|
"""
|
|
|
|
|
@@ -220,10 +214,12 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
tab_id: ID of the tab to close
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Self for chaining
|
|
|
|
|
tuple: (controller, header_wrapper, content_to_remove) for HTMX swapping,
|
|
|
|
|
or self if tab not found
|
|
|
|
|
"""
|
|
|
|
|
logger.debug(f"close_tab {tab_id=}")
|
|
|
|
|
if tab_id not in self._state.tabs:
|
|
|
|
|
logger.debug(f" Tab not found.")
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
# Copy state
|
|
|
|
|
@@ -234,8 +230,12 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
state.tabs_order.remove(tab_id)
|
|
|
|
|
|
|
|
|
|
# Remove from content
|
|
|
|
|
if tab_id in state._tabs_content:
|
|
|
|
|
del state._tabs_content[tab_id]
|
|
|
|
|
if tab_id in state.ns_tabs_content:
|
|
|
|
|
del state.ns_tabs_content[tab_id]
|
|
|
|
|
|
|
|
|
|
# Remove from content sent
|
|
|
|
|
if tab_id in state.ns_tabs_sent_to_client:
|
|
|
|
|
state.ns_tabs_sent_to_client.remove(tab_id)
|
|
|
|
|
|
|
|
|
|
# If closing active tab, activate another one
|
|
|
|
|
if state.active_tab == tab_id:
|
|
|
|
|
@@ -249,7 +249,8 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
self._state.update(state)
|
|
|
|
|
self._search.set_items(self._get_tab_list())
|
|
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
content_to_remove = Div(id=f"{self._id}-{tab_id}-content", hx_swap_oob=f"delete")
|
|
|
|
|
return self._mk_tabs_controller(), self._mk_tabs_header_wrapper(), content_to_remove
|
|
|
|
|
|
|
|
|
|
def add_tab_btn(self):
|
|
|
|
|
return mk.icon(tab_add24_regular,
|
|
|
|
|
@@ -259,11 +260,12 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
None,
|
|
|
|
|
True))
|
|
|
|
|
|
|
|
|
|
def _mk_tabs_controller(self):
|
|
|
|
|
return Div(
|
|
|
|
|
Div(id=f"{self._id}-controller", data_active_tab=f"{self._state.active_tab}"),
|
|
|
|
|
Script(f'updateTabs("{self._id}-controller");'),
|
|
|
|
|
)
|
|
|
|
|
def _mk_tabs_controller(self, oob=False):
|
|
|
|
|
return Div(id=f"{self._id}-controller",
|
|
|
|
|
data_active_tab=f"{self._state.active_tab}",
|
|
|
|
|
hx_on__after_settle=f'updateTabs("{self._id}-controller");',
|
|
|
|
|
hx_swap_oob="true" if oob else None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _mk_tabs_header_wrapper(self, oob=False):
|
|
|
|
|
# Create visible tab buttons
|
|
|
|
|
@@ -273,24 +275,20 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
if tab_id in self._state.tabs
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
header_content = [*visible_tab_buttons]
|
|
|
|
|
|
|
|
|
|
return Div(
|
|
|
|
|
Div(*header_content, cls="mf-tabs-header", id=f"{self._id}-header"),
|
|
|
|
|
Div(*visible_tab_buttons, cls="mf-tabs-header", id=f"{self._id}-header"),
|
|
|
|
|
self._mk_show_tabs_menu(),
|
|
|
|
|
id=f"{self._id}-header-wrapper",
|
|
|
|
|
cls="mf-tabs-header-wrapper",
|
|
|
|
|
hx_swap_oob="true" if oob else None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _mk_tab_button(self, tab_data: dict, in_dropdown: bool = False):
|
|
|
|
|
def _mk_tab_button(self, 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.)
|
|
|
|
|
in_dropdown: Whether this tab is rendered in the dropdown menu
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Button element representing the tab
|
|
|
|
|
@@ -308,12 +306,10 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
command=self.commands.show_tab(tab_id)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
extra_cls = "mf-tab-in-dropdown" if in_dropdown else ""
|
|
|
|
|
|
|
|
|
|
return Div(
|
|
|
|
|
tab_label,
|
|
|
|
|
close_btn,
|
|
|
|
|
cls=f"mf-tab-button {extra_cls} {'mf-tab-active' if is_active else ''}",
|
|
|
|
|
cls=f"mf-tab-button {'mf-tab-active' if is_active else ''}",
|
|
|
|
|
data_tab_id=tab_id,
|
|
|
|
|
data_manager_id=self._id
|
|
|
|
|
)
|
|
|
|
|
@@ -325,15 +321,9 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
Returns:
|
|
|
|
|
Div element containing the active tab content or empty container
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if self._state.active_tab:
|
|
|
|
|
active_tab = self._state.active_tab
|
|
|
|
|
if active_tab in self._state._tabs_content:
|
|
|
|
|
tab_content = self._state._tabs_content[active_tab]
|
|
|
|
|
else:
|
|
|
|
|
content = self._get_tab_content(active_tab)
|
|
|
|
|
tab_content = self._mk_tab_content(active_tab, content)
|
|
|
|
|
self._state._tabs_content[active_tab] = tab_content
|
|
|
|
|
content = self._get_or_create_tab_content(self._state.active_tab)
|
|
|
|
|
tab_content = self._mk_tab_content(self._state.active_tab, content)
|
|
|
|
|
else:
|
|
|
|
|
tab_content = self._mk_tab_content(None, None)
|
|
|
|
|
|
|
|
|
|
@@ -344,10 +334,13 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _mk_tab_content(self, tab_id: str, content):
|
|
|
|
|
if tab_id is None:
|
|
|
|
|
return Div("No Content", cls="mf-empty-content mf-tab-content hidden")
|
|
|
|
|
|
|
|
|
|
is_active = tab_id == self._state.active_tab
|
|
|
|
|
return Div(
|
|
|
|
|
content if content else Div("No Content", cls="mf-empty-content"),
|
|
|
|
|
cls=f"mf-tab-content {'hidden' if not is_active else ''}", # ← ici
|
|
|
|
|
cls=f"mf-tab-content {'hidden' if not is_active else ''}",
|
|
|
|
|
id=f"{self._id}-{tab_id}-content",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@@ -376,7 +369,7 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
if not isinstance(component, BaseInstance):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
component_type = component.get_prefix() if isinstance(component, BaseInstance) else type(component).__name__
|
|
|
|
|
component_type = component.get_prefix()
|
|
|
|
|
component_id = component.get_id()
|
|
|
|
|
|
|
|
|
|
if component_id is not None:
|
|
|
|
|
@@ -397,7 +390,7 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
# Extract component ID if the component has a get_id() method
|
|
|
|
|
component_type, component_id = None, None
|
|
|
|
|
if isinstance(component, BaseInstance):
|
|
|
|
|
component_type = component.get_prefix() if isinstance(component, BaseInstance) else type(component).__name__
|
|
|
|
|
component_type = component.get_prefix()
|
|
|
|
|
component_id = component.get_id()
|
|
|
|
|
|
|
|
|
|
# Add tab metadata to state
|
|
|
|
|
@@ -408,8 +401,12 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
'component_id': component_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Add tab to order list
|
|
|
|
|
if tab_id not in state.tabs_order:
|
|
|
|
|
state.tabs_order.append(tab_id)
|
|
|
|
|
|
|
|
|
|
# Add the content
|
|
|
|
|
state._tabs_content[tab_id] = component
|
|
|
|
|
state.ns_tabs_content[tab_id] = component
|
|
|
|
|
|
|
|
|
|
# Activate tab if requested
|
|
|
|
|
if activate:
|
|
|
|
|
@@ -433,6 +430,7 @@ class TabsManager(MultipleInstance):
|
|
|
|
|
self._mk_tabs_controller(),
|
|
|
|
|
self._mk_tabs_header_wrapper(),
|
|
|
|
|
self._mk_tab_content_wrapper(),
|
|
|
|
|
Script(f'updateTabs("{self._id}-controller");'), # first time, run the script to initialize the tabs
|
|
|
|
|
cls="mf-tabs-manager",
|
|
|
|
|
id=self._id,
|
|
|
|
|
)
|
|
|
|
|
|