feat: implement new grid creation with inline rename in DataGridsManager
- Add new_grid() method to create empty DataGrid under selected folder/leaf parent - Generate unique sheet names (Sheet1, Sheet2, ...) with _generate_unique_sheet_name() - Auto-select and open new node in edit mode for immediate renaming - Fix TreeView to cancel edit mode when selecting any node - Wire "New grid" icon to new_grid() instead of clear_tree() - Add 14 unit tests covering new_grid() scenarios and TreeView behavior
This commit is contained in:
@@ -50,7 +50,7 @@ class Commands(BaseCommands):
|
|||||||
return Command("NewGrid",
|
return Command("NewGrid",
|
||||||
"New grid",
|
"New grid",
|
||||||
self._owner,
|
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):
|
def open_from_excel(self, tab_id, file_upload):
|
||||||
return Command("OpenFromExcel",
|
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))
|
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):
|
||||||
|
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):
|
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
||||||
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())
|
||||||
@@ -257,7 +313,7 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
|
|||||||
def mk_main_icons(self):
|
def mk_main_icons(self):
|
||||||
return Div(
|
return Div(
|
||||||
mk.icon(folder_open20_regular, tooltip="Upload from source", command=self.commands.upload_from_source()),
|
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"
|
cls="flex"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ class TreeView(MultipleInstance):
|
|||||||
|
|
||||||
def _save_rename(self, node_id: str, node_label: str):
|
def _save_rename(self, node_id: str, node_label: str):
|
||||||
"""Save renamed node with new label."""
|
"""Save renamed node with new label."""
|
||||||
|
logger.debug(f"_save_rename {node_id=}, {node_label=}")
|
||||||
if node_id not in self._state.items:
|
if node_id not in self._state.items:
|
||||||
raise ValueError(f"Node {node_id} does not exist")
|
raise ValueError(f"Node {node_id} does not exist")
|
||||||
|
|
||||||
@@ -394,6 +395,8 @@ class TreeView(MultipleInstance):
|
|||||||
if node_id not in self._state.items:
|
if node_id not in self._state.items:
|
||||||
raise ValueError(f"Node {node_id} does not exist")
|
raise ValueError(f"Node {node_id} does not exist")
|
||||||
|
|
||||||
|
# Cancel edit mode when selecting
|
||||||
|
self._state.editing = None
|
||||||
self._state.selected = node_id
|
self._state.selected = node_id
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
261
tests/controls/test_datagridmanager.py
Normal file
261
tests/controls/test_datagridmanager.py
Normal file
@@ -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"
|
||||||
@@ -583,6 +583,44 @@ class TestTreeviewBehaviour:
|
|||||||
assert len(tree_view._state.items) == 1, "Node should not have been added to items"
|
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"
|
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:
|
class TestTreeViewRender:
|
||||||
"""Tests for TreeView HTML rendering."""
|
"""Tests for TreeView HTML rendering."""
|
||||||
|
|||||||
Reference in New Issue
Block a user