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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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:

View File

@@ -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
# =========================================================================