diff --git a/src/myfasthtml/controls/DataGridsManager.py b/src/myfasthtml/controls/DataGridsManager.py
index 8c7d18c..f5e4fb1 100644
--- a/src/myfasthtml/controls/DataGridsManager.py
+++ b/src/myfasthtml/controls/DataGridsManager.py
@@ -50,7 +50,7 @@ class Commands(BaseCommands):
return Command("NewGrid",
"New grid",
self._owner,
- self._owner.new_grid)
+ self._owner.new_grid).htmx(target=f"#{self._owner._tree.get_id()}")
def open_from_excel(self, tab_id, file_upload):
return Command("OpenFromExcel",
@@ -104,6 +104,62 @@ 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()
+
+ if selected_id is None:
+ parent_id = self._tree.ensure_path("Untitled")
+ else:
+ node = self._tree._state.items[selected_id]
+ if node.type == "folder":
+ parent_id = selected_id
+ else: # leaf
+ parent_id = node.parent
+
+ 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)
+
+ 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}
+ n = 1
+ while f"Sheet{n}" in existing_labels:
+ n += 1
+ return f"Sheet{n}"
+
def open_from_excel(self, tab_id, file_upload: FileUpload):
excel_content = file_upload.get_content()
df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
@@ -257,7 +313,7 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
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", command=self.commands.clear_tree()),
+ mk.icon(table_add20_regular, tooltip="New grid", command=self.commands.new_grid()),
cls="flex"
)
diff --git a/src/myfasthtml/controls/TreeView.py b/src/myfasthtml/controls/TreeView.py
index 3dc8417..df99050 100644
--- a/src/myfasthtml/controls/TreeView.py
+++ b/src/myfasthtml/controls/TreeView.py
@@ -184,10 +184,10 @@ class TreeView(MultipleInstance):
self._state = TreeViewState(self)
self.conf = conf or TreeViewConf()
self.commands = Commands(self)
-
+
if items:
self._state.items = items
-
+
if self.conf.icons:
self._state.icon_config = self.conf.icons
@@ -350,6 +350,7 @@ class TreeView(MultipleInstance):
def _save_rename(self, node_id: str, node_label: str):
"""Save renamed node with new label."""
+ logger.debug(f"_save_rename {node_id=}, {node_label=}")
if node_id not in self._state.items:
raise ValueError(f"Node {node_id} does not exist")
@@ -393,7 +394,9 @@ 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
return self
@@ -401,11 +404,11 @@ class TreeView(MultipleInstance):
"""Render action buttons for a node (visible on hover)."""
is_leaf = len(self._state.items[node_id].children) == 0
conf = self.conf
-
- add_visible = conf.add_leaf if is_leaf else conf.add_node
- edit_visible = conf.edit_leaf if is_leaf else conf.edit_node
+
+ add_visible = conf.add_leaf if is_leaf else conf.add_node
+ edit_visible = conf.edit_leaf if is_leaf else conf.edit_node
delete_visible = conf.delete_leaf if is_leaf else conf.delete_node
-
+
buttons = []
if add_visible:
buttons.append(mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)))
@@ -413,7 +416,7 @@ class TreeView(MultipleInstance):
buttons.append(mk.icon(edit20_regular, command=self.commands.start_rename(node_id)))
if delete_visible:
buttons.append(mk.icon(delete20_regular, command=self.commands.delete_node(node_id)))
-
+
return Div(*buttons, cls="mf-treenode-actions")
def _render_node(self, node_id: str, level: int = 0):
diff --git a/tests/controls/test_datagridmanager.py b/tests/controls/test_datagridmanager.py
new file mode 100644
index 0000000..92375ee
--- /dev/null
+++ b/tests/controls/test_datagridmanager.py
@@ -0,0 +1,261 @@
+"""Unit tests for DataGridsManager component."""
+import shutil
+
+import pytest
+
+from myfasthtml.controls.DataGridsManager import DataGridsManager
+from myfasthtml.controls.TabsManager import TabsManager
+from myfasthtml.controls.TreeView import TreeNode
+from myfasthtml.core.instances import InstancesManager
+from .conftest import root_instance
+
+
+@pytest.fixture(autouse=True)
+def cleanup_db():
+ shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
+
+
+@pytest.fixture
+def datagrid_manager(root_instance):
+ """Create a DataGridsManager instance for testing."""
+ InstancesManager.reset()
+ TabsManager(root_instance) # just define it
+ return DataGridsManager(root_instance)
+
+
+class TestDataGridsManagerBehaviour:
+ """Tests for DataGridsManager behavior and logic."""
+
+ def test_i_can_create_new_grid_with_nothing_selected(self, datagrid_manager):
+ """Test creating a new grid when no node is selected.
+
+ Verifies that:
+ - Grid is created under "Untitled" folder
+ - Name is "Sheet1"
+ - Node is selected and in edit mode
+ - Document definition is created
+ """
+ result = datagrid_manager.new_grid()
+
+ # Verify tree structure
+ tree = datagrid_manager._tree
+ assert len(tree._state.items) == 2, "Should have Untitled folder + Sheet1 node"
+
+ # Find the Untitled folder and Sheet1 node
+ nodes = list(tree._state.items.values())
+ untitled = [n for n in nodes if n.label == "Untitled"][0]
+ sheet = [n for n in nodes if n.label == "Sheet1"][0]
+
+ # Verify hierarchy
+ assert untitled.parent is None, "Untitled should be root"
+ assert sheet.parent == untitled.id, "Sheet1 should be under Untitled"
+
+ # Verify selection and edit mode
+ assert tree._state.selected == sheet.id, "Sheet1 should be selected"
+ assert tree._state.editing == sheet.id, "Sheet1 should be in edit mode"
+
+ # Verify document definition
+ assert len(datagrid_manager._state.elements) == 1, "Should have one document"
+ doc = datagrid_manager._state.elements[0]
+ assert doc.namespace == "Untitled"
+ assert doc.name == "Sheet1"
+ assert doc.type == "excel"
+
+ def test_i_can_create_new_grid_under_selected_folder(self, datagrid_manager):
+ """Test creating a new grid when a folder is selected.
+
+ Verifies that:
+ - Grid is created under the selected folder
+ - Namespace matches folder name
+ """
+ # Create a folder and select it
+ folder_id = datagrid_manager._tree.ensure_path("MyFolder")
+ datagrid_manager._tree._select_node(folder_id)
+
+ result = datagrid_manager.new_grid()
+
+ # Verify the new grid is under MyFolder
+ tree = datagrid_manager._tree
+ nodes = list(tree._state.items.values())
+ sheet = [n for n in nodes if n.label == "Sheet1"][0]
+
+ assert sheet.parent == folder_id, "Sheet1 should be under MyFolder"
+
+ # Verify document definition
+ doc = datagrid_manager._state.elements[0]
+ assert doc.namespace == "MyFolder"
+ assert doc.name == "Sheet1"
+
+ def test_i_can_create_new_grid_under_selected_leaf_parent(self, datagrid_manager):
+ """Test creating a new grid when a leaf node is selected.
+
+ Verifies that:
+ - Grid is created under the parent of the selected leaf
+ - Not under the leaf itself
+ """
+ # Create a folder with a leaf
+ folder_id = datagrid_manager._tree.ensure_path("MyFolder")
+ leaf = TreeNode(label="ExistingSheet", type="excel", parent=folder_id)
+ datagrid_manager._tree.add_node(leaf, parent_id=folder_id)
+
+ # Select the leaf
+ datagrid_manager._tree._select_node(leaf.id)
+
+ result = datagrid_manager.new_grid()
+
+ # Verify the new grid is under MyFolder (not under ExistingSheet)
+ tree = datagrid_manager._tree
+ nodes = list(tree._state.items.values())
+ new_sheet = [n for n in nodes if n.label == "Sheet1"][0]
+
+ assert new_sheet.parent == folder_id, "Sheet1 should be under MyFolder (leaf's parent)"
+ assert new_sheet.parent != leaf.id, "Sheet1 should not be under the leaf"
+
+ def test_new_grid_generates_unique_sheet_names(self, datagrid_manager):
+ """Test that new_grid generates unique sequential sheet names.
+
+ Verifies Sheet1, Sheet2, Sheet3... generation.
+ """
+ # Create first grid
+ datagrid_manager.new_grid()
+ assert datagrid_manager._state.elements[0].name == "Sheet1"
+
+ # Create second grid
+ datagrid_manager.new_grid()
+ assert datagrid_manager._state.elements[1].name == "Sheet2"
+
+ # Create third grid
+ datagrid_manager.new_grid()
+ assert datagrid_manager._state.elements[2].name == "Sheet3"
+
+ def test_new_grid_expands_parent_folder(self, datagrid_manager):
+ """Test that creating a new grid automatically expands the parent folder.
+
+ Verifies parent is added to tree._state.opened.
+ """
+ result = datagrid_manager.new_grid()
+
+ tree = datagrid_manager._tree
+ nodes = list(tree._state.items.values())
+ untitled = [n for n in nodes if n.label == "Untitled"][0]
+
+ # Verify parent is expanded
+ assert untitled.id in tree._state.opened, "Parent folder should be expanded"
+
+ def test_new_grid_selects_and_edits_new_node(self, datagrid_manager):
+ """Test that new grid node is both selected and in edit mode.
+
+ Verifies _state.selected and _state.editing are set to new node.
+ """
+ result = datagrid_manager.new_grid()
+
+ tree = datagrid_manager._tree
+ nodes = list(tree._state.items.values())
+ sheet = [n for n in nodes if n.label == "Sheet1"][0]
+
+ # Verify selection
+ assert tree._state.selected == sheet.id, "New node should be selected"
+
+ # Verify edit mode
+ assert tree._state.editing == sheet.id, "New node should be in edit mode"
+
+ def test_new_grid_creates_document_definition(self, datagrid_manager):
+ """Test that new_grid creates a DocumentDefinition with correct fields.
+
+ Verifies document_id, namespace, name, type, tab_id, datagrid_id.
+ """
+ result = datagrid_manager.new_grid()
+
+ # Verify document was created
+ assert len(datagrid_manager._state.elements) == 1, "Should have one document"
+
+ doc = datagrid_manager._state.elements[0]
+
+ # Verify all fields
+ assert doc.document_id is not None, "Should have document_id"
+ assert isinstance(doc.document_id, str), "document_id should be string"
+ assert doc.namespace == "Untitled", "namespace should match parent folder"
+ assert doc.name == "Sheet1", "name should be Sheet1"
+ assert doc.type == "excel", "type should be excel"
+ assert doc.tab_id is not None, "Should have tab_id"
+ assert doc.datagrid_id is not None, "Should have datagrid_id"
+
+ def test_new_grid_creates_datagrid_and_registers(self, datagrid_manager):
+ """Test that new_grid creates a DataGrid and registers it.
+
+ Verifies DataGrid exists and is in registry with namespace.name.
+ """
+ result = datagrid_manager.new_grid()
+
+ doc = datagrid_manager._state.elements[0]
+
+ # Verify DataGrid is registered
+ tables = datagrid_manager._registry.get_all_tables()
+ assert "Untitled.Sheet1" in tables, "DataGrid should be registered as Untitled.Sheet1"
+
+ # Verify DataGrid exists in InstancesManager
+ from myfasthtml.core.instances import InstancesManager
+ datagrid = InstancesManager.get(datagrid_manager._session, doc.datagrid_id, None)
+ assert datagrid is not None, "DataGrid instance should exist"
+
+ def test_new_grid_creates_tab_with_datagrid(self, datagrid_manager):
+ """Test that new_grid creates a tab with correct label and content.
+
+ Verifies tab is created via TabsManager with DataGrid as content.
+ """
+ result = datagrid_manager.new_grid()
+
+ doc = datagrid_manager._state.elements[0]
+ tabs_manager = datagrid_manager._tabs_manager
+
+ # Verify tab exists in TabsManager
+ assert doc.tab_id in tabs_manager._state.tabs, "Tab should exist in TabsManager"
+
+ # Verify tab label
+ tab_metadata = tabs_manager._state.tabs[doc.tab_id]
+ assert tab_metadata['label'] == "Sheet1", "Tab label should be Sheet1"
+
+ def test_generate_unique_sheet_name_with_no_children(self, datagrid_manager):
+ """Test _generate_unique_sheet_name on an empty folder.
+
+ Verifies it returns "Sheet1" when no children exist.
+ """
+ folder_id = datagrid_manager._tree.ensure_path("EmptyFolder")
+
+ name = datagrid_manager._generate_unique_sheet_name(folder_id)
+
+ assert name == "Sheet1", "Should generate Sheet1 for empty folder"
+
+ def test_generate_unique_sheet_name_with_existing_sheets(self, datagrid_manager):
+ """Test _generate_unique_sheet_name with existing sheets.
+
+ Verifies it generates the next sequential number.
+ """
+ folder_id = datagrid_manager._tree.ensure_path("MyFolder")
+
+ # Add Sheet1 and Sheet2 manually
+ sheet1 = TreeNode(label="Sheet1", type="excel", parent=folder_id)
+ sheet2 = TreeNode(label="Sheet2", type="excel", parent=folder_id)
+ datagrid_manager._tree.add_node(sheet1, parent_id=folder_id)
+ datagrid_manager._tree.add_node(sheet2, parent_id=folder_id)
+
+ name = datagrid_manager._generate_unique_sheet_name(folder_id)
+
+ assert name == "Sheet3", "Should generate Sheet3 when Sheet1 and Sheet2 exist"
+
+ def test_generate_unique_sheet_name_skips_gaps(self, datagrid_manager):
+ """Test _generate_unique_sheet_name fills gaps in sequence.
+
+ Verifies it generates Sheet2 when Sheet1 and Sheet3 exist (missing Sheet2).
+ """
+ folder_id = datagrid_manager._tree.ensure_path("MyFolder")
+
+ # Add Sheet1 and Sheet3 (skip Sheet2)
+ sheet1 = TreeNode(label="Sheet1", type="excel", parent=folder_id)
+ sheet3 = TreeNode(label="Sheet3", type="excel", parent=folder_id)
+ datagrid_manager._tree.add_node(sheet1, parent_id=folder_id)
+ datagrid_manager._tree.add_node(sheet3, parent_id=folder_id)
+
+ name = datagrid_manager._generate_unique_sheet_name(folder_id)
+
+ assert name == "Sheet2", "Should generate Sheet2 to fill the gap"
diff --git a/tests/controls/test_treeview.py b/tests/controls/test_treeview.py
index 90af53c..76b1f5b 100644
--- a/tests/controls/test_treeview.py
+++ b/tests/controls/test_treeview.py
@@ -583,6 +583,44 @@ class TestTreeviewBehaviour:
assert len(tree_view._state.items) == 1, "Node should not have been added to items"
assert tree_view._state.items[node1.id] == node2, "Node should not have been replaced"
+ def test_selecting_node_cancels_edit_mode(self, root_instance):
+ """Test that selecting a node cancels any active edit mode."""
+ tree_view = TreeView(root_instance)
+ node1 = TreeNode(label="Node 1", type="folder")
+ node2 = TreeNode(label="Node 2", type="folder")
+
+ tree_view.add_node(node1)
+ tree_view.add_node(node2)
+
+ # Start editing node1
+ tree_view._start_rename(node1.id)
+ assert tree_view._state.editing == node1.id
+
+ # Select node2
+ tree_view._select_node(node2.id)
+
+ # Edit mode should be cancelled
+ assert tree_view._state.editing is None
+ assert tree_view._state.selected == node2.id
+
+ def test_selecting_same_editing_node_cancels_edit_mode(self, root_instance):
+ """Test that selecting the same node being edited cancels edit mode."""
+ tree_view = TreeView(root_instance)
+ node = TreeNode(label="Node", type="folder")
+
+ tree_view.add_node(node)
+
+ # Start editing the node
+ tree_view._start_rename(node.id)
+ assert tree_view._state.editing == node.id
+
+ # Select the same node
+ tree_view._select_node(node.id)
+
+ # Edit mode should be cancelled
+ assert tree_view._state.editing is None
+ assert tree_view._state.selected == node.id
+
class TestTreeViewRender:
"""Tests for TreeView HTML rendering."""