diff --git a/src/myfasthtml/controls/DataGridsManager.py b/src/myfasthtml/controls/DataGridsManager.py index 368bdf0..36d9ac4 100644 --- a/src/myfasthtml/controls/DataGridsManager.py +++ b/src/myfasthtml/controls/DataGridsManager.py @@ -1,4 +1,5 @@ import pandas as pd +from fastcore.basics import NotStr from fasthtml.components import Div from myfasthtml.controls.BaseCommands import BaseCommands @@ -23,13 +24,12 @@ class Commands(BaseCommands): "New grid", self._owner.new_grid) - def open_from_excel(self, tab_id, get_content_callback): - excel_content = get_content_callback() + def open_from_excel(self, tab_id, file_upload): return Command("OpenFromExcel", "Open from Excel", self._owner.open_from_excel, tab_id, - excel_content).htmx(target=None) + file_upload).htmx(target=None) class DataGridsManager(MultipleInstance): @@ -42,13 +42,14 @@ class DataGridsManager(MultipleInstance): def upload_from_source(self): file_upload = FileUpload(self) tab_id = self._tabs_manager.create_tab("Upload Datagrid", file_upload) - file_upload.on_ok = self.commands.open_from_excel(tab_id, file_upload.get_content) + file_upload.set_on_ok(self.commands.open_from_excel(tab_id, file_upload)) return self._tabs_manager.show_tab(tab_id) - def open_from_excel(self, tab_id, excel_content): + def open_from_excel(self, tab_id, file_upload): + excel_content = file_upload.get_content() df = pd.read_excel(excel_content) content = df.to_html(index=False) - self._tabs_manager.switch(tab_id, content) + return self._tabs_manager.change_tab_content(tab_id, file_upload.get_file_name(), NotStr(content)) def mk_main_icons(self): return Div( diff --git a/src/myfasthtml/controls/FileUpload.py b/src/myfasthtml/controls/FileUpload.py index 35c012c..1a1434f 100644 --- a/src/myfasthtml/controls/FileUpload.py +++ b/src/myfasthtml/controls/FileUpload.py @@ -25,6 +25,8 @@ class FileUploadState(DbObject): self.ns_sheets_names: list | None = None self.ns_selected_sheet_name: str | None = None self.ns_file_content: bytes | None = None + self.ns_on_ok = None + self.ns_on_cancel = None class Commands(BaseCommands): @@ -49,11 +51,16 @@ class FileUpload(MultipleInstance): super().__init__(parent, _id=_id, **kwargs) self.commands = Commands(self) self._state = FileUploadState(self) + self._state.ns_on_ok = None + + def set_on_ok(self, callback): + self._state.ns_on_ok = callback def upload_file(self, file: UploadFile): logger.debug(f"upload_file: {file=}") if file: self._state.ns_file_content = file.file.read() + self._state.ns_file_name = file.filename self._state.ns_sheets_names = self.get_sheets_names(self._state.ns_file_content) self._state.ns_selected_sheet_name = self._state.ns_sheets_names[0] if len(self._state.ns_sheets_names) > 0 else 0 @@ -75,7 +82,9 @@ class FileUpload(MultipleInstance): def get_content(self): return self._state.ns_file_content - + + def get_file_name(self): + return self._state.ns_file_name @staticmethod def get_sheets_names(file_content): @@ -104,7 +113,7 @@ class FileUpload(MultipleInstance): self.mk_sheet_selector(), cls="flex" ), - mk.dialog_buttons(), + mk.dialog_buttons(on_ok=self._state.ns_on_ok, on_cancel=self._state.ns_on_cancel), ) def __ft__(self): diff --git a/src/myfasthtml/controls/TabsManager.py b/src/myfasthtml/controls/TabsManager.py index 496c372..1101db7 100644 --- a/src/myfasthtml/controls/TabsManager.py +++ b/src/myfasthtml/controls/TabsManager.py @@ -164,13 +164,14 @@ class TabsManager(MultipleInstance): self._add_or_update_tab(tab_id, label, component, activate) return tab_id - def show_tab(self, tab_id, activate: bool = True, oob=True): + def show_tab(self, tab_id, activate: bool = True, oob=True, is_new=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 + :param is_new: is it a new tab or an existing one? :return: """ logger.debug(f"show_tab {tab_id=}") @@ -190,7 +191,9 @@ class TabsManager(MultipleInstance): 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) - return self._mk_tabs_controller(oob), self._mk_tabs_header_wrapper(), self._wrap_tab_content(tab_content) + return (self._mk_tabs_controller(oob), + self._mk_tabs_header_wrapper(), + self._wrap_tab_content(tab_content, is_new)) else: 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 @@ -204,7 +207,7 @@ class TabsManager(MultipleInstance): self._add_or_update_tab(tab_id, label, component, activate) 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) + return self.show_tab(tab_id, activate=activate, oob=True, is_new=False) def close_tab(self, tab_id: str): """ @@ -359,11 +362,15 @@ class TabsManager(MultipleInstance): cls="dropdown dropdown-end" ) - def _wrap_tab_content(self, tab_content): - return Div( - tab_content, - hx_swap_oob=f"beforeend:#{self._id}-content-wrapper", - ) + def _wrap_tab_content(self, tab_content, is_new=True): + if is_new: + return Div( + tab_content, + hx_swap_oob=f"beforeend:#{self._id}-content-wrapper" + ) + else: + tab_content.attrs["hx-swap-oob"] = "outerHTML" + return tab_content def _tab_already_exists(self, label, component): if not isinstance(component, BaseInstance): diff --git a/src/myfasthtml/core/commands.py b/src/myfasthtml/core/commands.py index 7752442..315811b 100644 --- a/src/myfasthtml/core/commands.py +++ b/src/myfasthtml/core/commands.py @@ -25,7 +25,7 @@ class BaseCommand: :type description: str """ - def __init__(self, name, description): + def __init__(self, name, description, auto_register=True): self.id = uuid.uuid4() self.name = name self.description = description @@ -34,7 +34,8 @@ class BaseCommand: self._ft = None # register the command - CommandsManager.register(self) + if auto_register: + CommandsManager.register(self) def get_htmx_params(self): return { @@ -139,7 +140,7 @@ class Command(BaseCommand): self.args = args self.kwargs = kwargs - def _convert(self, key, value): + def _cast_parameter(self, key, value): if key in self.callback_parameters: param = self.callback_parameters[key] if param.annotation == bool: @@ -174,7 +175,7 @@ class Command(BaseCommand): if client_response: for k, v in client_response.items(): if k in self.callback_parameters: - new_kwargs[k] = self._convert(k, v) + new_kwargs[k] = self._cast_parameter(k, v) if 'client_response' in self.callback_parameters: new_kwargs['client_response'] = client_response @@ -186,7 +187,9 @@ class Command(BaseCommand): # Set the hx-swap-oob attribute on all elements returned by the callback if isinstance(ret, (list, tuple)): for r in ret[1:]: - if hasattr(r, 'attrs') and r.get("id", None) is not None: + if (hasattr(r, 'attrs') + and "hx-swap-oob" not in r.attrs + and r.get("id", None) is not None): r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true") if not ret_from_bindings: diff --git a/tests/controls/test_tabsmanager.py b/tests/controls/test_tabsmanager.py index a7ffdbc..d0993fe 100644 --- a/tests/controls/test_tabsmanager.py +++ b/tests/controls/test_tabsmanager.py @@ -700,6 +700,65 @@ class TestTabsManagerRender: expected = Div(cls=Contains("hidden")) assert matches(tab_content, expected) + def test_i_can_show_a_new_content(self, tabs_manager): + """Test that TabsManager.show_tab() send the correct div to the client""" + tab_id = tabs_manager.create_tab("Tab1", Div("My Content")) + actual = tabs_manager.show_tab(tab_id) + + expected = ( + Div(data_active_tab=tab_id, + hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");', + hx_swap_oob="true"), # the controller is correctly updated + Div( + id=f'{tabs_manager.get_id()}-header-wrapper' + ), # content of the header + Div( + Div(Div("My Content")), + hx_swap_oob=f"beforeend:#{tabs_manager.get_id()}-content-wrapper", # hx_swap_oob="beforeend:" important ! + ), # content + where to put it + ) + assert matches(actual, expected) + + def test_i_can_show_content_after_switch(self, tabs_manager): + tab_id = tabs_manager.create_tab("Tab1", Div("My Content")) + tabs_manager.show_tab(tab_id) # first time, send everything + actual = tabs_manager.show_tab(tab_id) # second time, send only the controller + + expected = Div(data_active_tab=tab_id, + hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");', + hx_swap_oob="true") + + assert matches(actual, expected) + + def test_i_can_close_a_tab(self, tabs_manager): + tab_id = tabs_manager.create_tab("Tab1", Div("My Content")) + tabs_manager.show_tab(tab_id) # was sent + actual = tabs_manager.close_tab(tab_id) + + expected = ( + Div(id=f'{tabs_manager.get_id()}-controller'), + Div(id=f'{tabs_manager.get_id()}-header-wrapper'), + Div(id=f'{tabs_manager.get_id()}-{tab_id}-content', hx_swap_oob="delete") # hx_swap_oob="delete" important ! + ) + + assert matches(actual, expected) + + def test_i_can_change_content(self, tabs_manager): + tab_id = tabs_manager.create_tab("Tab1", Div("My Content")) + tabs_manager.show_tab(tab_id) + actual = tabs_manager.change_tab_content(tab_id, "New Label", Div("New Content")) + + expected = ( + Div(data_active_tab=tab_id, hx_swap_oob="true"), + Div(id=f'{tabs_manager.get_id()}-header-wrapper'), + Div( + Div("New Content"), + id=f'{tabs_manager.get_id()}-{tab_id}-content', + hx_swap_oob="outerHTML", # hx_swap_oob="true" important ! + ), + ) + assert matches(actual, expected) + # ========================================================================= # Complete Render # =========================================================================