centralizing logic in the formstate

This commit is contained in:
filipriec
2025-06-08 00:00:37 +02:00
parent dc232b2523
commit f9841f2ef3
4 changed files with 98 additions and 91 deletions

View File

@@ -196,7 +196,8 @@ impl UiService {
.await .await
{ {
Ok(response) => { 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!( Ok(format!(
"Loaded entry {}/{} for table {}.{}", "Loaded entry {}/{} for table {}.{}",
form_state.current_position, form_state.current_position,

View File

@@ -23,7 +23,9 @@ pub struct FormState {
} }
impl 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( pub fn new(
profile_name: String, profile_name: String,
table_name: String, table_name: String,
@@ -34,8 +36,9 @@ impl FormState {
id: 0, // Default to 0, indicating a new or unloaded record id: 0, // Default to 0, indicating a new or unloaded record
profile_name, profile_name,
table_name, table_name,
total_count: 0, // Will be fetched after initialization 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) // FIX: Default to 1. A position of 0 is an invalid state.
current_position: 1,
fields, fields,
values, values,
current_field: 0, current_field: 0,
@@ -67,21 +70,20 @@ impl FormState {
theme, theme,
is_edit_mode, is_edit_mode,
highlight_state, highlight_state,
self.total_count, // MODIFIED: Use self.total_count self.total_count,
self.current_position, // MODIFIED: Use self.current_position 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) { pub fn reset_to_empty(&mut self) {
self.id = 0; self.id = 0;
self.values.iter_mut().for_each(|v| v.clear()); self.values.iter_mut().for_each(|v| v.clear());
self.current_field = 0; self.current_field = 0;
self.current_cursor_pos = 0; self.current_cursor_pos = 0;
self.has_unsaved_changes = false; self.has_unsaved_changes = false;
// current_position should be set to total_count + 1 for a new entry // Set the position 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.
if self.total_count > 0 { if self.total_count > 0 {
self.current_position = self.total_count + 1; self.current_position = self.total_count + 1;
} else { } else {
@@ -102,10 +104,13 @@ impl FormState {
.expect("Invalid current_field index") .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( pub fn update_from_response(
&mut self, &mut self,
response_data: &HashMap<String, String>, response_data: &HashMap<String, String>,
// 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. // Create a new vector for the values, ensuring they are in the correct order.
self.values = self.fields.iter().map(|field_from_schema| { self.values = self.fields.iter().map(|field_from_schema| {
@@ -135,6 +140,8 @@ impl FormState {
self.id = 0; self.id = 0;
} }
// FIX: Set the position from the provided parameter.
self.current_position = new_position;
self.has_unsaved_changes = false; self.has_unsaved_changes = false;
self.current_field = 0; self.current_field = 0;
self.current_cursor_pos = 0; self.current_cursor_pos = 0;

View File

@@ -124,6 +124,8 @@ pub async fn revert(
form_state.table_name 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()) Ok("Changes discarded, reloaded last saved version".to_string())
} }

View File

@@ -82,21 +82,13 @@ pub async fn run_ui() -> Result<()> {
} }
} }
// Initialize AppState and FormState with table data let (initial_profile, initial_table, initial_columns_from_service) =
let (initial_profile, initial_table, initial_columns) =
UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state) UiService::initialize_app_state_and_form(&mut grpc_client, &mut app_state)
.await .await
.context("Failed to initialize app state and form")?; .context("Failed to initialize app state and form")?;
// Initialize AppState and FormState with table data let filtered_columns = filter_user_columns(initial_columns_from_service);
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")?;
// 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( let mut form_state = FormState::new(
initial_profile.clone(), initial_profile.clone(),
initial_table.clone(), initial_table.clone(),
@@ -110,7 +102,6 @@ pub async fn run_ui() -> Result<()> {
initial_profile, initial_table initial_profile, initial_table
))?; ))?;
// Load initial data for the form
if form_state.total_count > 0 { if form_state.total_count > 0 {
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { 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); 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 needs_redraw = true;
let mut prev_view_profile_name = app_state.current_view_profile_name.clone(); 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 prev_view_table_name = app_state.current_view_table_name.clone();
let mut table_just_switched = false;
loop { loop {
// ===================================================================
// PROCESS EVENTS & UPDATE STATE
// ===================================================================
let position_before_event = form_state.current_position; let position_before_event = form_state.current_position;
let mut event_processed = false; let mut event_processed = false;
if crossterm_event::poll(std::time::Duration::from_millis(1))? { if crossterm_event::poll(std::time::Duration::from_millis(1))? {
@@ -156,7 +145,6 @@ pub async fn run_ui() -> Result<()> {
&mut app_state, &mut app_state,
).await; ).await;
// --- Handle the outcome of the event ---
let mut should_exit = false; let mut should_exit = false;
match event_outcome_result { match event_outcome_result {
Ok(outcome) => match outcome { 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() { match login_result_receiver.try_recv() {
Ok(result) => { Ok(result) => {
if login::handle_login_result(result, &mut app_state, &mut auth_state, &mut login_state) { 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() { if let Some(active_view) = buffer_state.get_active_view() {
app_state.ui.show_intro = false; app_state.ui.show_intro = false;
app_state.ui.show_login = 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 { if app_state.ui.show_form {
let current_view_profile = app_state.current_view_profile_name.clone(); let current_view_profile = app_state.current_view_profile_name.clone();
let current_view_table = app_state.current_view_table_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 prev_view_profile_name != current_view_profile
if let (Some(prof_name), Some(tbl_name)) = (current_view_profile.as_ref(), current_view_table.as_ref()) { || prev_view_table_name != current_view_table
app_state.show_loading_dialog("Loading Table", &format!("Fetching data for {}.{}...", prof_name, tbl_name)); {
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; 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) => { Ok(structure_response) => {
let new_columns: Vec<String> = structure_response.columns.iter().map(|c| c.name.clone()).collect(); let new_columns: Vec<String> = 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); let filtered_columns = filter_user_columns(new_columns);
form_state = FormState::new(prof_name.clone(), tbl_name.clone(), filtered_columns); form_state = FormState::new(
// --- END FIX --- 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 { if let Err(e) = UiService::fetch_and_set_table_count(
app_state.update_dialog_content(&format!("Error fetching count: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); &mut grpc_client,
} else { &mut form_state,
if form_state.total_count > 0 { )
if let Err(e) = UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { .await
app_state.update_dialog_content(&format!("Error loading data: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); {
} else { app_state.update_dialog_content(
app_state.hide_dialog(); &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 { } else {
form_state.reset_to_empty();
app_state.hide_dialog(); 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) => { Err(e) => {
app_state.update_dialog_content(&format!("Error fetching table structure: {}", e), vec!["OK".to_string()], DialogPurpose::LoginFailed); app_state.update_dialog_content(
app_state.current_view_profile_name = prev_view_profile_name.clone(); &format!("Error fetching table structure: {}", e),
app_state.current_view_table_name = prev_view_table_name.clone(); 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; needs_redraw = true;
} }
} }
// --- Handle pending async fetches ---
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() { if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
if app_state.ui.show_add_logic { if app_state.ui.show_add_logic {
if admin_state.add_logic_state.profile_name == profile_name && 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 position_changed = form_state.current_position != position_before_event;
let mut position_logic_needs_redraw = false; 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 { 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; position_logic_needs_redraw = true;
if form_state.total_count > 0 && form_state.current_position > form_state.total_count + 1 { if form_state.current_position > form_state.total_count {
form_state.current_position = form_state.total_count + 1; form_state.reset_to_empty();
} else if form_state.total_count == 0 && form_state.current_position > 1 { event_handler.command_message = format!("New entry for {}.{}", form_state.profile_name, form_state.table_name);
form_state.current_position = 1; } else {
}
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)
{
match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await { match UiService::load_table_data_by_position(&mut grpc_client, &mut form_state).await {
Ok(load_message) => { Ok(load_message) => {
if event_handler.command_message.is_empty() || !load_message.starts_with("Error") { 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); 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_after_load_str = form_state.get_current_input();
let current_input_len_after_load = current_input_after_load_str.chars().count(); let current_input_len_after_load = current_input_after_load_str.chars().count();
let max_cursor_pos = if current_input_len_after_load > 0 {
let max_cursor_pos_for_readonly_after_load = if current_input_len_after_load > 0 {
current_input_len_after_load.saturating_sub(1) current_input_len_after_load.saturating_sub(1)
} else { } else {
0 0
}; };
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
if event_handler.is_edit_mode { } else if !position_changed && !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 {
let current_input_str = form_state.get_current_input(); let current_input_str = form_state.get_current_input();
let current_input_len = current_input_str.chars().count(); let current_input_len = current_input_str.chars().count();
let max_cursor_pos = if current_input_len > 0 { let max_cursor_pos = if current_input_len > 0 {
@@ -498,9 +497,6 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = true; needs_redraw = true;
} }
// ===================================================================
// STEP 3: DRAW THE UI
// ===================================================================
if event_processed || needs_redraw || position_changed { if event_processed || needs_redraw || position_changed {
let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state); let current_mode = ModeManager::derive_mode(&app_state, &event_handler, &admin_state);
match current_mode { match current_mode {
@@ -543,12 +539,13 @@ pub async fn run_ui() -> Result<()> {
needs_redraw = false; needs_redraw = false;
} }
// --- FPS calculation ---
let now = Instant::now(); let now = Instant::now();
let frame_duration = now.duration_since(last_frame_time); let frame_duration = now.duration_since(last_frame_time);
last_frame_time = now; last_frame_time = now;
if frame_duration.as_secs_f64() > 1e-6 { if frame_duration.as_secs_f64() > 1e-6 {
current_fps = 1.0 / frame_duration.as_secs_f64(); current_fps = 1.0 / frame_duration.as_secs_f64();
} }
table_just_switched = false;
} }
} }