I can open an excel file and see its content
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user