diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index f6c5fdf..22e1c03 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -48,6 +48,7 @@ use crossterm::event::KeyCode; use crossterm::event::{Event, KeyEvent}; use tokio::sync::mpsc; use tokio::sync::mpsc::unbounded_channel; +use tracing::{info, error}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum EventOutcome { @@ -122,11 +123,11 @@ impl EventHandler { self.navigation_state.activate_find_file(options); } - // REFACTORED: This function now safely handles state changes. + // This function handles state changes. async fn handle_search_palette_event( &mut self, key_event: KeyEvent, - grpc_client: &mut GrpcClient, + mut grpc_client: GrpcClient, form_state: &mut FormState, app_state: &mut AppState, ) -> Result { @@ -175,6 +176,7 @@ impl EventHandler { _ => {} } + // --- START CORRECTED LOGIC --- if trigger_search { search_state.is_loading = true; search_state.results.clear(); @@ -182,14 +184,19 @@ impl EventHandler { let query = search_state.input.clone(); let table_name = search_state.table_name.clone(); - let mut client = grpc_client.clone(); let sender = self.search_result_sender.clone(); + // We now move the grpc_client into the task, just like with login. tokio::spawn(async move { - if let Ok(response) = client.search_table(table_name, query).await { - let _ = sender.send(response.hits); - } else { - let _ = sender.send(vec![]); + match grpc_client.search_table(table_name, query).await { + Ok(response) => { + info!("Search successful. Received {} hits.", response.hits.len()); + let _ = sender.send(response.hits); + } + Err(e) => { + error!("gRPC search call failed: {}", e); + let _ = sender.send(vec![]); + } } }); } @@ -232,7 +239,7 @@ impl EventHandler { if app_state.ui.show_search_palette { if let Event::Key(key_event) = event { - return self.handle_search_palette_event(key_event, grpc_client, form_state, app_state).await; + return self.handle_search_palette_event(key_event, grpc_client.clone(), form_state, app_state).await; } return Ok(EventOutcome::Ok(String::new())); } diff --git a/client/src/state/pages/canvas_state.rs b/client/src/state/pages/canvas_state.rs index c3ec0f1..5c6e85c 100644 --- a/client/src/state/pages/canvas_state.rs +++ b/client/src/state/pages/canvas_state.rs @@ -1,6 +1,5 @@ // src/state/canvas_state.rs - pub trait CanvasState { fn current_field(&self) -> usize; fn current_cursor_pos(&self) -> usize; diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index acf7777..fbeaa9b 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -1,23 +1,19 @@ // src/state/pages/form.rs -use std::collections::HashMap; // NEW +use std::collections::HashMap; use crate::config::colors::themes::Theme; use ratatui::layout::Rect; use ratatui::Frame; use crate::state::app::highlight::HighlightState; use crate::state::pages::canvas_state::CanvasState; -use crate::state::app::state::AppState; -use crate::components::common::search_palette::render_search_palette; pub struct FormState { pub id: i64, - // NEW fields for dynamic table context pub profile_name: String, pub table_name: String, pub total_count: u64, - pub current_position: u64, // 1-based index, 0 or total_count + 1 for new entry - - pub fields: Vec, // Already dynamic, which is good + pub current_position: u64, + pub fields: Vec, pub values: Vec, pub current_field: usize, pub has_unsaved_changes: bool, @@ -25,9 +21,6 @@ pub struct FormState { } impl FormState { - /// Creates a new, empty FormState for a given table. - /// The position defaults to 1, representing either the first record - /// or the position for a new entry if the table is empty. pub fn new( profile_name: String, table_name: String, @@ -35,11 +28,10 @@ impl FormState { ) -> Self { let values = vec![String::new(); fields.len()]; FormState { - id: 0, // Default to 0, indicating a new or unloaded record + id: 0, profile_name, table_name, - total_count: 0, // Will be fetched after initialization - // FIX: Default to 1. A position of 0 is an invalid state. + total_count: 0, current_position: 1, fields, values, @@ -49,6 +41,7 @@ impl FormState { } } + // This signature is now correct and only deals with form-related state. pub fn render( &self, f: &mut Frame, @@ -56,7 +49,6 @@ impl FormState { theme: &Theme, is_edit_mode: bool, highlight_state: &HighlightState, - app_state: &AppState, ) { let fields_str_slice: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect(); @@ -65,7 +57,7 @@ impl FormState { crate::components::form::form::render_form( f, area, - self, // Pass self as CanvasState + self, &fields_str_slice, &self.current_field, &values_str_slice, @@ -76,27 +68,19 @@ impl FormState { self.total_count, self.current_position, ); - - if app_state.ui.show_search_palette { - if let Some(search_state) = &app_state.search_state { - render_search_palette(f, f.size(), theme, search_state); - } - } } - /// Resets the form to a state for creating a new entry. - /// It clears all values and sets the position to be one after the last record. + // ... other methods are unchanged ... pub fn reset_to_empty(&mut self) { self.id = 0; self.values.iter_mut().for_each(|v| v.clear()); self.current_field = 0; self.current_cursor_pos = 0; self.has_unsaved_changes = false; - // Set the position for a new entry. if self.total_count > 0 { self.current_position = self.total_count + 1; } else { - self.current_position = 1; // If table is empty, new record is at position 1 + self.current_position = 1; } } @@ -113,26 +97,19 @@ impl FormState { .expect("Invalid current_field index") } - /// Updates the form's values from a data response and sets its position. - /// This is the single source of truth for populating the form after a data fetch. pub fn update_from_response( &mut self, response_data: &HashMap, - // FIX: Add new_position to make this method authoritative. new_position: u64, ) { - // Create a new vector for the values, ensuring they are in the correct order. self.values = self.fields.iter().map(|field_from_schema| { - // For each field from our schema, find the corresponding key in the - // response data by doing a case-insensitive comparison. response_data .iter() .find(|(key_from_data, _)| key_from_data.eq_ignore_ascii_case(field_from_schema)) - .map(|(_, value)| value.clone()) // If found, clone its value. - .unwrap_or_default() // If not found, use an empty string. + .map(|(_, value)| value.clone()) + .unwrap_or_default() }).collect(); - // Now, do the same case-insensitive lookup for the 'id' field. let id_str_opt = response_data .iter() .find(|(k, _)| k.eq_ignore_ascii_case("id")) @@ -149,7 +126,6 @@ impl FormState { self.id = 0; } - // FIX: Set the position from the provided parameter. self.current_position = new_position; self.has_unsaved_changes = false; self.current_field = 0; @@ -175,12 +151,10 @@ impl CanvasState for FormState { } fn get_current_input(&self) -> &str { - // Re-use the struct's own method FormState::get_current_input(self) } fn get_current_input_mut(&mut self) -> &mut String { - // Re-use the struct's own method FormState::get_current_input_mut(self) } diff --git a/client/src/ui/handlers/render.rs b/client/src/ui/handlers/render.rs index 46393a6..52abba3 100644 --- a/client/src/ui/handlers/render.rs +++ b/client/src/ui/handlers/render.rs @@ -4,7 +4,7 @@ use crate::components::{ admin::add_logic::render_add_logic, admin::render_add_table, auth::{login::render_login, register::render_register}, - common::dialog::render_dialog, // Make sure this is imported + common::dialog::render_dialog, common::find_file_palette, common::search_palette::render_search_palette, form::form::render_form, @@ -17,6 +17,7 @@ use crate::components::{ }; use crate::config::colors::themes::Theme; use crate::modes::general::command_navigation::NavigationState; +use crate::state::pages::canvas_state::CanvasState; use crate::state::app::buffer::BufferState; use crate::state::app::highlight::HighlightState; use crate::state::app::state::AppState; @@ -24,7 +25,6 @@ use crate::state::pages::admin::AdminState; use crate::state::pages::auth::AuthState; use crate::state::pages::auth::LoginState; use crate::state::pages::auth::RegisterState; -use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::form::FormState; use crate::state::pages::intro::IntroState; use ratatui::{ @@ -189,17 +189,13 @@ pub fn render_ui( .split(form_actual_area)[1] }; - // --- FIX START --- - // The call to `form_state.render` is now separate from the popup rendering. form_state.render( f, form_render_area, theme, is_event_handler_edit_mode, highlight_state, - app_state, ); - // --- FIX END --- } if let Some(area) = buffer_list_area { @@ -236,8 +232,7 @@ pub fn render_ui( } } - // --- FIX START --- - // This block now handles drawing popups over any view. This is the correct place for it. + // This block now correctly handles drawing popups over any view. if app_state.ui.show_search_palette { if let Some(search_state) = &app_state.search_state { render_search_palette(f, f.area(), theme, search_state); @@ -254,5 +249,4 @@ pub fn render_ui( app_state.ui.dialog.is_loading, ); } - // --- FIX END --- } diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index 827ded4..f362117 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -126,6 +126,24 @@ pub async fn run_ui() -> Result<()> { loop { let position_before_event = form_state.current_position; let mut event_processed = false; + + match event_handler.search_result_receiver.try_recv() { + Ok(hits) => { + if let Some(search_state) = app_state.search_state.as_mut() { + search_state.results = hits; + search_state.is_loading = false; + } + needs_redraw = true; + } + Err(mpsc::error::TryRecvError::Empty) => { + } + Err(mpsc::error::TryRecvError::Disconnected) => { + error!("Search result channel disconnected!"); + } + } + if app_state.ui.show_search_palette { + needs_redraw = true; + } 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;