diff --git a/client/src/functions/modes/navigation/admin_nav.rs b/client/src/functions/modes/navigation/admin_nav.rs index e4fec81..12814d5 100644 --- a/client/src/functions/modes/navigation/admin_nav.rs +++ b/client/src/functions/modes/navigation/admin_nav.rs @@ -234,31 +234,34 @@ pub fn handle_admin_navigation( admin_state.add_logic_state = AddLogicState { profile_name: profile.name.clone(), selected_table_name: Some(table.name.clone()), - // selected_table_id: table.id, // If you have table IDs + selected_table_id: Some(table.id), // If you have table IDs editor_keybinding_mode: config.editor.keybinding_mode.clone(), - current_focus: AddLogicFocus::default(), // Reset focus for the new screen + current_focus: AddLogicFocus::default(), ..AddLogicState::default() }; - buffer_state.update_history(AppView::AddLogic); // Switch view - app_state.ui.focus_outside_canvas = false; // Ensure canvas focus + + // Store table info for later fetching + app_state.pending_table_structure_fetch = Some(( + profile.name.clone(), + table.name.clone() + )); + + buffer_state.update_history(AppView::AddLogic); + app_state.ui.focus_outside_canvas = false; *command_message = format!( "Opening Add Logic for table '{}' in profile '{}'...", table.name, profile.name ); } else { - // This case should ideally not be reached if indices are managed correctly *command_message = "Error: Selected table data not found.".to_string(); } } else { - // Profile is selected, but table is not *command_message = "Select a table first!".to_string(); } } else { - // This case should ideally not be reached if p_idx is valid *command_message = "Error: Selected profile data not found.".to_string(); } } else { - // Profile is not selected *command_message = "Select a profile first!".to_string(); } handled = true; diff --git a/client/src/services/ui_service.rs b/client/src/services/ui_service.rs index 6c248e4..befffc9 100644 --- a/client/src/services/ui_service.rs +++ b/client/src/services/ui_service.rs @@ -3,6 +3,7 @@ use crate::services::grpc_client::GrpcClient; use crate::state::pages::form::FormState; use crate::tui::functions::common::form::SaveOutcome; +use crate::state::pages::add_logic::AddLogicState; use crate::state::app::state::AppState; use anyhow::{Context, Result}; @@ -37,6 +38,49 @@ impl UiService { Ok(column_names) } + pub async fn initialize_add_logic_table_data( + grpc_client: &mut GrpcClient, + add_logic_state: &mut AddLogicState, + ) -> Result { + let profile_name_clone_opt = Some(add_logic_state.profile_name.clone()); + let table_name_opt_clone = add_logic_state.selected_table_name.clone(); + + if let (Some(profile_name_clone), Some(table_name_clone)) = (profile_name_clone_opt, table_name_opt_clone) { + match grpc_client.get_table_structure(profile_name_clone.clone(), table_name_clone.clone()).await { + Ok(response) => { + let column_names: Vec = response.columns + .into_iter() + .map(|col| col.name) + .collect(); + + // This mutable borrow is now fine + add_logic_state.set_table_columns(column_names.clone()); + + Ok(format!( + "Loaded {} columns for table '{}' in profile '{}'", + column_names.len(), + table_name_clone, // Use cloned value + profile_name_clone // Use cloned value + )) + } + Err(e) => { + tracing::warn!( + "Failed to fetch table structure for {}.{}: {}", + profile_name_clone, // Use cloned value + table_name_clone, // Use cloned value + e + ); + Ok(format!( + "Warning: Could not load table structure for '{}'. Autocomplete will use basic suggestions.", + table_name_clone // Use cloned value + )) + } + } + } else { + Ok("No table selected or profile name missing for Add Logic".to_string()) + } + } + pub async fn initialize_adresar_count( grpc_client: &mut GrpcClient, app_state: &mut AppState, diff --git a/client/src/state/app/state.rs b/client/src/state/app/state.rs index 95cafcc..4f891fd 100644 --- a/client/src/state/app/state.rs +++ b/client/src/state/app/state.rs @@ -39,6 +39,7 @@ pub struct AppState { pub selected_profile: Option, pub current_mode: AppMode, pub focused_button_index: usize, + pub pending_table_structure_fetch: Option<(String, String)>, // UI preferences pub ui: UiState, @@ -57,6 +58,7 @@ impl AppState { selected_profile: None, current_mode: AppMode::General, focused_button_index: 0, + pending_table_structure_fetch: None, ui: UiState::default(), }) } diff --git a/client/src/state/pages/add_logic.rs b/client/src/state/pages/add_logic.rs index 8bd4539..eed8b45 100644 --- a/client/src/state/pages/add_logic.rs +++ b/client/src/state/pages/add_logic.rs @@ -131,22 +131,26 @@ impl AddLogicState { /// Updates script editor suggestions based on current filter text pub fn update_script_editor_suggestions(&mut self) { - let hardcoded_suggestions = vec![ - "sql".to_string(), - "tablename".to_string(), - "table column".to_string() - ]; + let mut suggestions = vec!["sql".to_string()]; + // Add actual table name if available + if let Some(ref table_name) = self.selected_table_name { + suggestions.push(table_name.clone()); + } + + // Add column names from the table + suggestions.extend(self.table_columns_for_suggestions.clone()); + if self.script_editor_filter_text.is_empty() { - self.script_editor_suggestions = hardcoded_suggestions; + self.script_editor_suggestions = suggestions; } else { let filter_lower = self.script_editor_filter_text.to_lowercase(); - self.script_editor_suggestions = hardcoded_suggestions + self.script_editor_suggestions = suggestions .into_iter() .filter(|suggestion| suggestion.to_lowercase().contains(&filter_lower)) .collect(); } - + // Update selection index if self.script_editor_suggestions.is_empty() { self.script_editor_selected_suggestion_index = None; @@ -160,6 +164,15 @@ impl AddLogicState { } } + /// Sets table columns for autocomplete suggestions + pub fn set_table_columns(&mut self, columns: Vec) { + self.table_columns_for_suggestions = columns.clone(); + // Also update target column suggestions for the input field + if !columns.is_empty() { + self.update_target_column_suggestions(); + } + } + /// Deactivates script editor autocomplete and clears related state pub fn deactivate_script_editor_autocomplete(&mut self) { self.script_editor_autocomplete_active = false; diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index ccfeaee..38057c2 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -23,16 +23,16 @@ use crate::tui::terminal::{EventReader, TerminalCore}; use crate::ui::handlers::render::render_ui; use crate::tui::functions::common::login::LoginResult; use crate::tui::functions::common::register::RegisterResult; -use crate::tui::functions::common::add_table::handle_save_table_action; -use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; -use crate::ui::handlers::context::{DialogPurpose, UiContext}; +// Removed: use crate::tui::functions::common::add_table::handle_save_table_action; +// Removed: use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; +use crate::ui::handlers::context::DialogPurpose; // UiContext removed if not used directly use crate::tui::functions::common::login; use crate::tui::functions::common::register; use std::time::Instant; use anyhow::{Context, Result}; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; -use tracing::{error, info}; +use tracing::{error, info, warn}; // Added warn use tokio::sync::mpsc; @@ -50,7 +50,7 @@ pub async fn run_ui() -> Result<()> { mpsc::channel::(1); let (save_table_result_sender, mut save_table_result_receiver) = mpsc::channel::>(1); - let (save_logic_result_sender, mut save_logic_result_receiver) = + let (save_logic_result_sender, _save_logic_result_receiver) = // Prefixed and removed mut mpsc::channel::>(1); let mut event_handler = EventHandler::new( @@ -73,8 +73,6 @@ pub async fn run_ui() -> Result<()> { let mut auto_logged_in = false; match load_auth_data() { Ok(Some(stored_data)) => { - // TODO: Optionally validate token with server here - // For now, assume valid if successfully loaded auth_state.auth_token = Some(stored_data.access_token); auth_state.user_id = Some(stored_data.user_id); auth_state.role = Some(stored_data.role); @@ -91,27 +89,20 @@ pub async fn run_ui() -> Result<()> { } // --- END DATA --- - // Initialize app state with profile tree and table structure let column_names = UiService::initialize_app_state(&mut grpc_client, &mut app_state) .await.context("Failed to initialize app state from UI service")?; let mut form_state = FormState::new(column_names); - // Fetch the total count of Adresar entries UiService::initialize_adresar_count(&mut grpc_client, &mut app_state).await?; form_state.reset_to_empty(); - // --- DATA2: Adjust initial view based on auth status --- if auto_logged_in { - // User is auto-logged in, go to main app view buffer_state.history = vec![AppView::Form]; buffer_state.active_index = 0; info!("Initial view set to Form due to auto-login."); } - // If not auto-logged in, BufferState default (Intro) will be used - // --- END DATA2 --- - // --- FPS Calculation State --- let mut last_frame_time = Instant::now(); let mut current_fps = 0.0; let mut needs_redraw = true; @@ -119,7 +110,6 @@ pub async fn run_ui() -> Result<()> { loop { // --- Synchronize UI View from Active Buffer --- if let Some(active_view) = buffer_state.get_active_view() { - // Reset all flags first app_state.ui.show_intro = false; app_state.ui.show_login = false; app_state.ui.show_register = false; @@ -142,21 +132,19 @@ pub async fn run_ui() -> Result<()> { event_handler.command_message = format!("Error refreshing admin data: {}", e); } } - 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 + app_state.ui.show_admin = true; + let profile_names = app_state.profile_tree.profiles.iter() + .map(|p| p.name.clone()) + .collect(); 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, + 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)); } @@ -164,16 +152,56 @@ pub async fn run_ui() -> Result<()> { AppView::AddTable => app_state.ui.show_add_table = true, AppView::AddLogic => app_state.ui.show_add_logic = true, AppView::Form => app_state.ui.show_form = true, - AppView::Scratch => {} // Or show a scratchpad component + AppView::Scratch => {} } } // --- End Synchronization --- + // --- Handle Pending Table Structure Fetches --- + if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { + if app_state.ui.show_add_logic { + // Ensure admin_state.add_logic_state matches the pending fetch + if admin_state.add_logic_state.profile_name == profile_name && + admin_state.add_logic_state.selected_table_name.as_deref() == Some(table_name.as_str()) { + + info!("Fetching table structure for {}.{}", profile_name, table_name); + let fetch_message = UiService::initialize_add_logic_table_data( + &mut grpc_client, + &mut admin_state.add_logic_state, + ).await.unwrap_or_else(|e| { + error!("Error initializing add_logic_table_data: {}", e); + format!("Error fetching table structure: {}", e) + }); + + if !fetch_message.contains("Error") && !fetch_message.contains("Warning") { + info!("{}", fetch_message); + // Optionally update command message on success if desired + // event_handler.command_message = fetch_message; + } else { + event_handler.command_message = fetch_message; // Show error/warning to user + } + needs_redraw = true; + } else { + error!( + "Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but add_logic_state is for {}.{:?}", + profile_name, table_name, + admin_state.add_logic_state.profile_name, + admin_state.add_logic_state.selected_table_name + ); + // Cleared by .take(), no need to set to None explicitly unless re-queueing + } + } else { + warn!( + "Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.", + profile_name, table_name + ); + // If you need to re-queue: + // app_state.pending_table_structure_fetch = Some((profile_name, table_name)); + } + } + // --- 3. Draw UI --- - // Draw the current state *first*. This ensures the loading dialog - // set in the *previous* iteration gets rendered before the pending - // action check below. - if needs_redraw { + if needs_redraw { terminal.draw(|f| { render_ui( f, @@ -185,7 +213,7 @@ pub async fn run_ui() -> Result<()> { &mut admin_state, &buffer_state, &theme, - event_handler.is_edit_mode, // Use event_handler's state + event_handler.is_edit_mode, &event_handler.highlight_state, app_state.total_count, app_state.current_position, @@ -201,7 +229,6 @@ pub async fn run_ui() -> Result<()> { } // --- Cursor Visibility Logic --- - // (Keep existing cursor logic here - depends on state drawn above) let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); match current_mode { AppMode::Edit => { terminal.show_cursor()?; } @@ -222,8 +249,6 @@ pub async fn run_ui() -> Result<()> { let total_count = app_state.total_count; let mut current_position = app_state.current_position; let position_before_event = current_position; - // --- Determine if redraw is needed based on active login --- - // Always redraw if the loading dialog is currently showing. if app_state.ui.dialog.is_loading { needs_redraw = true; } @@ -231,10 +256,9 @@ pub async fn run_ui() -> Result<()> { // --- 1. Handle Terminal Events --- let mut event_outcome_result = Ok(EventOutcome::Ok(String::new())); let mut event_processed = false; - // Poll for events *after* drawing and checking pending actions if crossterm_event::poll(std::time::Duration::from_millis(1))? { let event = event_reader.read_event().context("Failed to read terminal event")?; - event_processed = true; // Mark that we received and will process an event + event_processed = true; event_outcome_result = event_handler.handle_event( event, &config, @@ -257,9 +281,6 @@ pub async fn run_ui() -> Result<()> { if event_processed { needs_redraw = true; } - - // Update position based on handler's modification - // This happens *after* the event is handled app_state.current_position = current_position; // --- Check for Login Results from Channel --- @@ -272,7 +293,6 @@ pub async fn run_ui() -> Result<()> { Err(mpsc::error::TryRecvError::Empty) => { /* No message waiting */ } Err(mpsc::error::TryRecvError::Disconnected) => { error!("Login result channel disconnected unexpectedly."); - // Optionally show an error dialog here } } @@ -291,7 +311,7 @@ pub async fn run_ui() -> Result<()> { // --- Check for Save Table Results --- match save_table_result_receiver.try_recv() { Ok(result) => { - app_state.hide_dialog(); // Hide loading indicator + app_state.hide_dialog(); match result { Ok(ref success_message) => { app_state.show_dialog( @@ -304,12 +324,11 @@ pub async fn run_ui() -> Result<()> { } Err(e) => { event_handler.command_message = format!("Save failed: {}", e); - // Optionally show an error dialog instead of just command message } } needs_redraw = true; } - Err(mpsc::error::TryRecvError::Empty) => {} // No message + Err(mpsc::error::TryRecvError::Empty) => {} Err(mpsc::error::TryRecvError::Disconnected) => { error!("Save table result channel disconnected unexpectedly."); } @@ -319,19 +338,15 @@ pub async fn run_ui() -> Result<()> { let mut should_exit = false; match event_outcome_result { Ok(outcome) => match outcome { - EventOutcome::Ok(message) => { - if !message.is_empty() { - // Update command message only if event handling produced one - // Avoid overwriting messages potentially set by pending actions - // event_handler.command_message = message; - } + EventOutcome::Ok(_message) => { + // Message is often set directly in event_handler.command_message } EventOutcome::Exit(message) => { event_handler.command_message = message; should_exit = true; } EventOutcome::DataSaved(save_outcome, message) => { - event_handler.command_message = message; // Show save status + event_handler.command_message = message; if let Err(e) = UiService::handle_save_outcome( save_outcome, &mut grpc_client, @@ -345,119 +360,87 @@ pub async fn run_ui() -> Result<()> { } } EventOutcome::ButtonSelected { context: _, index: _ } => { - // This case should ideally be fully handled within handle_event - // If initiate_login was called, it returned early. - // If not, the message was set and returned via Ok(message). - // Log if necessary, but likely no action needed here. - // log::warn!("ButtonSelected outcome reached main loop unexpectedly."); + // Handled within event_handler or specific navigation modules } }, Err(e) => { event_handler.command_message = format!("Error: {}", e); } - } // --- End Consequence Handling --- + } + // --- End Consequence Handling --- - // --- Position Change Handling (after outcome processing and pending actions) --- + // --- Position Change Handling --- let position_changed = app_state.current_position != position_before_event; - let current_total_count = app_state.total_count; + let current_total_count = app_state.total_count; // Use current total_count let mut position_logic_needs_redraw = false; + if app_state.ui.show_form { if position_changed && !event_handler.is_edit_mode { let current_input = form_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { - current_input.len() - 1 // Limit to last character in readonly mode - } else { - 0 - }; - form_state.current_cursor_pos = - event_handler.ideal_cursor_column.min(max_cursor_pos); + let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); position_logic_needs_redraw = true; - // Ensure position never exceeds total_count + 1 if app_state.current_position > current_total_count + 1 { app_state.current_position = current_total_count + 1; } + if app_state.current_position > current_total_count { - // New entry - reset form form_state.reset_to_empty(); form_state.current_field = 0; - } else if app_state.current_position >= 1 - && app_state.current_position <= current_total_count - { - // Existing entry - load data - let current_position_to_load = app_state.current_position; // Use a copy + } else if app_state.current_position >= 1 && app_state.current_position <= current_total_count { + let current_position_to_load = app_state.current_position; let load_message = UiService::load_adresar_by_position( &mut grpc_client, - &mut app_state, // Pass app_state mutably if needed by the service + &mut app_state, &mut form_state, current_position_to_load, ) .await.with_context(|| format!("Failed to load adresar by position: {}", current_position_to_load))?; - let current_input = form_state.get_current_input(); - let max_cursor_pos = if !event_handler.is_edit_mode - && !current_input.is_empty() - { - current_input.len() - 1 // In readonly mode, limit to last character + let current_input_after_load = form_state.get_current_input(); + let max_cursor_pos_after_load = if !event_handler.is_edit_mode && !current_input_after_load.is_empty() { + current_input_after_load.len() - 1 } else { - current_input.len() + current_input_after_load.len() }; - form_state.current_cursor_pos = event_handler - .ideal_cursor_column - .min(max_cursor_pos); - // Don't overwrite message from handle_event if load_message is simple success - if !load_message.starts_with("Loaded entry") - || event_handler.command_message.is_empty() - { + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_after_load); + + if !load_message.starts_with("Loaded entry") || event_handler.command_message.is_empty() { event_handler.command_message = load_message; } - } else { - // Invalid position (e.g., 0) - reset to first entry or new entry mode - app_state.current_position = - 1.min(current_total_count + 1); // Go to 1 or new entry if empty - if app_state.current_position > total_count { + } else { // current_position is 0 or invalid + app_state.current_position = 1.min(current_total_count + 1); + if app_state.current_position > current_total_count { // Handles empty db case form_state.reset_to_empty(); form_state.current_field = 0; } + // If db is not empty, this will trigger load in next iteration if position changed to 1 } } else if !position_changed && !event_handler.is_edit_mode { - // If position didn't change but we are in read-only, just adjust cursor let current_input = form_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { - current_input.len() - 1 - } else { - 0 - }; - form_state.current_cursor_pos = - event_handler.ideal_cursor_column.min(max_cursor_pos); + let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } else if app_state.ui.show_register { if !event_handler.is_edit_mode { let current_input = register_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { - current_input.len() - 1 - } else { - 0 - }; + let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; register_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } else if app_state.ui.show_login { if !event_handler.is_edit_mode { let current_input = login_state.get_current_input(); - let max_cursor_pos = if !current_input.is_empty() { - current_input.len() - 1 - } else { - 0 - }; + let max_cursor_pos = if !current_input.is_empty() { current_input.len() - 1 } else { 0 }; login_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); } } + if position_logic_needs_redraw { needs_redraw = true; } // --- End Position Change Handling --- - // Check exit condition *after* all processing for the iteration if should_exit { return Ok(()); } @@ -466,9 +449,8 @@ pub async fn run_ui() -> Result<()> { let now = Instant::now(); let frame_duration = now.duration_since(last_frame_time); last_frame_time = now; - if frame_duration.as_secs_f64() > 1e-6 { + if frame_duration.as_secs_f64() > 1e-6 { // Avoid division by zero current_fps = 1.0 / frame_duration.as_secs_f64(); } } // End main loop } -