DatagridsManager : I can see the opened folders in the Treeview

This commit is contained in:
2025-12-07 20:57:20 +01:00
parent dc5ec450f0
commit 3aa36a91aa
7 changed files with 323 additions and 24 deletions

View File

@@ -445,7 +445,6 @@
/* Empty Content State */
.mf-empty-content {
display: flex;
align-items: center;
justify-content: center;
height: 100%;

View File

@@ -1,3 +1,5 @@
from dataclasses import dataclass
import pandas as pd
from fastcore.basics import NotStr
from fasthtml.components import Div
@@ -5,14 +7,31 @@ from fasthtml.components import Div
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.TreeView import TreeView
from myfasthtml.controls.TreeView import TreeView, TreeNode
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance, InstancesManager
from myfasthtml.icons.fluent_p1 import table_add20_regular
from myfasthtml.icons.fluent_p3 import folder_open20_regular
@dataclass
class DocumentDefinition:
namespace: str
name: str
type: str
tab_id: str
datagrid_id: str
class DataGridsState(DbObject):
def __init__(self, owner, name=None):
super().__init__(owner, name=name)
with self.initializing():
self.elements: list = []
class Commands(BaseCommands):
def upload_from_source(self):
return Command("UploadFromSource",
@@ -29,14 +48,20 @@ class Commands(BaseCommands):
"Open from Excel",
self._owner.open_from_excel,
tab_id,
file_upload).htmx(target=None)
file_upload).htmx(target=f"#{self._owner._tree.get_id()}")
def clear_tree(self):
return Command("ClearTree",
"Clear tree",
self._owner.clear_tree).htmx(target=f"#{self._owner._tree.get_id()}")
class DataGridsManager(MultipleInstance):
def __init__(self, parent, _id=None):
super().__init__(parent, _id=_id)
self.tree = TreeView(self, _id="-treeview")
self.commands = Commands(self)
self._state = DataGridsState(self)
self._tree = self._mk_tree()
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager)
def upload_from_source(self):
@@ -45,22 +70,45 @@ class DataGridsManager(MultipleInstance):
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, file_upload):
def open_from_excel(self, tab_id, file_upload: FileUpload):
excel_content = file_upload.get_content()
df = pd.read_excel(excel_content)
content = df.to_html(index=False)
return self._tabs_manager.change_tab_content(tab_id, file_upload.get_file_name(), NotStr(content))
document = DocumentDefinition(
namespace=file_upload.get_file_basename(),
name=file_upload.get_sheet_name(),
type="excel",
tab_id=tab_id,
datagrid_id=None
)
self._state.elements = self._state.elements + [document]
parent_id = self._tree.ensure_path(document.namespace)
tree_node = TreeNode(label=document.name, type="excel", parent=parent_id)
self._tree.add_node(tree_node, parent_id=parent_id)
return self._mk_tree(), self._tabs_manager.change_tab_content(tab_id, document.name, NotStr(content))
def clear_tree(self):
self._state.elements = []
self._tree.clear()
return self._tree
def mk_main_icons(self):
return Div(
mk.icon(folder_open20_regular, tooltip="Upload from source", command=self.commands.upload_from_source()),
mk.icon(table_add20_regular, tooltip="New grid"),
mk.icon(table_add20_regular, tooltip="New grid", command=self.commands.clear_tree()),
cls="flex"
)
def _mk_tree(self):
tree = TreeView(self, _id="-treeview")
for element in self._state.elements:
parent_id = tree.ensure_path(element.namespace)
tree.add_node(TreeNode(label=element.name, type=element.type, parent=parent_id))
return tree
def render(self):
return Div(
self.tree,
self._tree,
id=self._id,
)

View File

@@ -33,8 +33,11 @@ class Commands(BaseCommands):
def __init__(self, owner):
super().__init__(owner)
def upload_file(self):
def on_file_uploaded(self):
return Command("UploadFile", "Upload file", self._owner.upload_file).htmx(target=f"#sn_{self._id}")
def on_sheet_selected(self):
return Command("SheetSelected", "Sheet selected", self._owner.select_sheet).htmx(target=f"#sn_{self._id}")
class FileUpload(MultipleInstance):
@@ -66,6 +69,11 @@ class FileUpload(MultipleInstance):
return self.mk_sheet_selector()
def select_sheet(self, sheet_name: str):
logger.debug(f"select_sheet: {sheet_name=}")
self._state.ns_selected_sheet_name = sheet_name
return self.mk_sheet_selector()
def mk_sheet_selector(self):
options = [Option("Choose a file...", selected=True, disabled=True)] if self._state.ns_sheets_names is None else \
[Option(
@@ -73,12 +81,12 @@ class FileUpload(MultipleInstance):
selected=True if name == self._state.ns_selected_sheet_name else None,
) for name in self._state.ns_sheets_names]
return Select(
return mk.mk(Select(
*options,
name="sheet_name",
id=f"sn_{self._id}", # sn stands for 'sheet name'
cls="select select-bordered select-sm w-full ml-2"
)
), command=self.commands.on_sheet_selected())
def get_content(self):
return self._state.ns_file_content
@@ -86,6 +94,15 @@ class FileUpload(MultipleInstance):
def get_file_name(self):
return self._state.ns_file_name
def get_file_basename(self):
if self._state.ns_file_name is None:
return None
return self._state.ns_file_name.split(".")[0]
def get_sheet_name(self):
return self._state.ns_selected_sheet_name
@staticmethod
def get_sheets_names(file_content):
try:
@@ -108,7 +125,7 @@ class FileUpload(MultipleInstance):
hx_encoding='multipart/form-data',
cls="file-input file-input-bordered file-input-sm w-full",
),
command=self.commands.upload_file()
command=self.commands.on_file_uploaded()
),
self.mk_sheet_selector(),
cls="flex"

View File

@@ -192,7 +192,7 @@ class TabsManager(MultipleInstance):
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._mk_tabs_header_wrapper(oob),
self._wrap_tab_content(tab_content, is_new))
else:
logger.debug(f" Content already in client memory. Just switch.")

View File

@@ -173,7 +173,7 @@ class TreeView(MultipleInstance):
Format: {type: "provider.icon_name"}
"""
self._state.icon_config = config
def add_node(self, node: TreeNode, parent_id: Optional[str] = None, insert_index: Optional[int] = None):
"""
Add a node to the tree.
@@ -185,6 +185,9 @@ class TreeView(MultipleInstance):
If None, appends to end. If provided, inserts at that position.
"""
self._state.items[node.id] = node
if parent_id is None and node.parent is not None:
parent_id = node.parent
node.parent = parent_id
if parent_id and parent_id in self._state.items:
@@ -195,12 +198,66 @@ class TreeView(MultipleInstance):
else:
parent.children.append(node.id)
def ensure_path(self, path: str):
"""Add a node to the tree based on a path string.
Args:
path: Dot-separated path string (e.g., "folder1.folder2.file")
Raises:
ValueError: If path contains empty parts after stripping
"""
if path is None:
raise ValueError(f"Invalid path: path is None")
path = path.strip().strip(".")
if path == "":
raise ValueError(f"Invalid path: path is empty")
parent_id = None
current_nodes = [node for node in self._state.items.values() if node.parent is None]
path_parts = path.split(".")
for part in path_parts:
part = part.strip()
# Validate that part is not empty after stripping
if part == "":
raise ValueError(f"Invalid path: path contains empty parts")
node = [node for node in current_nodes if node.label == part]
if len(node) == 0:
# create the node
node = TreeNode(label=part, type="folder")
self.add_node(node, parent_id=parent_id)
else:
node = node[0]
current_nodes = [self._state.items[node_id] for node_id in node.children]
parent_id = node.id
return parent_id
def get_selected_id(self):
if self._state.selected is None:
return None
return self._state.items[self._state.selected].id
def expand_all(self):
"""Expand all nodes that have children."""
for node_id, node in self._state.items.items():
if node.children and node_id not in self._state.opened:
self._state.opened.append(node_id)
def clear(self):
state = self._state.copy()
state.items = {}
state.opened = []
state.selected = None
state.editing = None
self._state.update(state)
return self
def _toggle_node(self, node_id: str):
"""Toggle expand/collapse state of a node."""
if node_id in self._state.opened: