diff --git a/client/src/components/admin/admin_panel_admin.rs b/client/src/components/admin/admin_panel_admin.rs index 26399f6..0328814 100644 --- a/client/src/components/admin/admin_panel_admin.rs +++ b/client/src/components/admin/admin_panel_admin.rs @@ -6,12 +6,11 @@ use crate::state::app::state::AppState; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::Style, - text::{Line, Span, Text}, // Added Text + text::{Line, Span, Text}, widgets::{Block, BorderType, Borders, List, ListItem, Paragraph}, Frame, }; -/// Renders the view specific to admin users with a three-pane layout. pub fn render_admin_panel_admin( f: &mut Frame, area: Rect, @@ -19,15 +18,13 @@ pub fn render_admin_panel_admin( admin_state: &mut AdminState, theme: &Theme, ) { - // Split vertically: Top for panes, Bottom for buttons let main_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(1)].as_ref()) // 1 line for buttons + .constraints([Constraint::Min(0), Constraint::Length(1)].as_ref()) .split(area); let panes_area = main_chunks[0]; let buttons_area = main_chunks[1]; - // Split the top area into three panes: Profiles | Tables | Dependencies let pane_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ @@ -35,207 +32,148 @@ pub fn render_admin_panel_admin( Constraint::Percentage(40), // Tables Constraint::Percentage(35), // Dependencies ].as_ref()) - .split(panes_area); // Use the whole area directly + .split(panes_area); let profiles_pane = pane_chunks[0]; let tables_pane = pane_chunks[1]; let deps_pane = pane_chunks[2]; // --- Profiles Pane (Left) --- - let profile_focus = admin_state.current_focus == AdminFocus::Profiles; - let profile_border_style = if profile_focus { + let profile_pane_has_focus = matches!(admin_state.current_focus, AdminFocus::ProfilesPane | AdminFocus::InsideProfilesList); + let profile_border_style = if profile_pane_has_focus { Style::default().fg(theme.highlight) } else { Style::default().fg(theme.border) }; - - // Block for the profiles pane - let profiles_block = Block::default() - .title(" Profiles ") - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(profile_border_style); - let profiles_inner_area = profiles_block.inner(profiles_pane); // Get inner area for list - f.render_widget(profiles_block, profiles_pane); // Render the block itself - - // Create profile list items - let profile_list_items: Vec = app_state - .profile_tree - .profiles - .iter() - .enumerate() - .map(|(idx, profile)| { - // Check persistent selection for prefix, navigation state for style/highlight - let is_selected = admin_state.selected_profile_index == Some(idx); - let prefix = if is_selected { "[*] " } else { "[ ] " }; - let style = if is_selected { // Style based on selection too - Style::default().fg(theme.highlight).add_modifier(ratatui::style::Modifier::BOLD) - } else { - Style::default().fg(theme.fg) - }; - ListItem::new(Line::from(vec![ - Span::styled(prefix, style), - Span::styled(&profile.name, style) - ])) - }) - .collect(); - - // Build and render profile list inside the block's inner area + let profiles_block = Block::default().title(" Profiles ").borders(Borders::ALL).border_type(BorderType::Rounded).border_style(profile_border_style); + let profiles_inner_area = profiles_block.inner(profiles_pane); + f.render_widget(profiles_block, profiles_pane); + let profile_list_items: Vec = app_state.profile_tree.profiles.iter().enumerate().map(|(idx, profile)| { + let is_persistently_selected = admin_state.selected_profile_index == Some(idx); + let is_nav_highlighted = admin_state.profile_list_state.selected() == Some(idx) && admin_state.current_focus == AdminFocus::InsideProfilesList; + let prefix = if is_persistently_selected { "[*] " } else { "[ ] " }; + let item_style = if is_nav_highlighted { Style::default().fg(theme.highlight).add_modifier(ratatui::style::Modifier::BOLD) } + else if is_persistently_selected { Style::default().fg(theme.accent) } + else { Style::default().fg(theme.fg) }; + ListItem::new(Line::from(vec![Span::styled(prefix, item_style), Span::styled(&profile.name, item_style)])) + }).collect(); let profile_list = List::new(profile_list_items) - // Highlight style depends only on Profiles focus - .highlight_style(if profile_focus { - Style::default().add_modifier(ratatui::style::Modifier::REVERSED) - } else { - Style::default() - }) - .highlight_symbol(if profile_focus { "> " } else { " " }); - + .highlight_style(if admin_state.current_focus == AdminFocus::InsideProfilesList { Style::default().add_modifier(ratatui::style::Modifier::REVERSED) } else { Style::default() }) + .highlight_symbol(if admin_state.current_focus == AdminFocus::InsideProfilesList { "> " } else { " " }); f.render_stateful_widget(profile_list, profiles_inner_area, &mut admin_state.profile_list_state); + // --- Tables Pane (Middle) --- let table_pane_has_focus = matches!(admin_state.current_focus, AdminFocus::Tables | AdminFocus::InsideTablesList); - let table_border_style = if table_pane_has_focus { - Style::default().fg(theme.highlight) + let table_border_style = if table_pane_has_focus { Style::default().fg(theme.highlight) } else { Style::default().fg(theme.border) }; + + let profile_to_display_tables_for_idx: Option; + if admin_state.current_focus == AdminFocus::InsideProfilesList { + profile_to_display_tables_for_idx = admin_state.profile_list_state.selected(); } else { - Style::default().fg(theme.border) - }; + profile_to_display_tables_for_idx = admin_state.selected_profile_index + .or_else(|| admin_state.profile_list_state.selected()); + } + let tables_pane_title_profile_name = profile_to_display_tables_for_idx + .and_then(|idx| app_state.profile_tree.profiles.get(idx)) + .map_or("None Selected", |p| p.name.as_str()); + let tables_block = Block::default().title(format!(" Tables (Profile: {}) ", tables_pane_title_profile_name)).borders(Borders::ALL).border_type(BorderType::Rounded).border_style(table_border_style); + let tables_inner_area = tables_block.inner(tables_pane); + f.render_widget(tables_block, tables_pane); - // Get selected profile information - let navigated_profile_idx = admin_state.profile_list_state.selected(); // Use nav state for display - let selected_profile_name = app_state - .profile_tree - .profiles - .get(navigated_profile_idx.unwrap_or(usize::MAX)) // Use nav state for title - .map_or("None", |p| &p.name); - - // Block for the tables pane - let tables_block = Block::default() - .title(format!(" Tables (Profile: {}) ", selected_profile_name)) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(table_border_style); - let tables_inner_area = tables_block.inner(tables_pane); // Get inner area for list - f.render_widget(tables_block, tables_pane); // Render the block itself - - // Create table list items and get dependencies for the selected table - let (table_list_items, selected_table_deps): (Vec, Vec) = if let Some( - profile, // Get profile based on NAVIGATED profile index - ) = navigated_profile_idx.and_then(|idx| app_state.profile_tree.profiles.get(idx)) { - let items: Vec = profile - .tables - .iter() - .enumerate() - .map(|(idx, table)| { // Renamed i to idx for clarity - // Check persistent selection for prefix, navigation state for style/highlight - let is_selected = admin_state.selected_table_index == Some(idx); // Use persistent state for [*] - let is_navigated = admin_state.table_list_state.selected() == Some(idx); // Use nav state for highlight/> - let prefix = if is_selected { "[*] " } else { "[ ] " }; - let style = if is_navigated { // Style based on navigation highlight - Style::default().fg(theme.highlight).add_modifier(ratatui::style::Modifier::BOLD) - } else { - Style::default().fg(theme.fg) - }; - ListItem::new(Line::from(vec![ - Span::styled(prefix, style), - Span::styled(&table.name, style), - ])) - }) - .collect(); - - // Get dependencies only for the PERSISTENTLY selected table in the PERSISTENTLY selected profile - let chosen_profile_idx = admin_state.selected_profile_index; // Use persistent profile selection - let deps = chosen_profile_idx // Start with the chosen profile index - .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) // Get the chosen profile - .and_then(|p| admin_state.selected_table_index.and_then(|t_idx| p.tables.get(t_idx))) // Get the chosen table using its index - .map_or(Vec::new(), |t| t.depends_on.clone()); // If found, clone its depends_on, otherwise return empty Vec - - (items, deps) - } else { - // Default when no profile is selected - (vec![ListItem::new("Select a profile to see tables")], vec![]) - }; - - // Build and render table list inside the block's inner area - let table_list = List::new(table_list_items) - // Highlight style only applies when focus is *inside* the list - .highlight_style(if admin_state.current_focus == AdminFocus::InsideTablesList { - Style::default().add_modifier(ratatui::style::Modifier::REVERSED) + let table_list_items_for_display: Vec = + if let Some(profile_data_for_tables) = profile_to_display_tables_for_idx + .and_then(|idx| app_state.profile_tree.profiles.get(idx)) { + profile_data_for_tables.tables.iter().enumerate().map(|(idx, table)| { + let is_table_persistently_selected = admin_state.selected_table_index == Some(idx) && + profile_to_display_tables_for_idx == admin_state.selected_profile_index; + let is_table_nav_highlighted = admin_state.table_list_state.selected() == Some(idx) && + admin_state.current_focus == AdminFocus::InsideTablesList; + let prefix = if is_table_persistently_selected { "[*] " } else { "[ ] " }; + let style = if is_table_nav_highlighted { Style::default().fg(theme.highlight).add_modifier(ratatui::style::Modifier::BOLD) } + else if is_table_persistently_selected { Style::default().fg(theme.accent) } + else { Style::default().fg(theme.fg) }; + ListItem::new(Line::from(vec![Span::styled(prefix, style), Span::styled(&table.name, style)])) + }).collect() } else { - Style::default() - }) - .highlight_symbol(if admin_state.current_focus == AdminFocus::InsideTablesList { "> " } else { "" }); - + vec![ListItem::new("Select a profile to see tables")] + }; + let table_list = List::new(table_list_items_for_display) + .highlight_style(if admin_state.current_focus == AdminFocus::InsideTablesList { Style::default().add_modifier(ratatui::style::Modifier::REVERSED) } else { Style::default() }) + .highlight_symbol(if admin_state.current_focus == AdminFocus::InsideTablesList { "> " } else { " " }); f.render_stateful_widget(table_list, tables_inner_area, &mut admin_state.table_list_state); - // --- Dependencies Pane (Right) --- - // Get name based on PERSISTENT selections - let chosen_profile_idx = admin_state.selected_profile_index; // Use persistent profile selection - let selected_table_name = chosen_profile_idx - .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) - .and_then(|p| admin_state.selected_table_index.and_then(|t_idx| p.tables.get(t_idx))) // Use persistent table selection - .map_or("N/A", |t| &t.name); // Get name of the selected table - // Block for the dependencies pane + // --- Dependencies Pane (Right) --- + let mut deps_pane_title_table_name = "N/A".to_string(); + let dependencies_to_display: Vec; + + if admin_state.current_focus == AdminFocus::InsideTablesList { + // If navigating tables, show dependencies for the '>' highlighted table. + // The profile context is `profile_to_display_tables_for_idx` (from Tables pane logic). + if let Some(p_idx_for_current_tables) = profile_to_display_tables_for_idx { + if let Some(current_profile_showing_tables) = app_state.profile_tree.profiles.get(p_idx_for_current_tables) { + if let Some(table_nav_idx) = admin_state.table_list_state.selected() { // The '>' highlighted table + if let Some(navigated_table) = current_profile_showing_tables.tables.get(table_nav_idx) { + deps_pane_title_table_name = navigated_table.name.clone(); + dependencies_to_display = navigated_table.depends_on.clone(); + } else { + dependencies_to_display = Vec::new(); // Navigated table index out of bounds + } + } else { + dependencies_to_display = Vec::new(); // No table navigated with '>' + } + } else { + dependencies_to_display = Vec::new(); // Profile for tables out of bounds + } + } else { + dependencies_to_display = Vec::new(); // No profile active for table display + } + } else { + // Otherwise, show dependencies for the '[*]' persistently selected table & profile. + if let Some(p_idx) = admin_state.selected_profile_index { // Must be a persistently selected profile + if let Some(selected_profile) = app_state.profile_tree.profiles.get(p_idx) { + if let Some(t_idx) = admin_state.selected_table_index { // Must be a persistently selected table + if let Some(selected_table) = selected_profile.tables.get(t_idx) { + deps_pane_title_table_name = selected_table.name.clone(); + dependencies_to_display = selected_table.depends_on.clone(); + } else { dependencies_to_display = Vec::new(); } + } else { dependencies_to_display = Vec::new(); } + } else { dependencies_to_display = Vec::new(); } + } else { dependencies_to_display = Vec::new(); } + } + let deps_block = Block::default() - .title(format!(" Dependencies (Table: {}) ", selected_table_name)) + .title(format!(" Dependencies (Table: {}) ", deps_pane_title_table_name)) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(theme.border)); // No focus highlight for deps pane - let deps_inner_area = deps_block.inner(deps_pane); // Get inner area for content - f.render_widget(deps_block, deps_pane); // Render the block itself + .border_style(Style::default().fg(theme.border)); + let deps_inner_area = deps_block.inner(deps_pane); + f.render_widget(deps_block, deps_pane); - // Prepare content for the dependencies paragraph let mut deps_content = Text::default(); deps_content.lines.push(Line::from(Span::styled( "Depends On:", - Style::default().fg(theme.accent), // Use accent color for the label + Style::default().fg(theme.accent), ))); - if !selected_table_deps.is_empty() { - for dep in selected_table_deps { - // List each dependency + if !dependencies_to_display.is_empty() { + for dep in dependencies_to_display { deps_content.lines.push(Line::from(Span::styled(format!("- {}", dep), theme.fg))); } } else { - // Indicate if there are no dependencies deps_content.lines.push(Line::from(Span::styled(" None", theme.secondary))); } - - // Build and render dependencies paragraph inside the block's inner area let deps_paragraph = Paragraph::new(deps_content); f.render_widget(deps_paragraph, deps_inner_area); // --- Buttons Row --- - let button_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(33), - Constraint::Percentage(34), - Constraint::Percentage(33), - ].as_ref()) - .split(buttons_area); - + let button_chunks = Layout::default().direction(Direction::Horizontal).constraints([Constraint::Percentage(33), Constraint::Percentage(34), Constraint::Percentage(33)].as_ref()).split(buttons_area); let btn_base_style = Style::default().fg(theme.secondary); - - // Define the helper closure to get style based on focus - let get_btn_style = |button_focus: AdminFocus| { - if admin_state.current_focus == button_focus { - // Apply highlight style if this button is focused - btn_base_style.add_modifier(ratatui::style::Modifier::REVERSED) - } else { - btn_base_style // Use base style otherwise - } - }; - let btn1 = Paragraph::new("Add Logic") - .style(get_btn_style(AdminFocus::Button1)) - .alignment(Alignment::Center); - let btn2 = Paragraph::new("Add Table") - .style(get_btn_style(AdminFocus::Button2)) - .alignment(Alignment::Center); - let btn3 = Paragraph::new("Change Table") - .style(get_btn_style(AdminFocus::Button3)) - .alignment(Alignment::Center); - + let get_btn_style = |button_focus: AdminFocus| { if admin_state.current_focus == button_focus { btn_base_style.add_modifier(ratatui::style::Modifier::REVERSED) } else { btn_base_style } }; + let btn1 = Paragraph::new("Add Logic").style(get_btn_style(AdminFocus::Button1)).alignment(Alignment::Center); + let btn2 = Paragraph::new("Add Table").style(get_btn_style(AdminFocus::Button2)).alignment(Alignment::Center); + let btn3 = Paragraph::new("Change Table").style(get_btn_style(AdminFocus::Button3)).alignment(Alignment::Center); f.render_widget(btn1, button_chunks[0]); f.render_widget(btn2, button_chunks[1]); f.render_widget(btn3, button_chunks[2]); diff --git a/client/src/functions/modes/navigation/admin_nav.rs b/client/src/functions/modes/navigation/admin_nav.rs index 9b7cc5f..ad00f7a 100644 --- a/client/src/functions/modes/navigation/admin_nav.rs +++ b/client/src/functions/modes/navigation/admin_nav.rs @@ -1,27 +1,20 @@ // src/functions/modes/navigation/admin_nav.rs - +use crate::state::pages::admin::{AdminFocus, AdminState}; +use crate::state::app::state::AppState; use crate::config::binds::config::Config; -use crate::state::{ - app::state::AppState, - pages::admin::{AdminFocus, AdminState}, -}; -use crossterm::event::KeyEvent; -use crate::state::app::buffer::AppView; -use crate::state::app::buffer::BufferState; +use crate::state::app::buffer::{BufferState, AppView}; use crate::state::pages::add_table::{AddTableState, LinkDefinition}; -use crate::state::pages::add_logic::AddLogicState; use ratatui::widgets::ListState; +use crate::state::pages::add_logic::AddLogicState; -// --- Helper functions for ListState navigation (similar to TableState) --- +// Helper functions list_select_next and list_select_previous remain the same fn list_select_next(list_state: &mut ListState, item_count: usize) { if item_count == 0 { list_state.select(None); return; } let i = match list_state.selected() { - Some(i) => { - if i >= item_count - 1 { 0 } else { i + 1 } - } + Some(i) => if i >= item_count - 1 { 0 } else { i + 1 }, None => 0, }; list_state.select(Some(i)); @@ -33,252 +26,313 @@ fn list_select_previous(list_state: &mut ListState, item_count: usize) { return; } let i = match list_state.selected() { - Some(i) => { - if i == 0 { item_count - 1 } else { i - 1 } - } - None => item_count - 1, // Select last if nothing was selected + Some(i) => if i == 0 { item_count - 1 } else { i - 1 }, + None => if item_count > 0 { item_count - 1 } else { 0 }, }; list_state.select(Some(i)); } - -/// Handles navigation events specifically for the Admin Panel view. -/// Returns true if the event was handled, false otherwise. pub fn handle_admin_navigation( - key: KeyEvent, + key: crossterm::event::KeyEvent, config: &Config, app_state: &mut AppState, admin_state: &mut AdminState, buffer_state: &mut BufferState, command_message: &mut String, ) -> bool { - let action = config.get_general_action(key.code, key.modifiers).map(String::from); // Clone action string + let action = config.get_general_action(key.code, key.modifiers).map(String::from); let current_focus = admin_state.current_focus; let profile_count = app_state.profile_tree.profiles.len(); - let mut new_focus = current_focus; // Start with current focus - let mut handled = true; // Assume handled unless logic says otherwise + let mut handled = false; - match action.as_deref() { - // --- Vertical Navigation (Up/Down) --- - Some("move_up") => { - match current_focus { - AdminFocus::Profiles => { - if profile_count > 0 { - admin_state.previous_profile(profile_count); - *command_message = "Navigated profiles list".to_string(); - } - } - AdminFocus::Tables => { - *command_message = "Press Enter to select and scroll tables".to_string(); - } - AdminFocus::InsideTablesList => { - if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { - if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { - list_select_previous(&mut admin_state.table_list_state, profile.tables.len()); + match current_focus { + AdminFocus::ProfilesPane => { + match action.as_deref() { + Some("select") => { + admin_state.current_focus = AdminFocus::InsideProfilesList; + if !app_state.profile_tree.profiles.is_empty() { + if admin_state.profile_list_state.selected().is_none() { + admin_state.profile_list_state.select(Some(0)); } } + *command_message = "Navigating profiles. Use Up/Down. Esc to exit.".to_string(); + handled = true; } - AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3 => {} + Some("next_option") | Some("move_down") => { // move_down acts as next_option + admin_state.current_focus = AdminFocus::Tables; + *command_message = "Focus: Tables Pane".to_string(); + handled = true; + } + Some("previous_option") | Some("move_up") => { // move_up acts as previous_option + admin_state.current_focus = AdminFocus::Button3; // Cycle to last button + *command_message = "Focus: Button 3".to_string(); + handled = true; + } + // If move_up/move_down were not explicitly for pane nav, + // they would be caught here if not handled above. + // But since we want them for pane nav, this is fine. + _ => handled = false, } } - Some("move_down") => { - match current_focus { - AdminFocus::Profiles => { + + AdminFocus::InsideProfilesList => { + match action.as_deref() { + Some("move_up") => { // Only item navigation here if profile_count > 0 { - admin_state.next_profile(profile_count); - *command_message = "Navigated profiles list".to_string(); + list_select_previous(&mut admin_state.profile_list_state, profile_count); + *command_message = "".to_string(); + handled = true; } } - AdminFocus::Tables => { - *command_message = "Press Enter to select and scroll tables".to_string(); - } - AdminFocus::InsideTablesList => { - if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { - if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { - list_select_next(&mut admin_state.table_list_state, profile.tables.len()); - } + Some("move_down") => { // Only item navigation here + if profile_count > 0 { + list_select_next(&mut admin_state.profile_list_state, profile_count); + *command_message = "".to_string(); + handled = true; } } - AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3 => {} - } - } - // --- Horizontal Navigation (Focus Change) --- - Some("next_option") | Some("previous_option") => { - let old_focus = admin_state.current_focus; - let is_next = action.as_deref() == Some("next_option"); - - admin_state.current_focus = match old_focus { - AdminFocus::Profiles => if is_next { AdminFocus::Tables } else { AdminFocus::Button3 }, - AdminFocus::Tables => if is_next { AdminFocus::Button1 } else { AdminFocus::Profiles }, - AdminFocus::Button1 => if is_next { AdminFocus::Button2 } else { AdminFocus::Tables }, - AdminFocus::Button2 => if is_next { AdminFocus::Button3 } else { AdminFocus::Button1 }, - AdminFocus::Button3 => if is_next { AdminFocus::Profiles } else { AdminFocus::Button2 }, - AdminFocus::InsideTablesList => old_focus, - }; - - new_focus = admin_state.current_focus; // Update new_focus after changing admin_state.current_focus - *command_message = format!("Focus set to {:?}", new_focus); - if old_focus == AdminFocus::Profiles && new_focus == AdminFocus::Tables && is_next { - if let Some(profile_idx) = admin_state.profile_list_state.selected() { - if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) { - if !profile.tables.is_empty() { - admin_state.table_list_state.select(Some(0)); - } else { - admin_state.table_list_state.select(None); - } - } else { - admin_state.table_list_state.select(None); - } - } else { - admin_state.table_list_state.select(None); - } - } - if old_focus == AdminFocus::Tables && new_focus != AdminFocus::Tables && old_focus != AdminFocus::InsideTablesList { - admin_state.table_list_state.select(None); - } - // No change needed for profile_list_state clearing here based on current logic - } - // --- Selection --- - Some("select") => { - match current_focus { - AdminFocus::Profiles => { - if let Some(nav_idx) = admin_state.profile_list_state.selected() { - admin_state.selected_profile_index = Some(nav_idx); - new_focus = AdminFocus::Tables; - admin_state.table_list_state.select(None); - admin_state.selected_table_index = None; - if let Some(profile) = app_state.profile_tree.profiles.get(nav_idx) { + Some("select") => { + admin_state.selected_profile_index = admin_state.profile_list_state.selected(); + admin_state.selected_table_index = None; + if let Some(profile_idx) = admin_state.selected_profile_index { + if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) { if !profile.tables.is_empty() { admin_state.table_list_state.select(Some(0)); - } - *command_message = format!("Selected profile: {}", app_state.profile_tree.profiles[nav_idx].name); - } - } else { - *command_message = "No profile selected".to_string(); - } - } - AdminFocus::Tables => { - new_focus = AdminFocus::InsideTablesList; - if let Some(p_idx) = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) { - if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { - if admin_state.table_list_state.selected().is_none() && !profile.tables.is_empty() { - admin_state.table_list_state.select(Some(0)); + } else { + admin_state.table_list_state.select(None); } } - } - *command_message = "Entered Tables List (Select item with Enter, Exit with Esc)".to_string(); - } - AdminFocus::InsideTablesList => { - if let Some(nav_idx) = admin_state.table_list_state.selected() { - admin_state.selected_table_index = Some(nav_idx); - let table_name = admin_state.profile_list_state.selected().or(admin_state.selected_profile_index) - .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) - .and_then(|p| p.tables.get(nav_idx).map(|t| t.name.clone())) - .unwrap_or_else(|| "N/A".to_string()); - *command_message = format!("Selected table: {}", table_name); - } else { - *command_message = "No table highlighted".to_string(); - } - } - - // In src/functions/modes/navigation/admin_nav.rs - AdminFocus::Button1 => { // Add Logic - let mut logic_state_profile_name = "None (Global)".to_string(); - let mut selected_table_id: Option = None; - let mut selected_table_name_for_logic: Option = None; - if let Some(p_idx) = admin_state.selected_profile_index { - if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { - logic_state_profile_name = profile.name.clone(); - // Check for persistently selected table within this profile - if let Some(t_idx) = admin_state.selected_table_index { - if let Some(table) = profile.tables.get(t_idx) { - selected_table_id = None; - selected_table_name_for_logic = Some(table.name.clone()); - *command_message = format!("Adding logic for table: {}. CRITICAL: Table ID not found in profile tree response!", table.name); - } else { - *command_message = format!("Selected table index {} out of bounds for profile '{}'. Logic will not be table-specific.", t_idx, profile.name); - }} else { - *command_message = format!("No table selected in profile '{}'. Logic will not be table-specific.", profile.name); - } - } else { - *command_message = "Error: Selected profile index out of bounds, associating with 'None'.".to_string(); - } } else { - *command_message = "No profile selected ([*]), associating Logic with 'None (Global)'.".to_string(); - // Keep logic_state_profile_name as "None (Global)" + admin_state.table_list_state.select(None); } - - // Create AddLogicState with the loaded config - admin_state.add_logic_state = AddLogicState { - profile_name: logic_state_profile_name.clone(), - editor_keybinding_mode: config.editor.keybinding_mode.clone(), // Add this line - ..AddLogicState::default() - }; - - buffer_state.update_history(AppView::AddLogic); - app_state.ui.focus_outside_canvas = false; - // Rest of the code remains the same... + *command_message = format!( + "Profile '{}' set as active.", + admin_state.get_selected_profile_name().unwrap_or(&"N/A".to_string()) + ); + handled = true; } - AdminFocus::Button2 => { - if let Some(p_idx) = admin_state.selected_profile_index { - if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { - let selected_profile_name = profile.name.clone(); - let available_links: Vec = profile - .tables - .iter() - .map(|table| LinkDefinition { - linked_table_name: table.name.clone(), - is_required: false, - selected: false, - }) - .collect(); - - let new_add_table_state = AddTableState { - profile_name: selected_profile_name, - links: available_links, - ..AddTableState::default() - }; - admin_state.add_table_state = new_add_table_state; - buffer_state.update_history(AppView::AddTable); - app_state.ui.focus_outside_canvas = false; - *command_message = format!( - "Navigating to Add Table for profile '{}'...", - admin_state.add_table_state.profile_name - ); - } else { - *command_message = "Error: Selected profile index out of bounds.".to_string(); - } - } else { - *command_message = "Please select a profile ([*]) first.".to_string(); - } - } - AdminFocus::Button3 => { - *command_message = "Action: Change Table (Not Implemented)".to_string(); + Some("exit_table_scroll") => { // 'esc' + admin_state.current_focus = AdminFocus::ProfilesPane; + *command_message = "Focus: Profiles Pane".to_string(); + handled = true; } + // next_option & previous_option are NOT handled here to enforce 'esc' first + _ => handled = false, } } - Some("exit_table_scroll") => { - match current_focus { - AdminFocus::InsideTablesList => { - new_focus = AdminFocus::Tables; - admin_state.table_list_state.select(None); - *command_message = "Exited Tables List".to_string(); + + AdminFocus::Tables => { + match action.as_deref() { + Some("select") => { + admin_state.current_focus = AdminFocus::InsideTablesList; + let current_profile_idx = admin_state.selected_profile_index + .or_else(|| admin_state.profile_list_state.selected()); + if let Some(profile_idx) = current_profile_idx { + if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) { + if !profile.tables.is_empty() { + if admin_state.table_list_state.selected().is_none() { + admin_state.table_list_state.select(Some(0)); + } + } else { + admin_state.table_list_state.select(None); + } + } else { + admin_state.table_list_state.select(None); + } + } else { + admin_state.table_list_state.select(None); + *command_message = "Select a profile first to view its tables.".to_string(); + } + if admin_state.current_focus == AdminFocus::InsideTablesList && !admin_state.table_list_state.selected().is_none() { + *command_message = "Navigating tables. Use Up/Down. Esc to exit.".to_string(); + } else if admin_state.table_list_state.selected().is_none() { + // If no tables, or no profile, don't enter InsideTablesList or revert + if current_profile_idx.is_none() { + *command_message = "No profile selected to view tables.".to_string(); + } else { + *command_message = "No tables in selected profile.".to_string(); + } + admin_state.current_focus = AdminFocus::Tables; // Stay on Tables pane + } + handled = true; + } + Some("previous_option") | Some("move_up") => { // move_up acts as previous_option + admin_state.current_focus = AdminFocus::ProfilesPane; + *command_message = "Focus: Profiles Pane".to_string(); + handled = true; + } + Some("next_option") | Some("move_down") => { // move_down acts as next_option + admin_state.current_focus = AdminFocus::Button1; + *command_message = "Focus: Add Logic Button".to_string(); + handled = true; } _ => handled = false, } } - Some("toggle_sidebar") | Some("toggle_buffer_list") | Some("next_field") | Some("prev_field") => { - handled = false; - } - _ => handled = false, - } - if handled && admin_state.current_focus != new_focus { // Check admin_state.current_focus - admin_state.current_focus = new_focus; - if command_message.is_empty() || command_message.starts_with("Focus set to") { - *command_message = format!("Focus set to {:?}", admin_state.current_focus); + AdminFocus::InsideTablesList => { + match action.as_deref() { + Some("move_up") => { // Only item navigation + let current_profile_idx = admin_state.selected_profile_index + .or_else(|| admin_state.profile_list_state.selected()); + if let Some(p_idx) = current_profile_idx { + if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { + if !profile.tables.is_empty() { + list_select_previous(&mut admin_state.table_list_state, profile.tables.len()); + *command_message = "".to_string(); + handled = true; + } else { + *command_message = "No tables to navigate.".to_string(); + handled = true; + } + } + } else { + *command_message = "No active profile for tables.".to_string(); + handled = true; + } + } + Some("move_down") => { // Only item navigation + let current_profile_idx = admin_state.selected_profile_index + .or_else(|| admin_state.profile_list_state.selected()); + if let Some(p_idx) = current_profile_idx { + if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { + if !profile.tables.is_empty() { + list_select_next(&mut admin_state.table_list_state, profile.tables.len()); + *command_message = "".to_string(); + handled = true; + } else { + *command_message = "No tables to navigate.".to_string(); + handled = true; + } + } + } else { + *command_message = "No active profile for tables.".to_string(); + handled = true; + } + } + Some("select") => { + admin_state.selected_table_index = admin_state.table_list_state.selected(); + let table_name = admin_state.selected_profile_index + .or_else(|| admin_state.profile_list_state.selected()) + .and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx)) + .and_then(|p| admin_state.selected_table_index.and_then(|t_idx| p.tables.get(t_idx))) + .map_or("N/A", |t| t.name.as_str()); + *command_message = format!("Table '{}' set as active.", table_name); + handled = true; + } + Some("exit_table_scroll") => { // 'esc' + admin_state.current_focus = AdminFocus::Tables; + *command_message = "Focus: Tables Pane".to_string(); + handled = true; + } + // next_option & previous_option are NOT handled here + _ => handled = false, + } + } + + // --- Button Navigation --- + // move_up/move_down on buttons will cycle through them or to adjacent panes (Tables/Profiles) + AdminFocus::Button1 => { + match action.as_deref() { + Some("select") => { /* ... existing select logic ... */ + let mut logic_state_profile_name = "None (Global)".to_string(); + let mut selected_table_name_for_logic: Option = None; + if let Some(p_idx) = admin_state.selected_profile_index { + if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { + logic_state_profile_name = profile.name.clone(); + if let Some(t_idx) = admin_state.selected_table_index { + if let Some(table) = profile.tables.get(t_idx) { + selected_table_name_for_logic = Some(table.name.clone()); + } + } + } + } + admin_state.add_logic_state = AddLogicState { + profile_name: logic_state_profile_name.clone(), + selected_table_name: selected_table_name_for_logic, + editor_keybinding_mode: config.editor.keybinding_mode.clone(), + ..AddLogicState::default() + }; + buffer_state.update_history(AppView::AddLogic); + app_state.ui.focus_outside_canvas = false; + *command_message = "Opening Add Logic...".to_string(); + handled = true; + } + Some("previous_option") | Some("move_up") => { // move_up from Button1 goes to Tables Pane + admin_state.current_focus = AdminFocus::Tables; + *command_message = "Focus: Tables Pane".to_string(); + handled = true; + } + Some("next_option") | Some("move_down") => { // move_down from Button1 goes to Button2 + admin_state.current_focus = AdminFocus::Button2; + *command_message = "Focus: Add Table Button".to_string(); + handled = true; + } + _ => handled = false, + } + } + + AdminFocus::Button2 => { + match action.as_deref() { + Some("select") => { /* ... existing select logic ... */ + if let Some(p_idx) = admin_state.selected_profile_index { + if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) { + let selected_profile_name = profile.name.clone(); + let available_links: Vec = profile.tables.iter() + .map(|table| LinkDefinition { + linked_table_name: table.name.clone(), + is_required: false, selected: false, + }).collect(); + admin_state.add_table_state = AddTableState { + profile_name: selected_profile_name, links: available_links, + ..AddTableState::default() + }; + buffer_state.update_history(AppView::AddTable); + app_state.ui.focus_outside_canvas = false; + *command_message = format!("Opening Add Table for profile '{}'...", admin_state.add_table_state.profile_name); + handled = true; + } else { + *command_message = "Error: Selected profile index out of bounds.".to_string(); + handled = true; + } + } else { + *command_message = "Please select a profile ([*]) first to add a table.".to_string(); + handled = true; + } + } + Some("previous_option") | Some("move_up") => { // move_up from Button2 goes to Button1 + admin_state.current_focus = AdminFocus::Button1; + *command_message = "Focus: Add Logic Button".to_string(); + handled = true; + } + Some("next_option") | Some("move_down") => { // move_down from Button2 goes to Button3 + admin_state.current_focus = AdminFocus::Button3; + *command_message = "Focus: Change Table Button".to_string(); + handled = true; + } + _ => handled = false, + } + } + + AdminFocus::Button3 => { + match action.as_deref() { + Some("select") => { + *command_message = "Action: Change Table (Not Implemented)".to_string(); + handled = true; + } + Some("previous_option") | Some("move_up") => { // move_up from Button3 goes to Button2 + admin_state.current_focus = AdminFocus::Button2; + *command_message = "Focus: Add Table Button".to_string(); + handled = true; + } + Some("next_option") | Some("move_down") => { // move_down from Button3 goes to Profiles Pane + admin_state.current_focus = AdminFocus::ProfilesPane; + *command_message = "Focus: Profiles Pane".to_string(); + handled = true; + } + _ => handled = false, + } } } - handled } diff --git a/client/src/state/pages/admin.rs b/client/src/state/pages/admin.rs index b8d6888..2b18ba8 100644 --- a/client/src/state/pages/admin.rs +++ b/client/src/state/pages/admin.rs @@ -8,7 +8,8 @@ use crate::state::pages::add_logic::AddLogicState; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum AdminFocus { #[default] // Default focus is on the profiles list - Profiles, + ProfilesPane, + InsideProfilesList, Tables, InsideTablesList, Button1, diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 7ba4738..9172513 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -13,6 +13,7 @@ use crate::state::pages::auth::AuthState; use crate::state::pages::auth::LoginState; use crate::state::pages::auth::RegisterState; use crate::state::pages::admin::AdminState; +use crate::state::pages::admin::AdminFocus; use crate::state::pages::intro::IntroState; use crate::state::app::buffer::BufferState; use crate::state::app::buffer::AppView; @@ -108,11 +109,24 @@ pub async fn run_ui() -> Result<()> { event_handler.command_message = format!("Error refreshing admin data: {}", e); } } - app_state.ui.show_admin = true; - let profile_names = app_state.profile_tree.profiles.iter() - .map(|p| p.name.clone()) - .collect(); + app_state.ui.show_admin = true; // <<< RESTORE THIS + let profile_names = app_state.profile_tree.profiles.iter() // <<< RESTORE THIS + .map(|p| p.name.clone()) // <<< RESTORE THIS + .collect(); // <<< RESTORE THIS admin_state.set_profiles(profile_names); + + // Only reset to ProfilesPane if not already in a specific admin sub-focus + if admin_state.current_focus == AdminFocus::default() || + !matches!(admin_state.current_focus, + AdminFocus::InsideProfilesList | + AdminFocus::Tables | AdminFocus::InsideTablesList | + AdminFocus::Button1 | AdminFocus::Button2 | AdminFocus::Button3) { + admin_state.current_focus = AdminFocus::ProfilesPane; + } + // Pre-select first profile item for visual consistency, but '>' won't show until 'select' + if admin_state.profile_list_state.selected().is_none() && !app_state.profile_tree.profiles.is_empty() { + admin_state.profile_list_state.select(Some(0)); + } } AppView::AddTable => app_state.ui.show_add_table = true, AppView::AddLogic => app_state.ui.show_add_logic = true,