DatagridsManager : I can see the opened folders in the Treeview
This commit is contained in:
@@ -445,7 +445,6 @@
|
|||||||
|
|
||||||
/* Empty Content State */
|
/* Empty Content State */
|
||||||
.mf-empty-content {
|
.mf-empty-content {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from fastcore.basics import NotStr
|
from fastcore.basics import NotStr
|
||||||
from fasthtml.components import Div
|
from fasthtml.components import Div
|
||||||
@@ -5,14 +7,31 @@ from fasthtml.components import Div
|
|||||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||||
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
|
from myfasthtml.controls.TreeView import TreeView, TreeNode
|
||||||
from myfasthtml.controls.helpers import mk
|
from myfasthtml.controls.helpers import mk
|
||||||
from myfasthtml.core.commands import Command
|
from myfasthtml.core.commands import Command
|
||||||
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.instances import MultipleInstance, InstancesManager
|
from myfasthtml.core.instances import MultipleInstance, InstancesManager
|
||||||
from myfasthtml.icons.fluent_p1 import table_add20_regular
|
from myfasthtml.icons.fluent_p1 import table_add20_regular
|
||||||
from myfasthtml.icons.fluent_p3 import folder_open20_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):
|
class Commands(BaseCommands):
|
||||||
def upload_from_source(self):
|
def upload_from_source(self):
|
||||||
return Command("UploadFromSource",
|
return Command("UploadFromSource",
|
||||||
@@ -29,14 +48,20 @@ class Commands(BaseCommands):
|
|||||||
"Open from Excel",
|
"Open from Excel",
|
||||||
self._owner.open_from_excel,
|
self._owner.open_from_excel,
|
||||||
tab_id,
|
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):
|
class DataGridsManager(MultipleInstance):
|
||||||
def __init__(self, parent, _id=None):
|
def __init__(self, parent, _id=None):
|
||||||
super().__init__(parent, _id=_id)
|
super().__init__(parent, _id=_id)
|
||||||
self.tree = TreeView(self, _id="-treeview")
|
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
|
self._state = DataGridsState(self)
|
||||||
|
self._tree = self._mk_tree()
|
||||||
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager)
|
self._tabs_manager = InstancesManager.get_by_type(self._session, TabsManager)
|
||||||
|
|
||||||
def upload_from_source(self):
|
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))
|
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 open_from_excel(self, tab_id, file_upload):
|
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(excel_content)
|
df = pd.read_excel(excel_content)
|
||||||
content = df.to_html(index=False)
|
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):
|
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"),
|
mk.icon(table_add20_regular, tooltip="New grid", command=self.commands.clear_tree()),
|
||||||
cls="flex"
|
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):
|
def render(self):
|
||||||
return Div(
|
return Div(
|
||||||
self.tree,
|
self._tree,
|
||||||
id=self._id,
|
id=self._id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ class Commands(BaseCommands):
|
|||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
super().__init__(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}")
|
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):
|
class FileUpload(MultipleInstance):
|
||||||
@@ -66,6 +69,11 @@ class FileUpload(MultipleInstance):
|
|||||||
|
|
||||||
return self.mk_sheet_selector()
|
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):
|
def mk_sheet_selector(self):
|
||||||
options = [Option("Choose a file...", selected=True, disabled=True)] if self._state.ns_sheets_names is None else \
|
options = [Option("Choose a file...", selected=True, disabled=True)] if self._state.ns_sheets_names is None else \
|
||||||
[Option(
|
[Option(
|
||||||
@@ -73,12 +81,12 @@ class FileUpload(MultipleInstance):
|
|||||||
selected=True if name == self._state.ns_selected_sheet_name else None,
|
selected=True if name == self._state.ns_selected_sheet_name else None,
|
||||||
) for name in self._state.ns_sheets_names]
|
) for name in self._state.ns_sheets_names]
|
||||||
|
|
||||||
return Select(
|
return mk.mk(Select(
|
||||||
*options,
|
*options,
|
||||||
name="sheet_name",
|
name="sheet_name",
|
||||||
id=f"sn_{self._id}", # sn stands for 'sheet name'
|
id=f"sn_{self._id}", # sn stands for 'sheet name'
|
||||||
cls="select select-bordered select-sm w-full ml-2"
|
cls="select select-bordered select-sm w-full ml-2"
|
||||||
)
|
), command=self.commands.on_sheet_selected())
|
||||||
|
|
||||||
def get_content(self):
|
def get_content(self):
|
||||||
return self._state.ns_file_content
|
return self._state.ns_file_content
|
||||||
@@ -86,6 +94,15 @@ class FileUpload(MultipleInstance):
|
|||||||
def get_file_name(self):
|
def get_file_name(self):
|
||||||
return self._state.ns_file_name
|
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
|
@staticmethod
|
||||||
def get_sheets_names(file_content):
|
def get_sheets_names(file_content):
|
||||||
try:
|
try:
|
||||||
@@ -108,7 +125,7 @@ class FileUpload(MultipleInstance):
|
|||||||
hx_encoding='multipart/form-data',
|
hx_encoding='multipart/form-data',
|
||||||
cls="file-input file-input-bordered file-input-sm w-full",
|
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(),
|
self.mk_sheet_selector(),
|
||||||
cls="flex"
|
cls="flex"
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class TabsManager(MultipleInstance):
|
|||||||
self._state.ns_tabs_sent_to_client.add(tab_id)
|
self._state.ns_tabs_sent_to_client.add(tab_id)
|
||||||
tab_content = self._mk_tab_content(tab_id, content)
|
tab_content = self._mk_tab_content(tab_id, content)
|
||||||
return (self._mk_tabs_controller(oob),
|
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))
|
self._wrap_tab_content(tab_content, is_new))
|
||||||
else:
|
else:
|
||||||
logger.debug(f" Content already in client memory. Just switch.")
|
logger.debug(f" Content already in client memory. Just switch.")
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class TreeView(MultipleInstance):
|
|||||||
Format: {type: "provider.icon_name"}
|
Format: {type: "provider.icon_name"}
|
||||||
"""
|
"""
|
||||||
self._state.icon_config = config
|
self._state.icon_config = config
|
||||||
|
|
||||||
def add_node(self, node: TreeNode, parent_id: Optional[str] = None, insert_index: Optional[int] = None):
|
def add_node(self, node: TreeNode, parent_id: Optional[str] = None, insert_index: Optional[int] = None):
|
||||||
"""
|
"""
|
||||||
Add a node to the tree.
|
Add a node to the tree.
|
||||||
@@ -185,6 +185,9 @@ class TreeView(MultipleInstance):
|
|||||||
If None, appends to end. If provided, inserts at that position.
|
If None, appends to end. If provided, inserts at that position.
|
||||||
"""
|
"""
|
||||||
self._state.items[node.id] = node
|
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
|
node.parent = parent_id
|
||||||
|
|
||||||
if parent_id and parent_id in self._state.items:
|
if parent_id and parent_id in self._state.items:
|
||||||
@@ -195,12 +198,66 @@ class TreeView(MultipleInstance):
|
|||||||
else:
|
else:
|
||||||
parent.children.append(node.id)
|
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):
|
def expand_all(self):
|
||||||
"""Expand all nodes that have children."""
|
"""Expand all nodes that have children."""
|
||||||
for node_id, node in self._state.items.items():
|
for node_id, node in self._state.items.items():
|
||||||
if node.children and node_id not in self._state.opened:
|
if node.children and node_id not in self._state.opened:
|
||||||
self._state.opened.append(node_id)
|
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):
|
def _toggle_node(self, node_id: str):
|
||||||
"""Toggle expand/collapse state of a node."""
|
"""Toggle expand/collapse state of a node."""
|
||||||
if node_id in self._state.opened:
|
if node_id in self._state.opened:
|
||||||
|
|||||||
@@ -710,7 +710,8 @@ class TestTabsManagerRender:
|
|||||||
hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");',
|
hx_on__after_settle=f'updateTabs("{tabs_manager.get_id()}-controller");',
|
||||||
hx_swap_oob="true"), # the controller is correctly updated
|
hx_swap_oob="true"), # the controller is correctly updated
|
||||||
Div(
|
Div(
|
||||||
id=f'{tabs_manager.get_id()}-header-wrapper'
|
id=f'{tabs_manager.get_id()}-header-wrapper',
|
||||||
|
hx_swap_oob="true"
|
||||||
), # content of the header
|
), # content of the header
|
||||||
Div(
|
Div(
|
||||||
Div(Div("My Content")),
|
Div(Div("My Content")),
|
||||||
@@ -750,7 +751,7 @@ class TestTabsManagerRender:
|
|||||||
|
|
||||||
expected = (
|
expected = (
|
||||||
Div(data_active_tab=tab_id, hx_swap_oob="true"),
|
Div(data_active_tab=tab_id, hx_swap_oob="true"),
|
||||||
Div(id=f'{tabs_manager.get_id()}-header-wrapper'),
|
Div(id=f'{tabs_manager.get_id()}-header-wrapper', hx_swap_oob="true"),
|
||||||
Div(
|
Div(
|
||||||
Div("New Content"),
|
Div("New Content"),
|
||||||
id=f'{tabs_manager.get_id()}-{tab_id}-content',
|
id=f'{tabs_manager.get_id()}-{tab_id}-content',
|
||||||
|
|||||||
@@ -391,6 +391,184 @@ class TestTreeviewBehaviour:
|
|||||||
assert tree_view._state.items[node1.id].type == "folder"
|
assert tree_view._state.items[node1.id].type == "folder"
|
||||||
assert tree_view._state.items[node2.id].label == "Node 2"
|
assert tree_view._state.items[node2.id].label == "Node 2"
|
||||||
assert tree_view._state.items[node2.id].type == "file"
|
assert tree_view._state.items[node2.id].type == "file"
|
||||||
|
|
||||||
|
def test_i_can_ensure_simple_path(self, root_instance):
|
||||||
|
"""Test that ensure_path creates a simple hierarchical path."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
tree_view.ensure_path("folder1.folder2.file")
|
||||||
|
|
||||||
|
# Should have 3 nodes
|
||||||
|
assert len(tree_view._state.items) == 3
|
||||||
|
|
||||||
|
# Find nodes by label
|
||||||
|
nodes = list(tree_view._state.items.values())
|
||||||
|
folder1 = [n for n in nodes if n.label == "folder1"][0]
|
||||||
|
folder2 = [n for n in nodes if n.label == "folder2"][0]
|
||||||
|
file_node = [n for n in nodes if n.label == "file"][0]
|
||||||
|
|
||||||
|
# Verify hierarchy
|
||||||
|
assert folder1.parent is None
|
||||||
|
assert folder2.parent == folder1.id
|
||||||
|
assert file_node.parent == folder2.id
|
||||||
|
|
||||||
|
# Verify children relationships
|
||||||
|
assert folder2.id in folder1.children
|
||||||
|
assert file_node.id in folder2.children
|
||||||
|
|
||||||
|
# Verify all nodes have folder type
|
||||||
|
assert folder1.type == "folder"
|
||||||
|
assert folder2.type == "folder"
|
||||||
|
assert file_node.type == "folder"
|
||||||
|
|
||||||
|
def test_i_can_ensure_path_with_existing_nodes(self, root_instance):
|
||||||
|
"""Test that ensure_path reuses existing nodes in the path."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
# Create initial path
|
||||||
|
tree_view.ensure_path("folder1.folder2.file1")
|
||||||
|
initial_count = len(tree_view._state.items)
|
||||||
|
|
||||||
|
# Ensure same path again
|
||||||
|
tree_view.ensure_path("folder1.folder2.file1")
|
||||||
|
|
||||||
|
# Should not create any new nodes
|
||||||
|
assert len(tree_view._state.items) == initial_count
|
||||||
|
assert len(tree_view._state.items) == 3
|
||||||
|
|
||||||
|
def test_i_can_ensure_path_partially_existing(self, root_instance):
|
||||||
|
"""Test that ensure_path creates only missing nodes when path partially exists."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
# Create initial path
|
||||||
|
tree_view.ensure_path("folder1.folder2")
|
||||||
|
assert len(tree_view._state.items) == 2
|
||||||
|
|
||||||
|
# Extend the path
|
||||||
|
tree_view.ensure_path("folder1.folder2.subfolder.file")
|
||||||
|
|
||||||
|
# Should have 4 nodes total (folder1, folder2, subfolder, file)
|
||||||
|
assert len(tree_view._state.items) == 4
|
||||||
|
|
||||||
|
# Find all nodes
|
||||||
|
nodes = list(tree_view._state.items.values())
|
||||||
|
folder1 = [n for n in nodes if n.label == "folder1"][0]
|
||||||
|
folder2 = [n for n in nodes if n.label == "folder2"][0]
|
||||||
|
subfolder = [n for n in nodes if n.label == "subfolder"][0]
|
||||||
|
file_node = [n for n in nodes if n.label == "file"][0]
|
||||||
|
|
||||||
|
# Verify new nodes are children of existing path
|
||||||
|
assert subfolder.parent == folder2.id
|
||||||
|
assert file_node.parent == subfolder.id
|
||||||
|
|
||||||
|
def test_i_cannot_ensure_path_with_none(self, root_instance):
|
||||||
|
"""Test that ensure_path raises ValueError when path is None."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid path.*None"):
|
||||||
|
tree_view.ensure_path(None)
|
||||||
|
|
||||||
|
def test_i_cannot_ensure_path_with_empty_string(self, root_instance):
|
||||||
|
"""Test that ensure_path raises ValueError for empty strings after stripping."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid path.*empty"):
|
||||||
|
tree_view.ensure_path(" ")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid path.*empty"):
|
||||||
|
tree_view.ensure_path("")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid path.*empty"):
|
||||||
|
tree_view.ensure_path("...")
|
||||||
|
|
||||||
|
def test_i_can_ensure_path_strips_leading_trailing_dots(self, root_instance):
|
||||||
|
"""Test that ensure_path strips leading and trailing dots."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
tree_view.ensure_path(".folder1.folder2.")
|
||||||
|
|
||||||
|
# Should only create 2 nodes (folder1, folder2)
|
||||||
|
assert len(tree_view._state.items) == 2
|
||||||
|
|
||||||
|
nodes = list(tree_view._state.items.values())
|
||||||
|
labels = [n.label for n in nodes]
|
||||||
|
|
||||||
|
assert "folder1" in labels
|
||||||
|
assert "folder2" in labels
|
||||||
|
|
||||||
|
def test_i_can_ensure_path_strips_spaces_in_parts(self, root_instance):
|
||||||
|
"""Test that ensure_path strips spaces from each path part."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
tree_view.ensure_path("folder1. folder2 . folder3 ")
|
||||||
|
|
||||||
|
# Should create 3 nodes with trimmed labels
|
||||||
|
assert len(tree_view._state.items) == 3
|
||||||
|
|
||||||
|
nodes = list(tree_view._state.items.values())
|
||||||
|
labels = [n.label for n in nodes]
|
||||||
|
|
||||||
|
assert "folder1" in labels
|
||||||
|
assert "folder2" in labels
|
||||||
|
assert "folder3" in labels
|
||||||
|
|
||||||
|
def test_ensure_path_creates_folder_type_nodes(self, root_instance):
|
||||||
|
"""Test that ensure_path creates nodes with type='folder'."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
tree_view.ensure_path("folder1.folder2")
|
||||||
|
|
||||||
|
for node in tree_view._state.items.values():
|
||||||
|
assert node.type == "folder"
|
||||||
|
|
||||||
|
def test_i_cannot_ensure_path_with_empty_parts(self, root_instance):
|
||||||
|
"""Test that ensure_path raises ValueError for paths with empty parts."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid path"):
|
||||||
|
tree_view.ensure_path("folder1..folder2")
|
||||||
|
|
||||||
|
def test_i_cannot_ensure_path_with_only_spaces_parts(self, root_instance):
|
||||||
|
"""Test that ensure_path raises ValueError for path parts with only spaces."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Invalid path"):
|
||||||
|
tree_view.ensure_path("folder1. .folder2")
|
||||||
|
|
||||||
|
def test_ensure_path_returns_last_node_id(self, root_instance):
|
||||||
|
"""Test that ensure_path returns the ID of the last node in the path."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
# Create a path and get the returned ID
|
||||||
|
returned_id = tree_view.ensure_path("folder1.folder2.folder3")
|
||||||
|
|
||||||
|
# Verify the returned ID is not None
|
||||||
|
assert returned_id is not None
|
||||||
|
|
||||||
|
# Verify the returned ID corresponds to folder3
|
||||||
|
assert returned_id in tree_view._state.items
|
||||||
|
assert tree_view._state.items[returned_id].label == "folder3"
|
||||||
|
|
||||||
|
# Verify we can use this ID to add a child
|
||||||
|
leaf = TreeNode(label="file.txt", type="file")
|
||||||
|
tree_view.add_node(leaf, parent_id=returned_id)
|
||||||
|
|
||||||
|
assert leaf.parent == returned_id
|
||||||
|
assert leaf.id in tree_view._state.items[returned_id].children
|
||||||
|
|
||||||
|
def test_ensure_path_returns_existing_node_id(self, root_instance):
|
||||||
|
"""Test that ensure_path returns ID even when path already exists."""
|
||||||
|
tree_view = TreeView(root_instance)
|
||||||
|
|
||||||
|
# Create initial path
|
||||||
|
first_id = tree_view.ensure_path("folder1.folder2")
|
||||||
|
|
||||||
|
# Ensure same path again
|
||||||
|
second_id = tree_view.ensure_path("folder1.folder2")
|
||||||
|
|
||||||
|
# Should return the same ID
|
||||||
|
assert first_id == second_id
|
||||||
|
assert tree_view._state.items[first_id].label == "folder2"
|
||||||
|
|
||||||
|
|
||||||
class TestTreeViewRender:
|
class TestTreeViewRender:
|
||||||
@@ -810,7 +988,6 @@ class TestTreeViewRender:
|
|||||||
|
|
||||||
# Step 3: Compare
|
# Step 3: Compare
|
||||||
assert matches(keyboard, expected)
|
assert matches(keyboard, expected)
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_root_nodes_are_rendered(self, tree_view):
|
def test_multiple_root_nodes_are_rendered(self, tree_view):
|
||||||
"""Test that multiple root nodes are rendered at the same level.
|
"""Test that multiple root nodes are rendered at the same level.
|
||||||
@@ -822,18 +999,18 @@ class TestTreeViewRender:
|
|||||||
"""
|
"""
|
||||||
root1 = TreeNode(label="Root 1", type="folder")
|
root1 = TreeNode(label="Root 1", type="folder")
|
||||||
root2 = TreeNode(label="Root 2", type="folder")
|
root2 = TreeNode(label="Root 2", type="folder")
|
||||||
|
|
||||||
tree_view.add_node(root1)
|
tree_view.add_node(root1)
|
||||||
tree_view.add_node(root2)
|
tree_view.add_node(root2)
|
||||||
|
|
||||||
rendered = tree_view.render()
|
rendered = tree_view.render()
|
||||||
root_containers = find(rendered, Div(cls=Contains("mf-treenode-container")))
|
root_containers = find(rendered, Div(cls=Contains("mf-treenode-container")))
|
||||||
|
|
||||||
assert len(root_containers) == 2, "Should have two root-level containers"
|
assert len(root_containers) == 2, "Should have two root-level containers"
|
||||||
|
|
||||||
root1_container = find_one(rendered, Div(data_node_id=root1.id))
|
root1_container = find_one(rendered, Div(data_node_id=root1.id))
|
||||||
root2_container = find_one(rendered, Div(data_node_id=root2.id))
|
root2_container = find_one(rendered, Div(data_node_id=root2.id))
|
||||||
|
|
||||||
expected_root1 = Div(
|
expected_root1 = Div(
|
||||||
Div(
|
Div(
|
||||||
Div(None), # No icon, leaf node
|
Div(None), # No icon, leaf node
|
||||||
@@ -844,7 +1021,7 @@ class TestTreeViewRender:
|
|||||||
cls="mf-treenode-container",
|
cls="mf-treenode-container",
|
||||||
data_node_id=root1.id
|
data_node_id=root1.id
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_root2 = Div(
|
expected_root2 = Div(
|
||||||
Div(
|
Div(
|
||||||
Div(None), # No icon, leaf node
|
Div(None), # No icon, leaf node
|
||||||
@@ -855,6 +1032,6 @@ class TestTreeViewRender:
|
|||||||
cls="mf-treenode-container",
|
cls="mf-treenode-container",
|
||||||
data_node_id=root2.id
|
data_node_id=root2.id
|
||||||
)
|
)
|
||||||
|
|
||||||
assert matches(root1_container, expected_root1)
|
assert matches(root1_container, expected_root1)
|
||||||
assert matches(root2_container, expected_root2)
|
assert matches(root2_container, expected_root2)
|
||||||
|
|||||||
Reference in New Issue
Block a user