Refactored DataGridsManager.py for better reading

This commit is contained in:
2026-02-20 23:32:11 +01:00
parent c49f28da26
commit 730f55d65b
6 changed files with 141 additions and 63 deletions

View File

@@ -1,4 +1,3 @@
/* ********************************************* */ /* ********************************************* */
/* ************* Datagrid Component ************ */ /* ************* Datagrid Component ************ */
/* ********************************************* */ /* ********************************************* */
@@ -9,6 +8,11 @@
background-color: var(--color-base-200); background-color: var(--color-base-200);
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
min-width: max-content; /* Content width propagates to scrollable parent */ min-width: max-content; /* Content width propagates to scrollable parent */
height: 24px;
display: flex;
width: 100%;
font-size: var(--text-xl);
margin: 4px 0;
} }
/* Body */ /* Body */
@@ -131,10 +135,21 @@
} }
/* Selection border - outlines the entire selection rectangle */ /* Selection border - outlines the entire selection rectangle */
.dt2-selection-border-top { border-top: 2px solid var(--color-primary); } .dt2-selection-border-top {
.dt2-selection-border-bottom { border-bottom: 2px solid var(--color-primary); } border-top: 2px solid var(--color-primary);
.dt2-selection-border-left { border-left: 2px solid var(--color-primary); } }
.dt2-selection-border-right { border-right: 2px solid var(--color-primary); }
.dt2-selection-border-bottom {
border-bottom: 2px solid var(--color-primary);
}
.dt2-selection-border-left {
border-left: 2px solid var(--color-primary);
}
.dt2-selection-border-right {
border-right: 2px solid var(--color-primary);
}
/* *********************************************** */ /* *********************************************** */

View File

@@ -667,7 +667,7 @@ class DataGrid(MultipleInstance):
cls="dt2-cell dt2-resizable flex", cls="dt2-cell dt2-resizable flex",
) )
header_class = "dt2-row dt2-header" header_class = "dt2-header"
return Div( return Div(
*[_mk_header(col_def) for col_def in self._state.columns], *[_mk_header(col_def) for col_def in self._state.columns],
cls=header_class, cls=header_class,
@@ -829,7 +829,6 @@ class DataGrid(MultipleInstance):
) )
def mk_footers(self): def mk_footers(self):
return self.mk_headers()
return Div( return Div(
*[Div( *[Div(
*[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns], *[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns],

View File

@@ -9,7 +9,7 @@ from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.DataGrid import DataGrid, DatagridConf from myfasthtml.controls.DataGrid import DataGrid, DatagridConf
from myfasthtml.controls.FileUpload import FileUpload from myfasthtml.controls.FileUpload import FileUpload
from myfasthtml.controls.TabsManager import TabsManager from myfasthtml.controls.TabsManager import TabsManager
from myfasthtml.controls.TreeView import TreeView, TreeNode from myfasthtml.controls.TreeView import TreeView, TreeNode, TreeViewConf
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
from myfasthtml.core.DataGridsRegistry import DataGridsRegistry from myfasthtml.core.DataGridsRegistry import DataGridsRegistry
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
@@ -104,9 +104,76 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
file_upload.set_on_ok(self.commands.open_from_excel(tab_id, file_upload)) 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 new_grid(self): def _create_and_register_grid(self, namespace: str, name: str, df: pd.DataFrame) -> DataGrid:
selected_id = self._tree.get_selected_id() """
Create and register a DataGrid.
Args:
namespace: Grid namespace
name: Grid name
df: DataFrame to initialize the grid with
Returns:
Created DataGrid instance
"""
dg_conf = DatagridConf(namespace=namespace, name=name)
dg = DataGrid(self, conf=dg_conf, save_state=True)
dg.init_from_dataframe(df)
self._registry.put(namespace, name, dg.get_id())
return dg
def _create_document(self, namespace: str, name: str, datagrid: DataGrid, tab_id: str = None) -> tuple[
str, DocumentDefinition]:
"""
Create a DocumentDefinition and its associated tab.
Args:
namespace: Document namespace
name: Document name
datagrid: Associated DataGrid instance
tab_id: Optional existing tab ID. If None, creates a new tab
Returns:
Tuple of (tab_id, document)
"""
if tab_id is None:
tab_id = self._tabs_manager.create_tab(name, datagrid)
document = DocumentDefinition(
document_id=str(uuid.uuid4()),
namespace=namespace,
name=name,
type="excel",
tab_id=tab_id,
datagrid_id=datagrid.get_id()
)
self._state.elements = self._state.elements + [document]
return tab_id, document
def _add_document_to_tree(self, document: DocumentDefinition, parent_id: str) -> TreeNode:
"""
Add a document to the tree view.
Args:
document: Document to add
parent_id: Parent node ID in the tree
Returns:
Created TreeNode
"""
tree_node = TreeNode(
id=document.document_id,
label=document.name,
type=document.type,
parent=parent_id,
bag=document.document_id
)
self._tree.add_node(tree_node, parent_id=parent_id)
return tree_node
def new_grid(self):
# Determine parent folder
selected_id = self._tree.get_selected_id()
if selected_id is None: if selected_id is None:
parent_id = self._tree.ensure_path("Untitled") parent_id = self._tree.ensure_path("Untitled")
else: else:
@@ -116,37 +183,22 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
else: # leaf else: # leaf
parent_id = node.parent parent_id = node.parent
# Get namespace and generate unique name
namespace = self._tree._state.items[parent_id].label namespace = self._tree._state.items[parent_id].label
name = self._generate_unique_sheet_name(parent_id) name = self._generate_unique_sheet_name(parent_id)
dg_conf = DatagridConf(namespace=namespace, name=name) # Create and register DataGrid
dg = DataGrid(self, conf=dg_conf, save_state=True) dg = self._create_and_register_grid(namespace, name, pd.DataFrame())
dg.init_from_dataframe(pd.DataFrame())
self._registry.put(namespace, name, dg.get_id())
tab_id = self._tabs_manager.create_tab(name, dg) # Create document and tab
document = DocumentDefinition( tab_id, document = self._create_document(namespace, name, dg)
document_id=str(uuid.uuid4()),
namespace=namespace,
name=name,
type="excel",
tab_id=tab_id,
datagrid_id=dg.get_id()
)
self._state.elements = self._state.elements + [document]
tree_node = TreeNode( # Add to tree
id=document.document_id, self._add_document_to_tree(document, parent_id)
label=name,
type="excel",
parent=parent_id,
bag=document.document_id
)
self._tree.add_node(tree_node, parent_id=parent_id)
# UI-specific handling: open parent, select node, start rename
if parent_id not in self._tree._state.opened: if parent_id not in self._tree._state.opened:
self._tree._state.opened.append(parent_id) self._tree._state.opened.append(parent_id)
self._tree._state.selected = document.document_id self._tree._state.selected = document.document_id
self._tree._start_rename(document.document_id) self._tree._start_rename(document.document_id)
@@ -161,26 +213,22 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
return f"Sheet{n}" return f"Sheet{n}"
def open_from_excel(self, tab_id, file_upload: FileUpload): def open_from_excel(self, tab_id, file_upload: FileUpload):
# Read Excel data
excel_content = file_upload.get_content() excel_content = file_upload.get_content()
df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name()) df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
namespace = file_upload.get_file_basename() namespace = file_upload.get_file_basename()
name = file_upload.get_sheet_name() name = file_upload.get_sheet_name()
dg_conf = DatagridConf(namespace=namespace, name=name)
dg = DataGrid(self, conf=dg_conf, save_state=True) # first time the Datagrid is created # Create and register DataGrid
dg.init_from_dataframe(df) dg = self._create_and_register_grid(namespace, name, df)
self._registry.put(namespace, name, dg.get_id())
document = DocumentDefinition( # Create document with existing tab
document_id=str(uuid.uuid4()), tab_id, document = self._create_document(namespace, name, dg, tab_id=tab_id)
namespace=namespace,
name=name, # Add to tree
type="excel",
tab_id=tab_id,
datagrid_id=dg.get_id()
)
self._state.elements = self._state.elements + [document] # do not use append() other it won't be saved
parent_id = self._tree.ensure_path(document.namespace) parent_id = self._tree.ensure_path(document.namespace)
tree_node = TreeNode(label=document.name, type="excel", parent=parent_id) self._add_document_to_tree(document, 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, dg) return self._mk_tree(), self._tabs_manager.change_tab_content(tab_id, document.name, dg)
def select_document(self, node_id): def select_document(self, node_id):
@@ -318,7 +366,8 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
) )
def _mk_tree(self): def _mk_tree(self):
tree = TreeView(self, _id="-treeview") conf = TreeViewConf(add_leaf=False, icons={"folder": "database20_regular", "excel": "table20_regular"})
tree = TreeView(self, conf=conf, _id="-treeview")
for element in self._state.elements: for element in self._state.elements:
parent_id = tree.ensure_path(element.namespace, node_type="folder") parent_id = tree.ensure_path(element.namespace, node_type="folder")
tree.add_node(TreeNode(id=element.document_id, tree.add_node(TreeNode(id=element.document_id,

View File

@@ -2,7 +2,7 @@ from myfasthtml.core.constants import ColumnType
from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \ from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \
number_row20_regular number_row20_regular
from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \ from myfasthtml.icons.fluent_p1 import checkbox_checked20_regular, checkbox_unchecked20_regular, \
checkbox_checked20_filled, math_formula16_regular checkbox_checked20_filled, math_formula16_regular, folder20_regular
from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular from myfasthtml.icons.fluent_p2 import text_field20_regular, text_bullet_list_square20_regular
from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular from myfasthtml.icons.fluent_p3 import calendar_ltr20_regular
@@ -12,6 +12,7 @@ default_icons = {
False: checkbox_unchecked20_regular, False: checkbox_unchecked20_regular,
"Brain": brain_circuit20_regular, "Brain": brain_circuit20_regular,
"TreeViewFolder" : folder20_regular,
ColumnType.RowIndex: number_symbol20_regular, ColumnType.RowIndex: number_symbol20_regular,
ColumnType.Text: text_field20_regular, ColumnType.Text: text_field20_regular,

View File

@@ -12,6 +12,7 @@ from typing import Optional
from fasthtml.components import Div, Input, Span from fasthtml.components import Div, Input, Span
from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.IconsHelper import IconsHelper
from myfasthtml.controls.Keyboard import Keyboard from myfasthtml.controls.Keyboard import Keyboard
from myfasthtml.controls.helpers import mk from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command, CommandTemplate from myfasthtml.core.commands import Command, CommandTemplate
@@ -190,6 +191,8 @@ class TreeView(MultipleInstance):
if self.conf.icons: if self.conf.icons:
self._state.icon_config = self.conf.icons self._state.icon_config = self.conf.icons
else:
self._state.icon_config = {"folder": "TreeViewFolder"}
def set_icon_config(self, config: dict[str, str]): def set_icon_config(self, config: dict[str, str]):
""" """
@@ -446,6 +449,7 @@ class TreeView(MultipleInstance):
toggle = None toggle = None
# Label or input for editing # Label or input for editing
icon = IconsHelper.get(self._state.icon_config.get(node.type, None))
if is_editing: if is_editing:
label_element = mk.mk(Input( label_element = mk.mk(Input(
name="node_label", name="node_label",
@@ -453,18 +457,24 @@ class TreeView(MultipleInstance):
cls="mf-treenode-input input input-sm" cls="mf-treenode-input input input-sm"
), command=CommandTemplate("TreeView.SaveRename", self.commands.save_rename, args=[node_id])) ), command=CommandTemplate("TreeView.SaveRename", self.commands.save_rename, args=[node_id]))
else: else:
label_element = mk.mk( label_element = mk.label(
Span(node.label, cls="mf-treenode-label text-sm"), Span(node.label, cls="mf-treenode-label text-sm"),
icon=icon,
enable_button=False,
command=self.commands.select_node(node_id) command=self.commands.select_node(node_id)
) )
offset = 20
if icon is not None:
offset += 25
# Node element # Node element
node_element = Div( node_element = Div(
toggle, toggle,
label_element, label_element,
*([self._render_action_buttons(node_id)] if not is_editing else []), *([self._render_action_buttons(node_id)] if not is_editing else []),
cls=f"mf-treenode flex {'selected' if is_selected and not is_editing else ''}", cls=f"mf-treenode flex {'selected' if is_selected and not is_editing else ''}",
style=f"padding-left: {level * 20}px" style=f"padding-left: {level * offset}px"
) )
# Children (if expanded) # Children (if expanded)

View File

@@ -95,10 +95,14 @@ class mk:
icon=None, icon=None,
size: str = "sm", size: str = "sm",
cls='', cls='',
enable_button=True,
command: Command | CommandTemplate = None, command: Command | CommandTemplate = None,
binding: Binding = None, binding: Binding = None,
**kwargs): **kwargs):
merged_cls = merge_classes("flex truncate items-center pr-2", "mf-button" if command else None, cls, kwargs) merged_cls = merge_classes("flex truncate items-center pr-2",
"mf-button" if command and enable_button else None,
cls,
kwargs)
icon_part = Span(icon, cls=f"mf-icon-{mk.convert_size(size)} mr-1") if icon else None icon_part = Span(icon, cls=f"mf-icon-{mk.convert_size(size)} mr-1") if icon else None
text_part = Span(text, cls=f"text-{size} truncate") text_part = Span(text, cls=f"text-{size} truncate")
return mk.mk(Label(icon_part, text_part, cls=merged_cls, **kwargs), command=command, binding=binding) return mk.mk(Label(icon_part, text_part, cls=merged_cls, **kwargs), command=command, binding=binding)