From 4ed8e7b4210cef8140352d02ac58d0e9a48a1de6 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 22 Aug 2025 00:27:23 +0200 Subject: [PATCH] fixed form state removed, but not won, aint working yet --- client/config.toml | 2 +- client/src/modes/common/command_mode.rs | 7 +- client/src/modes/common/commands.rs | 12 +- client/src/modes/general/navigation.rs | 9 +- client/src/modes/handlers/event.rs | 189 ++++++++++--------- client/src/tui/functions/common/form.rs | 240 ++++++++++++------------ client/src/ui/handlers/ui.rs | 68 ++++--- 7 files changed, 275 insertions(+), 252 deletions(-) diff --git a/client/config.toml b/client/config.toml index ab3ae25..d370570 100644 --- a/client/config.toml +++ b/client/config.toml @@ -50,7 +50,7 @@ move_right = ["l", "Right"] move_down = ["j", "Down"] # Optional move_line_end = ["$"] -# move_word_next = ["w"] +move_word_next = ["w"] next_field = ["Tab"] move_word_prev = ["b"] move_word_end = ["e"] diff --git a/client/src/modes/common/command_mode.rs b/client/src/modes/common/command_mode.rs index 2fac602..b1d53c9 100644 --- a/client/src/modes/common/command_mode.rs +++ b/client/src/modes/common/command_mode.rs @@ -18,7 +18,6 @@ pub async fn handle_command_event( app_state: &mut AppState, login_state: &LoginState, register_state: &RegisterState, - form_state: &mut FormState, command_input: &mut String, command_message: &mut String, grpc_client: &mut GrpcClient, @@ -38,7 +37,6 @@ pub async fn handle_command_event( if config.is_command_execute(key.code, key.modifiers) { return process_command( config, - form_state, app_state, login_state, register_state, @@ -73,7 +71,6 @@ pub async fn handle_command_event( async fn process_command( config: &Config, - form_state: &mut FormState, app_state: &mut AppState, login_state: &LoginState, register_state: &RegisterState, @@ -103,7 +100,6 @@ async fn process_command( action, terminal, app_state, - form_state, login_state, register_state, ) @@ -118,7 +114,6 @@ async fn process_command( "save" => { let outcome = save( app_state, - form_state, grpc_client, ).await?; let message = match outcome { @@ -131,7 +126,7 @@ async fn process_command( }, "revert" => { let message = revert( - form_state, + app_state, grpc_client, ).await?; command_input.clear(); diff --git a/client/src/modes/common/commands.rs b/client/src/modes/common/commands.rs index aa8a6a7..104e908 100644 --- a/client/src/modes/common/commands.rs +++ b/client/src/modes/common/commands.rs @@ -15,13 +15,12 @@ impl CommandHandler { &mut self, action: &str, terminal: &mut TerminalCore, - app_state: &AppState, - form_state: &FormState, + app_state: &mut AppState, login_state: &LoginState, register_state: &RegisterState, ) -> Result<(bool, String)> { match action { - "quit" => self.handle_quit(terminal, app_state, form_state, login_state, register_state).await, + "quit" => self.handle_quit(terminal, app_state, login_state, register_state).await, "force_quit" => self.handle_force_quit(terminal).await, "save_and_quit" => self.handle_save_quit(terminal).await, _ => Ok((false, format!("Unknown command: {}", action))), @@ -31,8 +30,7 @@ impl CommandHandler { async fn handle_quit( &self, terminal: &mut TerminalCore, - app_state: &AppState, - form_state: &FormState, + app_state: &mut AppState, login_state: &LoginState, register_state: &RegisterState, ) -> Result<(bool, String)> { @@ -41,8 +39,10 @@ impl CommandHandler { login_state.has_unsaved_changes() } else if app_state.ui.show_register { register_state.has_unsaved_changes() + } else if let Some(fs) = app_state.form_state_mut() { + fs.has_unsaved_changes } else { - form_state.has_unsaved_changes + false }; if !has_unsaved { diff --git a/client/src/modes/general/navigation.rs b/client/src/modes/general/navigation.rs index 895513a..dbadc01 100644 --- a/client/src/modes/general/navigation.rs +++ b/client/src/modes/general/navigation.rs @@ -17,7 +17,6 @@ use anyhow::Result; pub async fn handle_navigation_event( key: KeyEvent, config: &Config, - form_state: &mut FormState, app_state: &mut AppState, login_state: &mut LoginState, register_state: &mut RegisterState, @@ -52,11 +51,15 @@ pub async fn handle_navigation_event( return Ok(EventOutcome::Ok(String::new())); } "next_field" => { - next_field(form_state); + if let Some(fs) = app_state.form_state_mut() { + next_field(fs); + } return Ok(EventOutcome::Ok(String::new())); } "prev_field" => { - prev_field(form_state); + if let Some(fs) = app_state.form_state_mut() { + prev_field(fs); + } return Ok(EventOutcome::Ok(String::new())); } "enter_command_mode" => { diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 861e3c6..bae9602 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -220,66 +220,90 @@ impl EventHandler { async fn handle_search_palette_event( &mut self, key_event: KeyEvent, - form_state: &mut FormState, app_state: &mut AppState, ) -> Result { let mut should_close = false; let mut outcome_message = String::new(); let mut trigger_search = false; - if let Some(search_state) = app_state.search_state.as_mut() { - match key_event.code { - KeyCode::Esc => { - should_close = true; - outcome_message = "Search cancelled".to_string(); - } - KeyCode::Enter => { - if let Some(selected_hit) = - search_state.results.get(search_state.selected_index) - { - if let Ok(data) = serde_json::from_str::< - std::collections::HashMap, - >(&selected_hit.content_json) - { - let detached_pos = form_state.total_count + 2; - form_state - .update_from_response(&data, detached_pos); - } + // Step 1: Handle search_state logic in a short scope + let (maybe_data, maybe_id) = { + if let Some(search_state) = app_state.search_state.as_mut() { + match key_event.code { + KeyCode::Esc => { should_close = true; - outcome_message = - format!("Loaded record ID {}", selected_hit.id); + outcome_message = "Search cancelled".to_string(); + (None, None) } - } - KeyCode::Up => search_state.previous_result(), - KeyCode::Down => search_state.next_result(), - KeyCode::Char(c) => { - search_state - .input - .insert(search_state.cursor_position, c); - search_state.cursor_position += 1; - trigger_search = true; - } - KeyCode::Backspace => { - if search_state.cursor_position > 0 { - search_state.cursor_position -= 1; - search_state.input.remove(search_state.cursor_position); - trigger_search = true; + KeyCode::Enter => { + if let Some(selected_hit) = + search_state.results.get(search_state.selected_index) + { + if let Ok(data) = serde_json::from_str::< + std::collections::HashMap, + >(&selected_hit.content_json) + { + (Some(data), Some(selected_hit.id)) + } else { + (None, None) + } + } else { + (None, None) + } } - } - KeyCode::Left => { - search_state.cursor_position = - search_state.cursor_position.saturating_sub(1); - } - KeyCode::Right => { - if search_state.cursor_position < search_state.input.len() - { + KeyCode::Up => { + search_state.previous_result(); + (None, None) + } + KeyCode::Down => { + search_state.next_result(); + (None, None) + } + KeyCode::Char(c) => { + search_state.input.insert(search_state.cursor_position, c); search_state.cursor_position += 1; + trigger_search = true; + (None, None) } + KeyCode::Backspace => { + if search_state.cursor_position > 0 { + search_state.cursor_position -= 1; + search_state.input.remove(search_state.cursor_position); + trigger_search = true; + } + (None, None) + } + KeyCode::Left => { + search_state.cursor_position = + search_state.cursor_position.saturating_sub(1); + (None, None) + } + KeyCode::Right => { + if search_state.cursor_position < search_state.input.len() { + search_state.cursor_position += 1; + } + (None, None) + } + _ => (None, None), } - _ => {} + } else { + (None, None) } + }; - if trigger_search { + // Step 2: Now safe to borrow form_state + if let (Some(data), Some(id)) = (maybe_data, maybe_id) { + if let Some(fs) = app_state.form_state_mut() { + let detached_pos = fs.total_count + 2; + fs.update_from_response(&data, detached_pos); + } + should_close = true; + outcome_message = format!("Loaded record ID {}", id); + } + + // Step 3: Trigger async search if needed + if trigger_search { + if let Some(search_state) = app_state.search_state.as_mut() { search_state.is_loading = true; search_state.results.clear(); search_state.selected_index = 0; @@ -289,10 +313,7 @@ impl EventHandler { let sender = self.search_result_sender.clone(); let mut grpc_client = self.grpc_client.clone(); - info!( - "--- 1. Spawning search task for query: '{}' ---", - query - ); + info!("--- 1. Spawning search task for query: '{}' ---", query); tokio::spawn(async move { info!("--- 2. Background task started. ---"); match grpc_client.search_table(table_name, query).await { @@ -328,7 +349,6 @@ impl EventHandler { config: &Config, terminal: &mut TerminalCore, command_handler: &mut CommandHandler, - form_state: &mut FormState, auth_state: &mut AuthState, login_state: &mut LoginState, register_state: &mut RegisterState, @@ -339,13 +359,7 @@ impl EventHandler { ) -> Result { if app_state.ui.show_search_palette { if let Event::Key(key_event) = event { - return self - .handle_search_palette_event( - key_event, - form_state, - app_state, - ) - .await; + return self.handle_search_palette_event(key_event, app_state).await; } return Ok(EventOutcome::Ok(String::new())); } @@ -570,7 +584,6 @@ impl EventHandler { let nav_outcome = navigation::handle_navigation_event( key_event, config, - form_state, app_state, login_state, register_state, @@ -580,9 +593,7 @@ impl EventHandler { &mut self.command_input, &mut self.command_message, &mut self.navigation_state, - ) - .await; - + ).await; match nav_outcome { Ok(EventOutcome::ButtonSelected { context, index }) => { let message = match context { @@ -692,7 +703,6 @@ impl EventHandler { return self .handle_core_action( action, - form_state, auth_state, login_state, register_state, @@ -739,7 +749,6 @@ impl EventHandler { return self .handle_core_action( action, - form_state, auth_state, login_state, register_state, @@ -792,15 +801,18 @@ impl EventHandler { } if config.is_command_execute(key_code, modifiers) { - let mut current_position = form_state.current_position; - let total_count = form_state.total_count; + let (mut current_position, total_count) = if let Some(fs) = app_state.form_state() { + (fs.current_position, fs.total_count) + } else { + (1, 0) + }; + let outcome = command_mode::handle_command_event( key_event, config, app_state, login_state, register_state, - form_state, &mut self.command_input, &mut self.command_message, &mut self.grpc_client, @@ -808,9 +820,10 @@ impl EventHandler { terminal, &mut current_position, total_count, - ) - .await?; - form_state.current_position = current_position; + ).await?; + if let Some(fs) = app_state.form_state_mut() { + fs.current_position = current_position; + } self.command_mode = false; self.key_sequence_tracker.reset(); let new_mode = ModeManager::derive_mode( @@ -921,7 +934,6 @@ impl EventHandler { async fn handle_core_action( &mut self, action: &str, - form_state: &mut FormState, auth_state: &mut AuthState, login_state: &mut LoginState, register_state: &mut RegisterState, @@ -940,12 +952,15 @@ impl EventHandler { .await?; Ok(EventOutcome::Ok(message)) } else { - let save_outcome = crate::tui::functions::common::form::save( - app_state, - form_state, - &mut self.grpc_client, - ) - .await?; + let save_outcome = if let Some(fs) = app_state.form_state_mut() { + crate::tui::functions::common::form::save( + app_state, + &mut self.grpc_client, + ) + .await? + } else { + SaveOutcome::NoChange + }; let message = match save_outcome { SaveOutcome::NoChange => "No changes to save.".to_string(), SaveOutcome::UpdatedExisting => "Entry updated.".to_string(), @@ -975,10 +990,8 @@ impl EventHandler { } else { let save_outcome = crate::tui::functions::common::form::save( app_state, - form_state, &mut self.grpc_client, - ) - .await?; + ).await?; match save_outcome { SaveOutcome::NoChange => "No changes to save.".to_string(), SaveOutcome::UpdatedExisting => "Entry updated.".to_string(), @@ -1003,13 +1016,17 @@ impl EventHandler { register_state, app_state, ) - .await + .await } else { - crate::tui::functions::common::form::revert( - form_state, - &mut self.grpc_client, - ) - .await? + if let Some(fs) = app_state.form_state_mut() { + crate::tui::functions::common::form::revert( + app_state, + &mut self.grpc_client, + ) + .await? + } else { + "Nothing to revert".to_string() + } }; Ok(EventOutcome::Ok(message)) } diff --git a/client/src/tui/functions/common/form.rs b/client/src/tui/functions/common/form.rs index 86c8bec..8916d59 100644 --- a/client/src/tui/functions/common/form.rs +++ b/client/src/tui/functions/common/form.rs @@ -1,9 +1,7 @@ // src/tui/functions/common/form.rs - use crate::services::grpc_client::GrpcClient; -use crate::state::app::state::AppState; // NEW: Import AppState -use crate::state::pages::form::FormState; -use crate::utils::data_converter; // NEW: Import our translator +use crate::state::app::state::AppState; +use crate::utils::data_converter; use anyhow::{anyhow, Context, Result}; use std::collections::HashMap; @@ -14,143 +12,137 @@ pub enum SaveOutcome { CreatedNew(i64), } -// MODIFIED save function signature and logic pub async fn save( - app_state: &AppState, // NEW: Pass in AppState - form_state: &mut FormState, + app_state: &mut AppState, grpc_client: &mut GrpcClient, ) -> Result { - if !form_state.has_unsaved_changes { - return Ok(SaveOutcome::NoChange); - } - - // --- NEW: VALIDATION & CONVERSION STEP --- - let cache_key = - format!("{}.{}", form_state.profile_name, form_state.table_name); - let schema = match app_state.schema_cache.get(&cache_key) { - Some(s) => s, - None => { - return Err(anyhow!( - "Schema for table '{}' not found in cache. Cannot save.", - form_state.table_name - )); + if let Some(fs) = app_state.form_state_mut() { + if !fs.has_unsaved_changes { + return Ok(SaveOutcome::NoChange); } - }; - let data_map: HashMap = form_state - .fields - .iter() - .zip(form_state.values.iter()) - .map(|(field_def, value)| (field_def.data_key.clone(), value.clone())) - .collect(); + // Copy out what we need before dropping the mutable borrow + let profile_name = fs.profile_name.clone(); + let table_name = fs.table_name.clone(); + let fields = fs.fields.clone(); + let values = fs.values.clone(); + let id = fs.id; + let total_count = fs.total_count; + let current_position = fs.current_position; - // Use our new translator. It returns a user-friendly error on failure. - let converted_data = - match data_converter::convert_and_validate_data(&data_map, schema) { - Ok(data) => data, - Err(user_error) => return Err(anyhow!(user_error)), + let cache_key = format!("{}.{}", profile_name, table_name); + let schema = app_state + .schema_cache + .get(&cache_key) + .ok_or_else(|| { + anyhow!( + "Schema for table '{}' not found in cache. Cannot save.", + table_name + ) + })?; + + let data_map: HashMap = fields + .iter() + .zip(values.iter()) + .map(|(field_def, value)| (field_def.data_key.clone(), value.clone())) + .collect(); + + let converted_data = + data_converter::convert_and_validate_data(&data_map, schema) + .map_err(|user_error| anyhow!(user_error))?; + + let is_new_entry = id == 0 + || (total_count > 0 && current_position > total_count) + || (total_count == 0 && current_position == 1); + + let outcome = if is_new_entry { + let response = grpc_client + .post_table_data(profile_name.clone(), table_name.clone(), converted_data) + .await + .context("Failed to post new table data")?; + + if response.success { + if let Some(fs) = app_state.form_state_mut() { + fs.id = response.inserted_id; + fs.total_count += 1; + fs.current_position = fs.total_count; + fs.has_unsaved_changes = false; + } + SaveOutcome::CreatedNew(response.inserted_id) + } else { + return Err(anyhow!("Server failed to insert data: {}", response.message)); + } + } else { + if id == 0 { + return Err(anyhow!( + "Cannot update record: ID is 0, but not classified as new entry." + )); + } + let response = grpc_client + .put_table_data(profile_name.clone(), table_name.clone(), id, converted_data) + .await + .context("Failed to put (update) table data")?; + + if response.success { + if let Some(fs) = app_state.form_state_mut() { + fs.has_unsaved_changes = false; + } + SaveOutcome::UpdatedExisting + } else { + return Err(anyhow!("Server failed to update data: {}", response.message)); + } }; - // --- END OF NEW STEP --- - let outcome: SaveOutcome; - let is_new_entry = form_state.id == 0 - || (form_state.total_count > 0 - && form_state.current_position > form_state.total_count) - || (form_state.total_count == 0 && form_state.current_position == 1); - - if is_new_entry { - let response = grpc_client - .post_table_data( - form_state.profile_name.clone(), - form_state.table_name.clone(), - converted_data, // Use the validated & converted data - ) - .await - .context("Failed to post new table data")?; - - if response.success { - form_state.id = response.inserted_id; - form_state.total_count += 1; - form_state.current_position = form_state.total_count; - outcome = SaveOutcome::CreatedNew(response.inserted_id); - } else { - return Err(anyhow!( - "Server failed to insert data: {}", - response.message - )); - } + Ok(outcome) } else { - if form_state.id == 0 { - return Err(anyhow!( - "Cannot update record: ID is 0, but not classified as new entry." - )); - } - let response = grpc_client - .put_table_data( - form_state.profile_name.clone(), - form_state.table_name.clone(), - form_state.id, - converted_data, // Use the validated & converted data - ) - .await - .context("Failed to put (update) table data")?; - - if response.success { - outcome = SaveOutcome::UpdatedExisting; - } else { - return Err(anyhow!( - "Server failed to update data: {}", - response.message - )); - } + Ok(SaveOutcome::NoChange) } - - form_state.has_unsaved_changes = false; - Ok(outcome) } pub async fn revert( - form_state: &mut FormState, // Takes &mut FormState to update it + app_state: &mut AppState, grpc_client: &mut GrpcClient, ) -> Result { - if form_state.id == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) || (form_state.total_count == 0 && form_state.current_position == 1) { - let old_total_count = form_state.total_count; // Preserve for correct new position - form_state.reset_to_empty(); // reset_to_empty will clear values and set id=0 - form_state.total_count = old_total_count; // Restore total_count - if form_state.total_count > 0 { // Correctly set current_position for new - form_state.current_position = form_state.total_count + 1; - } else { - form_state.current_position = 1; + if let Some(fs) = app_state.form_state_mut() { + if fs.id == 0 + || (fs.total_count > 0 && fs.current_position > fs.total_count) + || (fs.total_count == 0 && fs.current_position == 1) + { + let old_total_count = fs.total_count; + fs.reset_to_empty(); + fs.total_count = old_total_count; + if fs.total_count > 0 { + fs.current_position = fs.total_count + 1; + } else { + fs.current_position = 1; + } + return Ok("New entry cleared".to_string()); } - return Ok("New entry cleared".to_string()); - } - if form_state.current_position == 0 || form_state.current_position > form_state.total_count { - if form_state.total_count > 0 { - form_state.current_position = 1; - } else { - // No records to revert to, effectively a new entry state. - form_state.reset_to_empty(); - return Ok("No saved data to revert to; form cleared.".to_string()); + if fs.current_position == 0 || fs.current_position > fs.total_count { + if fs.total_count > 0 { + fs.current_position = 1; + } else { + fs.reset_to_empty(); + return Ok("No saved data to revert to; form cleared.".to_string()); + } } + + let response = grpc_client + .get_table_data_by_position( + fs.profile_name.clone(), + fs.table_name.clone(), + fs.current_position as i32, + ) + .await + .context(format!( + "Failed to get table data by position {} for table {}.{}", + fs.current_position, fs.profile_name, fs.table_name + ))?; + + fs.update_from_response(&response.data, fs.current_position); + Ok("Changes discarded, reloaded last saved version".to_string()) + } else { + Ok("Nothing to revert".to_string()) } - - let response = grpc_client - .get_table_data_by_position( - form_state.profile_name.clone(), - form_state.table_name.clone(), - form_state.current_position as i32, - ) - .await - .context(format!( - "Failed to get table data by position {} for table {}.{}", - form_state.current_position, - form_state.profile_name, - form_state.table_name - ))?; - - // FIX: Pass the current position as the second argument - form_state.update_from_response(&response.data, form_state.current_position); - Ok("Changes discarded, reloaded last saved version".to_string()) } - diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index d22a4cb..d4eebd9 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -26,6 +26,7 @@ use crate::ui::handlers::context::DialogPurpose; use crate::tui::functions::common::login; use crate::tui::functions::common::register; use crate::utils::columns::filter_user_columns; +use canvas::keymap::KeyEventOutcome; use anyhow::{Context, Result}; use crossterm::cursor::SetCursorStyle; use crossterm::event as crossterm_event; @@ -193,33 +194,48 @@ pub async fn run_ui() -> Result<()> { 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; - let event_outcome_result = { - // We need to avoid borrowing app_state twice, so we'll need to modify the handle_event call - // For now, let's create a temporary approach - let mut temp_form_state = app_state.form_state_mut().unwrap().clone(); - let result = event_handler.handle_event( - event, - &config, - &mut terminal, - &mut command_handler, - &mut temp_form_state, - &mut auth_state, - &mut login_state, - &mut register_state, - &mut intro_state, - &mut admin_state, - &mut buffer_state, - &mut app_state, - ).await; - - // Update the app_state with any changes from temp_form_state - if let Some(form_state) = app_state.form_state_mut() { - *form_state = temp_form_state; - } - - result - }; + if let crossterm_event::Event::Key(key_event) = &event { + if app_state.ui.show_form { + if let Some(editor) = app_state.form_editor.as_mut() { + match editor.handle_key_event(*key_event) { + KeyEventOutcome::Consumed(Some(msg)) => { + event_handler.command_message = msg; + needs_redraw = true; + continue; + } + KeyEventOutcome::Consumed(None) => { + needs_redraw = true; + continue; + } + KeyEventOutcome::Pending => { + needs_redraw = true; + continue; + } + KeyEventOutcome::NotMatched => { + // fall through to client-level handling + } + } + } + } + } + + // Get form state from app_state and pass to handle_event + let form_state = app_state.form_state_mut().unwrap(); + + let event_outcome_result = event_handler.handle_event( + event, + &config, + &mut terminal, + &mut command_handler, + &mut auth_state, + &mut login_state, + &mut register_state, + &mut intro_state, + &mut admin_state, + &mut buffer_state, + &mut app_state, + ).await; let mut should_exit = false; match event_outcome_result { Ok(outcome) => match outcome {