diff --git a/client/src/services/ui_service.rs b/client/src/services/ui_service.rs index fe236f8..d0a3f05 100644 --- a/client/src/services/ui_service.rs +++ b/client/src/services/ui_service.rs @@ -196,7 +196,8 @@ impl UiService { .await { Ok(response) => { - form_state.update_from_response(&response.data); + // FIX: Pass the current position as the second argument + form_state.update_from_response(&response.data, form_state.current_position); Ok(format!( "Loaded entry {}/{} for table {}.{}", form_state.current_position, diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index cdf66aa..75fd303 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -23,7 +23,9 @@ pub struct FormState { } impl FormState { - // MODIFIED constructor + /// 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, @@ -34,8 +36,9 @@ impl FormState { id: 0, // Default to 0, indicating a new or unloaded record profile_name, table_name, - total_count: 0, // Will be fetched after initialization - current_position: 0, // Will be set after count is fetched (e.g., 1 or total_count + 1) + total_count: 0, // Will be fetched after initialization + // FIX: Default to 1. A position of 0 is an invalid state. + current_position: 1, fields, values, current_field: 0, @@ -67,21 +70,20 @@ impl FormState { theme, is_edit_mode, highlight_state, - self.total_count, // MODIFIED: Use self.total_count - self.current_position, // MODIFIED: Use self.current_position + self.total_count, + self.current_position, ); } - // MODIFIED: Reset now also considers table context for counts + /// 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. 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; - // current_position should be set to total_count + 1 for a new entry - // This might be better handled by the logic that calls reset_to_empty - // For now, let's ensure it's consistent with a "new" state. + // Set the position for a new entry. if self.total_count > 0 { self.current_position = self.total_count + 1; } else { @@ -102,10 +104,13 @@ impl FormState { .expect("Invalid current_field index") } - // MODIFIED: Update from a generic HashMap response + /// 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| { @@ -135,6 +140,8 @@ 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; self.current_cursor_pos = 0; diff --git a/client/src/tui/functions/common/form.rs b/client/src/tui/functions/common/form.rs index e556410..8f663c4 100644 --- a/client/src/tui/functions/common/form.rs +++ b/client/src/tui/functions/common/form.rs @@ -124,6 +124,8 @@ pub async fn revert( form_state.table_name ))?; - form_state.update_from_response(&response.data); + // 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 73fac6d..f3993af 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -82,21 +82,13 @@ pub async fn run_ui() -> Result<()> { } } - // Initialize AppState and FormState with table data - let (initial_profile, initial_table, initial_columns) = + let (initial_profile, initial_table, initial_columns_from_service) = UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state) .await .context("Failed to initialize app state and form")?; - // Initialize AppState and FormState with table data - let (initial_profile, initial_table, initial_columns_from_service) = // Renamed for clarity - UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state) - .await - .context("Failed to initialize app state and form")?; + let filtered_columns = filter_user_columns(initial_columns_from_service); - // Filter the columns obtained from the service - let filtered_columns = filter_user_columns(initial_columns_from_service); // Use the correct variable - let mut form_state = FormState::new( initial_profile.clone(), initial_table.clone(), @@ -110,7 +102,6 @@ pub async fn run_ui() -> Result<()> { initial_profile, initial_table ))?; - // Load initial data for the form if form_state.total_count > 0 { if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { event_handler.command_message = format!("Error loading initial data: {}", e); @@ -130,11 +121,9 @@ pub async fn run_ui() -> Result<()> { let mut needs_redraw = true; let mut prev_view_profile_name = app_state.current_view_profile_name.clone(); let mut prev_view_table_name = app_state.current_view_table_name.clone(); + let mut table_just_switched = false; loop { - // =================================================================== - // PROCESS EVENTS & UPDATE STATE - // =================================================================== let position_before_event = form_state.current_position; let mut event_processed = false; if crossterm_event::poll(std::time::Duration::from_millis(1))? { @@ -156,7 +145,6 @@ pub async fn run_ui() -> Result<()> { &mut app_state, ).await; - // --- Handle the outcome of the event --- let mut should_exit = false; match event_outcome_result { Ok(outcome) => match outcome { @@ -205,7 +193,6 @@ pub async fn run_ui() -> Result<()> { } } - // --- Handle async results from other tasks --- match login_result_receiver.try_recv() { Ok(result) => { if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) { @@ -255,11 +242,6 @@ pub async fn run_ui() -> Result<()> { } } - // =================================================================== - // STATE-DEPENDENT LOGIC (NOW RUNS AFTER STATE IS UPDATED) - // =================================================================== - - // --- Determine which view is active --- if let Some(active_view) = buffer_state.get_active_view() { app_state.ui.show_intro = false; app_state.ui.show_login = false; @@ -307,55 +289,92 @@ pub async fn run_ui() -> Result<()> { } } - // --- Handle table change for FormView --- if app_state.ui.show_form { let current_view_profile = app_state.current_view_profile_name.clone(); let current_view_table = app_state.current_view_table_name.clone(); - if prev_view_profile_name != current_view_profile || prev_view_table_name != current_view_table { - if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) { - app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name)); + if prev_view_profile_name != current_view_profile + || prev_view_table_name != current_view_table + { + if let (Some(prof_name), Some(tbl_name)) = + (current_view_profile.as_ref(), current_view_table.as_ref()) + { + app_state.show_loading_dialog( + "Loading Table", + &format!("Fetching data for {}.{}...", prof_name, tbl_name), + ); needs_redraw = true; - match grpc_client.get_table_structure(prof_name.clone(), tbl_name.clone()).await { + match grpc_client + .get_table_structure(prof_name.clone(), tbl_name.clone()) + .await + { Ok(structure_response) => { - let new_columns: Vec = structure_response.columns.iter().map(|c| c.name.clone()).collect(); + let new_columns: Vec = structure_response + .columns + .iter() + .map(|c| c.name.clone()) + .collect(); - // --- START FIX --- - // Apply the same filtering logic here that is used on initial load. let filtered_columns = filter_user_columns(new_columns); - form_state = FormState::new(prof_name.clone(), tbl_name.clone(), filtered_columns); - // --- END FIX --- + form_state = FormState::new( + prof_name.clone(), + tbl_name.clone(), + filtered_columns, + ); - if let Err(e) = UiService::fetch_and_set_table_count(&mut grpc_client, &mut form_state).await { - app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); - } else { - if form_state.total_count > 0 { - if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { - app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); - } else { - app_state.hide_dialog(); - } + if let Err(e) = UiService::fetch_and_set_table_count( + &mut grpc_client, + &mut form_state, + ) + .await + { + app_state.update_dialog_content( + &format!("Error fetching count: {}", e), + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + } else if form_state.total_count > 0 { + if let Err(e) = UiService::load_table_data_by_position( + &mut grpc_client, + &mut form_state, + ) + .await + { + app_state.update_dialog_content( + &format!("Error loading data: {}", e), + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); } else { - form_state.reset_to_empty(); app_state.hide_dialog(); } + } else { + form_state.reset_to_empty(); + app_state.hide_dialog(); } + + prev_view_profile_name = current_view_profile; + prev_view_table_name = current_view_table; + table_just_switched = true; } Err(e) => { - app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); - app_state.current_view_profile_name = prev_view_profile_name.clone(); - app_state.current_view_table_name = prev_view_table_name.clone(); + app_state.update_dialog_content( + &format!("Error fetching table structure: {}", e), + vec!["OK".to_string()], + DialogPurpose::LoginFailed, + ); + app_state.current_view_profile_name = + prev_view_profile_name.clone(); + app_state.current_view_table_name = + prev_view_table_name.clone(); } } } - prev_view_profile_name = current_view_profile; - prev_view_table_name = current_view_table; needs_redraw = true; } } - // --- Handle pending async fetches --- if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { if app_state.ui.show_add_logic { if admin_state.add_logic_state.profile_name == profile_name && @@ -414,28 +433,17 @@ pub async fn run_ui() -> Result<()> { } } - // --- Handle cursor updates based on position changes --- let position_changed = form_state.current_position != position_before_event; let mut position_logic_needs_redraw = false; - if app_state.ui.show_form { + if app_state.ui.show_form && !table_just_switched { if position_changed && !event_handler.is_edit_mode { - let current_input_before_load = form_state.get_current_input(); - let max_cursor_pos_before_load = if !current_input_before_load.is_empty() { current_input_before_load.chars().count() } else { 0 }; - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_before_load); position_logic_needs_redraw = true; - if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 { - form_state.current_position = form_state.total_count + 1; - } else if form_state.total_count == 0 && form_state.current_position > 1 { - form_state.current_position = 1; - } - if form_state.current_position == 0 && form_state.total_count > 0 { - form_state.current_position = 1; - } - - if (form_state.total_count > 0 && form_state.current_position <= form_state.total_count && form_state.current_position > 0) - { + if form_state.current_position > form_state.total_count { + form_state.reset_to_empty(); + event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name); + } else { match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { Ok(load_message) => { if event_handler.command_message.is_empty() || !load_message.starts_with("Error") { @@ -446,27 +454,18 @@ pub async fn run_ui() -> Result<()> { event_handler.command_message = format!("Error loading data: {}", e); } } - } else { - form_state.reset_to_empty(); - event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name); } let current_input_after_load_str = form_state.get_current_input(); let current_input_len_after_load = current_input_after_load_str.chars().count(); - - let max_cursor_pos_for_readonly_after_load = if current_input_len_after_load > 0 { + let max_cursor_pos = if current_input_len_after_load > 0 { current_input_len_after_load.saturating_sub(1) } else { 0 }; + form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos); - if event_handler.is_edit_mode { - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(current_input_len_after_load); - } else { - form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos_for_readonly_after_load); - } - - } else if !position_changed && !event_handler.is_edit_mode && app_state.ui.show_form { + } else if !position_changed && !event_handler.is_edit_mode { let current_input_str = form_state.get_current_input(); let current_input_len = current_input_str.chars().count(); let max_cursor_pos = if current_input_len > 0 { @@ -498,9 +497,6 @@ pub async fn run_ui() -> Result<()> { needs_redraw = true; } - // =================================================================== - // STEP 3: DRAW THE UI - // =================================================================== if event_processed || needs_redraw || position_changed { let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); match current_mode { @@ -543,12 +539,13 @@ pub async fn run_ui() -> Result<()> { needs_redraw = false; } - // --- FPS calculation --- 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 { current_fps = 1.0 / frame_duration.as_secs_f64(); } + + table_just_switched = false; } }