diff --git a/src/myfasthtml/assets/datagrid/datagrid.css b/src/myfasthtml/assets/datagrid/datagrid.css index e755403..96dbaed 100644 --- a/src/myfasthtml/assets/datagrid/datagrid.css +++ b/src/myfasthtml/assets/datagrid/datagrid.css @@ -1,4 +1,3 @@ - /* ********************************************* */ /* ************* Datagrid Component ************ */ /* ********************************************* */ @@ -9,6 +8,11 @@ background-color: var(--color-base-200); border-radius: 10px 10px 0 0; 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 */ @@ -131,10 +135,21 @@ } /* Selection border - outlines the entire selection rectangle */ -.dt2-selection-border-top { border-top: 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); } +.dt2-selection-border-top { + border-top: 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); +} /* *********************************************** */ diff --git a/src/myfasthtml/controls/DataGrid.py b/src/myfasthtml/controls/DataGrid.py index 8ec725d..a3fa3be 100644 --- a/src/myfasthtml/controls/DataGrid.py +++ b/src/myfasthtml/controls/DataGrid.py @@ -667,7 +667,7 @@ class DataGrid(MultipleInstance): cls="dt2-cell dt2-resizable flex", ) - header_class = "dt2-row dt2-header" + header_class = "dt2-header" return Div( *[_mk_header(col_def) for col_def in self._state.columns], cls=header_class, @@ -829,7 +829,6 @@ class DataGrid(MultipleInstance): ) def mk_footers(self): - return self.mk_headers() return Div( *[Div( *[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns], diff --git a/src/myfasthtml/controls/DataGridsManager.py b/src/myfasthtml/controls/DataGridsManager.py index 14c0899..998f439 100644 --- a/src/myfasthtml/controls/DataGridsManager.py +++ b/src/myfasthtml/controls/DataGridsManager.py @@ -9,7 +9,7 @@ from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.DataGrid import DataGrid, DatagridConf from myfasthtml.controls.FileUpload import FileUpload 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.core.DataGridsRegistry import DataGridsRegistry 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)) return self._tabs_manager.show_tab(tab_id) - def new_grid(self): - selected_id = self._tree.get_selected_id() + def _create_and_register_grid(self, namespace: str, name: str, df: pd.DataFrame) -> DataGrid: + """ + 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: parent_id = self._tree.ensure_path("Untitled") else: @@ -115,43 +182,28 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider): parent_id = selected_id else: # leaf parent_id = node.parent - + + # Get namespace and generate unique name namespace = self._tree._state.items[parent_id].label name = self._generate_unique_sheet_name(parent_id) - - dg_conf = DatagridConf(namespace=namespace, name=name) - dg = DataGrid(self, conf=dg_conf, save_state=True) - dg.init_from_dataframe(pd.DataFrame()) - self._registry.put(namespace, name, dg.get_id()) - - tab_id = self._tabs_manager.create_tab(name, dg) - document = DocumentDefinition( - 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( - id=document.document_id, - label=name, - type="excel", - parent=parent_id, - bag=document.document_id - ) - self._tree.add_node(tree_node, parent_id=parent_id) - + + # Create and register DataGrid + dg = self._create_and_register_grid(namespace, name, pd.DataFrame()) + + # Create document and tab + tab_id, document = self._create_document(namespace, name, dg) + + # Add to tree + self._add_document_to_tree(document, parent_id) + + # UI-specific handling: open parent, select node, start rename if parent_id not in self._tree._state.opened: self._tree._state.opened.append(parent_id) - self._tree._state.selected = document.document_id self._tree._start_rename(document.document_id) - + return self._tree, self._tabs_manager.show_tab(tab_id) - + def _generate_unique_sheet_name(self, parent_id: str) -> str: children = self._tree._state.items[parent_id].children existing_labels = {self._tree._state.items[c].label for c in children} @@ -159,28 +211,24 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider): while f"Sheet{n}" in existing_labels: n += 1 return f"Sheet{n}" - + def open_from_excel(self, tab_id, file_upload: FileUpload): + # Read Excel data excel_content = file_upload.get_content() df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name()) namespace = file_upload.get_file_basename() 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 - dg.init_from_dataframe(df) - self._registry.put(namespace, name, dg.get_id()) - document = DocumentDefinition( - 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] # do not use append() other it won't be saved + + # Create and register DataGrid + dg = self._create_and_register_grid(namespace, name, df) + + # Create document with existing tab + tab_id, document = self._create_document(namespace, name, dg, tab_id=tab_id) + + # Add to tree 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) + self._add_document_to_tree(document, parent_id) + return self._mk_tree(), self._tabs_manager.change_tab_content(tab_id, document.name, dg) def select_document(self, node_id): @@ -318,7 +366,8 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider): ) 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: parent_id = tree.ensure_path(element.namespace, node_type="folder") tree.add_node(TreeNode(id=element.document_id, diff --git a/src/myfasthtml/controls/IconsHelper.py b/src/myfasthtml/controls/IconsHelper.py index d83e7a2..efa3428 100644 --- a/src/myfasthtml/controls/IconsHelper.py +++ b/src/myfasthtml/controls/IconsHelper.py @@ -2,7 +2,7 @@ from myfasthtml.core.constants import ColumnType from myfasthtml.icons.fluent import question20_regular, brain_circuit20_regular, number_symbol20_regular, \ number_row20_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_p3 import calendar_ltr20_regular @@ -12,6 +12,7 @@ default_icons = { False: checkbox_unchecked20_regular, "Brain": brain_circuit20_regular, + "TreeViewFolder" : folder20_regular, ColumnType.RowIndex: number_symbol20_regular, ColumnType.Text: text_field20_regular, diff --git a/src/myfasthtml/controls/TreeView.py b/src/myfasthtml/controls/TreeView.py index 87bc06c..60677f5 100644 --- a/src/myfasthtml/controls/TreeView.py +++ b/src/myfasthtml/controls/TreeView.py @@ -12,6 +12,7 @@ from typing import Optional from fasthtml.components import Div, Input, Span from myfasthtml.controls.BaseCommands import BaseCommands +from myfasthtml.controls.IconsHelper import IconsHelper from myfasthtml.controls.Keyboard import Keyboard from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command, CommandTemplate @@ -190,6 +191,8 @@ class TreeView(MultipleInstance): if 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]): """ @@ -344,7 +347,7 @@ class TreeView(MultipleInstance): """Start renaming a node (sets editing state and selection).""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") - + self._state.selected = node_id self._state.editing = node_id return self @@ -395,7 +398,7 @@ class TreeView(MultipleInstance): """Select a node.""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") - + # Cancel edit mode when selecting self._state.editing = None self._state.selected = node_id @@ -446,6 +449,7 @@ class TreeView(MultipleInstance): toggle = None # Label or input for editing + icon = IconsHelper.get(self._state.icon_config.get(node.type, None)) if is_editing: label_element = mk.mk(Input( name="node_label", @@ -453,18 +457,24 @@ class TreeView(MultipleInstance): cls="mf-treenode-input input input-sm" ), command=CommandTemplate("TreeView.SaveRename", self.commands.save_rename, args=[node_id])) else: - label_element = mk.mk( + label_element = mk.label( Span(node.label, cls="mf-treenode-label text-sm"), + icon=icon, + enable_button=False, command=self.commands.select_node(node_id) ) + offset = 20 + if icon is not None: + offset += 25 + # Node element node_element = Div( toggle, label_element, *([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 ''}", - style=f"padding-left: {level * 20}px" + style=f"padding-left: {level * offset}px" ) # Children (if expanded) diff --git a/src/myfasthtml/controls/helpers.py b/src/myfasthtml/controls/helpers.py index 420e170..e1bc59c 100644 --- a/src/myfasthtml/controls/helpers.py +++ b/src/myfasthtml/controls/helpers.py @@ -95,10 +95,14 @@ class mk: icon=None, size: str = "sm", cls='', + enable_button=True, command: Command | CommandTemplate = None, binding: Binding = None, **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 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)