I can open an excel file and see its content

This commit is contained in:
2025-12-07 17:46:39 +01:00
parent fde2e85c92
commit dc5ec450f0
5 changed files with 100 additions and 21 deletions

View File

@@ -1,4 +1,5 @@
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
@@ -23,13 +24,12 @@ class Commands(BaseCommands):
"New grid", "New grid",
self._owner.new_grid) self._owner.new_grid)
def open_from_excel(self, tab_id, get_content_callback): def open_from_excel(self, tab_id, file_upload):
excel_content = get_content_callback()
return Command("OpenFromExcel", return Command("OpenFromExcel",
"Open from Excel", "Open from Excel",
self._owner.open_from_excel, self._owner.open_from_excel,
tab_id, tab_id,
excel_content).htmx(target=None) file_upload).htmx(target=None)
class DataGridsManager(MultipleInstance): class DataGridsManager(MultipleInstance):
@@ -42,13 +42,14 @@ class DataGridsManager(MultipleInstance):
def upload_from_source(self): def upload_from_source(self):
file_upload = FileUpload(self) file_upload = FileUpload(self)
tab_id = self._tabs_manager.create_tab("Upload Datagrid", file_upload) 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) 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) df = pd.read_excel(excel_content)
content = df.to_html(index=False) 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): def mk_main_icons(self):
return Div( return Div(

View File

@@ -25,6 +25,8 @@ class FileUploadState(DbObject):
self.ns_sheets_names: list | None = None self.ns_sheets_names: list | None = None
self.ns_selected_sheet_name: str | None = None self.ns_selected_sheet_name: str | None = None
self.ns_file_content: bytes | None = None self.ns_file_content: bytes | None = None
self.ns_on_ok = None
self.ns_on_cancel = None
class Commands(BaseCommands): class Commands(BaseCommands):
@@ -49,11 +51,16 @@ class FileUpload(MultipleInstance):
super().__init__(parent, _id=_id, **kwargs) super().__init__(parent, _id=_id, **kwargs)
self.commands = Commands(self) self.commands = Commands(self)
self._state = FileUploadState(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): def upload_file(self, file: UploadFile):
logger.debug(f"upload_file: {file=}") logger.debug(f"upload_file: {file=}")
if file: if file:
self._state.ns_file_content = file.file.read() 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_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 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): def get_content(self):
return self._state.ns_file_content return self._state.ns_file_content
def get_file_name(self):
return self._state.ns_file_name
@staticmethod @staticmethod
def get_sheets_names(file_content): def get_sheets_names(file_content):
@@ -104,7 +113,7 @@ class FileUpload(MultipleInstance):
self.mk_sheet_selector(), self.mk_sheet_selector(),
cls="flex" 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): def __ft__(self):

View File

@@ -164,13 +164,14 @@ class TabsManager(MultipleInstance):
self._add_or_update_tab(tab_id, label, component, activate) self._add_or_update_tab(tab_id, label, component, activate)
return tab_id 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. Send the tab to the client if needed.
If the tab was already sent, just update the active tab. If the tab was already sent, just update the active tab.
:param tab_id: :param tab_id:
:param activate: :param activate:
:param oob: default=True so other control will not care of the target :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: :return:
""" """
logger.debug(f"show_tab {tab_id=}") logger.debug(f"show_tab {tab_id=}")
@@ -190,7 +191,9 @@ class TabsManager(MultipleInstance):
logger.debug(f" Content not in client memory. Sending it.") logger.debug(f" Content not in client memory. Sending it.")
self._state.ns_tabs_sent_to_client.add(tab_id) self._state.ns_tabs_sent_to_client.add(tab_id)
tab_content = self._mk_tab_content(tab_id, content) 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: else:
logger.debug(f" Content already in client memory. Just switch.") 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 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._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 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): def close_tab(self, tab_id: str):
""" """
@@ -359,11 +362,15 @@ class TabsManager(MultipleInstance):
cls="dropdown dropdown-end" cls="dropdown dropdown-end"
) )
def _wrap_tab_content(self, tab_content): def _wrap_tab_content(self, tab_content, is_new=True):
return Div( if is_new:
tab_content, return Div(
hx_swap_oob=f"beforeend:#{self._id}-content-wrapper", 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): def _tab_already_exists(self, label, component):
if not isinstance(component, BaseInstance): if not isinstance(component, BaseInstance):

View File

@@ -25,7 +25,7 @@ class BaseCommand:
:type description: str :type description: str
""" """
def __init__(self, name, description): def __init__(self, name, description, auto_register=True):
self.id = uuid.uuid4() self.id = uuid.uuid4()
self.name = name self.name = name
self.description = description self.description = description
@@ -34,7 +34,8 @@ class BaseCommand:
self._ft = None self._ft = None
# register the command # register the command
CommandsManager.register(self) if auto_register:
CommandsManager.register(self)
def get_htmx_params(self): def get_htmx_params(self):
return { return {
@@ -139,7 +140,7 @@ class Command(BaseCommand):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
def _convert(self, key, value): def _cast_parameter(self, key, value):
if key in self.callback_parameters: if key in self.callback_parameters:
param = self.callback_parameters[key] param = self.callback_parameters[key]
if param.annotation == bool: if param.annotation == bool:
@@ -174,7 +175,7 @@ class Command(BaseCommand):
if client_response: if client_response:
for k, v in client_response.items(): for k, v in client_response.items():
if k in self.callback_parameters: 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: if 'client_response' in self.callback_parameters:
new_kwargs['client_response'] = client_response 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 # Set the hx-swap-oob attribute on all elements returned by the callback
if isinstance(ret, (list, tuple)): if isinstance(ret, (list, tuple)):
for r in ret[1:]: 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") r.attrs["hx-swap-oob"] = r.attrs.get("hx-swap-oob", "true")
if not ret_from_bindings: if not ret_from_bindings:

View File

@@ -700,6 +700,65 @@ class TestTabsManagerRender:
expected = Div(cls=Contains("hidden")) expected = Div(cls=Contains("hidden"))
assert matches(tab_content, expected) 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 # Complete Render
# ========================================================================= # =========================================================================