""" TreeView component for hierarchical data visualization with inline editing. This component provides an interactive tree structure with expand/collapse, selection, and inline editing capabilities. """ import uuid from dataclasses import dataclass, field from typing import Optional from fasthtml.components import Div, Input, Span from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.Keyboard import Keyboard from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.instances import MultipleInstance from myfasthtml.icons.fluent_p1 import chevron_right20_regular, edit20_regular from myfasthtml.icons.fluent_p2 import chevron_down20_regular, add_circle20_regular, delete20_regular @dataclass class TreeNode: """ Represents a node in the tree structure. Attributes: id: Unique identifier (auto-generated UUID if not provided) label: Display text for the node type: Node type for icon mapping parent: ID of parent node (None for root) children: List of child node IDs """ id: str = field(default_factory=lambda: str(uuid.uuid4())) label: str = "" type: str = "default" parent: Optional[str] = None children: list[str] = field(default_factory=list) class TreeViewState(DbObject): """ Persistent state for TreeView component. Attributes: items: Dictionary mapping node IDs to TreeNode instances opened: List of expanded node IDs selected: Currently selected node ID editing: Node ID currently being edited (None if not editing) icon_config: Mapping of node types to icon identifiers """ def __init__(self, owner): super().__init__(owner) with self.initializing(): self.items: dict[str, TreeNode] = {} self.opened: list[str] = [] self.selected: Optional[str] = None self.editing: Optional[str] = None self.icon_config: dict[str, str] = {} class Commands(BaseCommands): """Command handlers for TreeView actions.""" def toggle_node(self, node_id: str): """Create command to expand/collapse a node.""" return Command( "ToggleNode", f"Toggle node {node_id}", self._owner._toggle_node, node_id ).htmx(target=f"#{self._owner.get_id()}") def add_child(self, parent_id: str): """Create command to add a child node.""" return Command( "AddChild", f"Add child to {parent_id}", self._owner._add_child, parent_id ).htmx(target=f"#{self._owner.get_id()}") def add_sibling(self, node_id: str): """Create command to add a sibling node.""" return Command( "AddSibling", f"Add sibling to {node_id}", self._owner._add_sibling, node_id ).htmx(target=f"#{self._owner.get_id()}") def start_rename(self, node_id: str): """Create command to start renaming a node.""" return Command( "StartRename", f"Start renaming {node_id}", self._owner._start_rename, node_id ).htmx(target=f"#{self._owner.get_id()}") def save_rename(self, node_id: str): """Create command to save renamed node.""" return Command( "SaveRename", f"Save rename for {node_id}", self._owner._save_rename, node_id ).htmx(target=f"#{self._owner.get_id()}") def cancel_rename(self): """Create command to cancel renaming.""" return Command( "CancelRename", "Cancel rename", self._owner._cancel_rename ).htmx(target=f"#{self._owner.get_id()}") def delete_node(self, node_id: str): """Create command to delete a node.""" return Command( "DeleteNode", f"Delete node {node_id}", self._owner._delete_node, node_id ).htmx(target=f"#{self._owner.get_id()}") def select_node(self, node_id: str): """Create command to select a node.""" return Command( "SelectNode", f"Select node {node_id}", self._owner._select_node, node_id ).htmx(target=f"#{self._owner.get_id()}") class TreeView(MultipleInstance): """ Interactive TreeView component with hierarchical data visualization. Supports: - Expand/collapse nodes - Add child/sibling nodes - Inline rename - Delete nodes - Node selection """ def __init__(self, parent, items: Optional[dict] = None, _id: Optional[str] = None): """ Initialize TreeView component. Args: parent: Parent instance items: Optional initial items dictionary {node_id: TreeNode} _id: Optional custom ID """ super().__init__(parent, _id=_id) self._state = TreeViewState(self) self.commands = Commands(self) if items: self._state.items = items def set_icon_config(self, config: dict[str, str]): """ Set icon configuration for node types. Args: config: Dictionary mapping node types to icon identifiers Format: {type: "provider.icon_name"} """ self._state.icon_config = config def add_node(self, node: TreeNode, parent_id: Optional[str] = None, insert_index: Optional[int] = None): """ Add a node to the tree. Args: node: TreeNode instance to add parent_id: Optional parent node ID (None for root) insert_index: Optional index to insert at in parent's children list. If None, appends to end. If provided, inserts at that position. """ self._state.items[node.id] = node node.parent = parent_id if parent_id and parent_id in self._state.items: parent = self._state.items[parent_id] if node.id not in parent.children: if insert_index is not None: parent.children.insert(insert_index, node.id) else: parent.children.append(node.id) def expand_all(self): """Expand all nodes that have children.""" for node_id, node in self._state.items.items(): if node.children and node_id not in self._state.opened: self._state.opened.append(node_id) def _toggle_node(self, node_id: str): """Toggle expand/collapse state of a node.""" if node_id in self._state.opened: self._state.opened.remove(node_id) else: self._state.opened.append(node_id) return self def _add_child(self, parent_id: str, new_label: Optional[str] = None): """Add a child node to a parent.""" if parent_id not in self._state.items: raise ValueError(f"Parent node {parent_id} does not exist") parent = self._state.items[parent_id] new_node = TreeNode( label=new_label or "New Node", type=parent.type ) self.add_node(new_node, parent_id=parent_id) # Auto-expand parent if parent_id not in self._state.opened: self._state.opened.append(parent_id) return self def _add_sibling(self, node_id: str, new_label: Optional[str] = None): """Add a sibling node next to a node.""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") node = self._state.items[node_id] if node.parent is None: raise ValueError("Cannot add sibling to root node") parent = self._state.items[node.parent] new_node = TreeNode( label=new_label or "New Node", type=node.type ) # Insert after current node insert_idx = parent.children.index(node_id) + 1 self.add_node(new_node, parent_id=node.parent, insert_index=insert_idx) return self def _start_rename(self, node_id: str): """Start renaming a node (sets editing state).""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") self._state.editing = node_id return self def _save_rename(self, node_id: str, node_label: str): """Save renamed node with new label.""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") self._state.items[node_id].label = node_label self._state.editing = None return self def _cancel_rename(self): """Cancel renaming operation.""" self._state.editing = None return self def _delete_node(self, node_id: str): """Delete a node (only if it has no children).""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") node = self._state.items[node_id] if node.children: raise ValueError(f"Cannot delete node {node_id} with children") # Remove from parent's children list if node.parent and node.parent in self._state.items: parent = self._state.items[node.parent] parent.children.remove(node_id) # Remove from state del self._state.items[node_id] if node_id in self._state.opened: self._state.opened.remove(node_id) if self._state.selected == node_id: self._state.selected = None return self def _select_node(self, node_id: str): """Select a node.""" if node_id not in self._state.items: raise ValueError(f"Node {node_id} does not exist") self._state.selected = node_id return self def _render_action_buttons(self, node_id: str): """Render action buttons for a node (visible on hover).""" return Div( mk.icon(add_circle20_regular, command=self.commands.add_child(node_id)), mk.icon(edit20_regular, command=self.commands.start_rename(node_id)), mk.icon(delete20_regular, command=self.commands.delete_node(node_id)), cls="mf-treenode-actions" ) def _render_node(self, node_id: str, level: int = 0): """ Render a single node and its children recursively. Args: node_id: ID of node to render level: Indentation level Returns: Div containing the node and its children """ node = self._state.items[node_id] is_expanded = node_id in self._state.opened is_selected = node_id == self._state.selected is_editing = node_id == self._state.editing has_children = len(node.children) > 0 # Toggle icon toggle = mk.icon( chevron_down20_regular if is_expanded else chevron_right20_regular if has_children else " ", command=self.commands.toggle_node(node_id)) # Label or input for editing if is_editing: # TODO: Bind input to save_rename (Enter) and cancel_rename (Escape) label_element = mk.mk(Input( name="node_label", value=node.label, cls="mf-treenode-input input input-sm" ), command=self.commands.save_rename(node_id)) else: label_element = mk.mk( Span(node.label, cls="mf-treenode-label text-sm"), command=self.commands.select_node(node_id) ) # Node element node_element = Div( toggle, label_element, self._render_action_buttons(node_id), cls=f"mf-treenode flex {'selected' if is_selected and not is_editing else ''}", data_node_id=node_id, style=f"padding-left: {level * 20}px" ) # Children (if expanded) children_elements = [] if is_expanded and has_children: for child_id in node.children: children_elements.append( self._render_node(child_id, level + 1) ) return Div( node_element, *children_elements, cls="mf-treenode-container" ) def render(self): """ Render the complete TreeView. Returns: Div: Complete TreeView HTML structure """ # Find root nodes (nodes without parent) root_nodes = [ node_id for node_id, node in self._state.items.items() if node.parent is None ] return Div( *[self._render_node(node_id) for node_id in root_nodes], Keyboard(self, {"esc": self.commands.cancel_rename()}, _id="_keyboard"), id=self._id, cls="mf-treeview" ) def __ft__(self): """FastHTML magic method for rendering.""" return self.render()