diff --git a/.gitignore b/.gitignore index 0388179..ecc4e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .env /tantivy_indexes +server/tantivy_indexes diff --git a/client/src/components/common/autocomplete.rs b/client/src/components/common/autocomplete.rs index 856c54a..dd3c6e2 100644 --- a/client/src/components/common/autocomplete.rs +++ b/client/src/components/common/autocomplete.rs @@ -1,5 +1,8 @@ // src/components/common/autocomplete.rs +use common::proto::multieko2::search::search_response::Hit; +use serde::Deserialize; +// Keep all existing imports use crate::config::colors::themes::Theme; use ratatui::{ layout::Rect, @@ -9,6 +12,11 @@ use ratatui::{ }; use unicode_width::UnicodeWidthStr; +// Helper struct for parsing the JSON inside a Hit +#[derive(Deserialize)] +struct SuggestionContent { + name: String, +} /// Renders an opaque dropdown list for autocomplete suggestions. pub fn render_autocomplete_dropdown( f: &mut Frame, @@ -88,3 +96,84 @@ pub fn render_autocomplete_dropdown( f.render_stateful_widget(list, dropdown_area, &mut profile_list_state); } +// --- NEW FUNCTION FOR RICH SUGGESTIONS --- +/// Renders an opaque dropdown list for rich `Hit`-based suggestions. +pub fn render_rich_autocomplete_dropdown( + f: &mut Frame, + input_rect: Rect, + frame_area: Rect, + theme: &Theme, + suggestions: &[Hit], // <-- Accepts &[Hit] + selected_index: Option, +) { + if suggestions.is_empty() { + return; + } + + // --- Get display names from Hits, with a fallback for parsing errors --- + let display_names: Vec = suggestions + .iter() + .map(|hit| { + serde_json::from_str::(&hit.content_json) + .map(|content| content.name) + .unwrap_or_else(|_| format!("ID: {}", hit.id)) // Fallback display + }) + .collect(); + + // --- Calculate Dropdown Size & Position --- + let max_suggestion_width = + display_names.iter().map(|s| s.width()).max().unwrap_or(0) as u16; + let horizontal_padding: u16 = 2; + let dropdown_width = (max_suggestion_width + horizontal_padding).max(10); + let dropdown_height = (suggestions.len() as u16).min(5); + + let mut dropdown_area = Rect { + x: input_rect.x, + y: input_rect.y + 1, + width: dropdown_width, + height: dropdown_height, + }; + + // --- Clamping Logic (prevent rendering off-screen) --- + if dropdown_area.bottom() > frame_area.height { + dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); + } + if dropdown_area.right() > frame_area.width { + dropdown_area.x = frame_area.width.saturating_sub(dropdown_width); + } + dropdown_area.x = dropdown_area.x.max(0); + dropdown_area.y = dropdown_area.y.max(0); + + // --- Rendering Logic --- + let background_block = + Block::default().style(Style::default().bg(Color::DarkGray)); + f.render_widget(background_block, dropdown_area); + + let items: Vec = display_names + .iter() + .enumerate() + .map(|(i, s)| { + let is_selected = selected_index == Some(i); + let s_width = s.width() as u16; + let padding_needed = dropdown_width.saturating_sub(s_width); + let padded_s = + format!("{}{}", s, " ".repeat(padding_needed as usize)); + + ListItem::new(padded_s).style(if is_selected { + Style::default() + .fg(theme.bg) + .bg(theme.highlight) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.fg).bg(Color::DarkGray) + }) + }) + .collect(); + + let list = List::new(items); + let mut list_state = ListState::default(); + list_state.select(selected_index); + + f.render_stateful_widget(list, dropdown_area, &mut list_state); +} + diff --git a/client/src/components/form/form.rs b/client/src/components/form/form.rs index bafa5d2..ebf8c7f 100644 --- a/client/src/components/form/form.rs +++ b/client/src/components/form/form.rs @@ -78,16 +78,34 @@ pub fn render_form( // --- NEW: RENDER AUTOCOMPLETE --- if form_state.autocomplete_active { - if let Some(suggestions) = form_state.get_suggestions() { - if let Some(active_rect) = active_field_rect { - if !suggestions.is_empty() { + // Use the Rect of the active field that render_canvas found for us. + if let Some(active_rect) = active_field_rect { + let selected_index = form_state.get_selected_suggestion_index(); + + // THE DECIDER LOGIC: + // 1. Check for rich suggestions first. + if let Some(rich_suggestions) = form_state.get_rich_suggestions() { + if !rich_suggestions.is_empty() { + autocomplete::render_rich_autocomplete_dropdown( + f, + active_rect, + f.area(), // Use f.area() for clamping, not f.size() + theme, + rich_suggestions, + selected_index, + ); + } + } + // 2. Fallback to simple suggestions if rich ones aren't available. + else if let Some(simple_suggestions) = form_state.get_suggestions() { + if !simple_suggestions.is_empty() { autocomplete::render_autocomplete_dropdown( f, active_rect, f.area(), theme, - suggestions, - form_state.get_selected_suggestion_index(), + simple_suggestions, + selected_index, ); } } diff --git a/client/src/modes/canvas/edit.rs b/client/src/modes/canvas/edit.rs index 3421b89..fab50db 100644 --- a/client/src/modes/canvas/edit.rs +++ b/client/src/modes/canvas/edit.rs @@ -1,16 +1,19 @@ // src/modes/canvas/edit.rs use crate::config::binds::config::Config; +use crate::functions::modes::edit::{ + add_logic_e, add_table_e, auth_e, form_e, +}; +use crate::modes::handlers::event::EventOutcome; use crate::services::grpc_client::GrpcClient; +use crate::state::app::state::AppState; +use crate::state::pages::admin::AdminState; use crate::state::pages::{ auth::{LoginState, RegisterState}, canvas_state::CanvasState, + form::FormState, }; -use crate::state::pages::form::FormState; -use crate::state::pages::admin::AdminState; -use crate::modes::handlers::event::EventOutcome; -use crate::functions::modes::edit::{add_logic_e, auth_e, form_e, add_table_e}; -use crate::state::app::state::AppState; use anyhow::Result; +use common::proto::multieko2::search::search_response::Hit; use crossterm::event::KeyEvent; use tracing::debug; @@ -33,20 +36,25 @@ pub async fn handle_edit_event( grpc_client: &mut GrpcClient, app_state: &AppState, ) -> Result { -if app_state.ui.show_form && form_state.autocomplete_active { - if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) { + if app_state.ui.show_form && form_state.autocomplete_active { + if let Some(action) = + config.get_edit_action_for_key(key.code, key.modifiers) + { match action { "suggestion_down" => { if !form_state.autocomplete_suggestions.is_empty() { - let current = form_state.selected_suggestion_index.unwrap_or(0); - let next = (current + 1) % form_state.autocomplete_suggestions.len(); + let current = + form_state.selected_suggestion_index.unwrap_or(0); + let next = (current + 1) + % form_state.autocomplete_suggestions.len(); form_state.selected_suggestion_index = Some(next); } return Ok(EditEventOutcome::Message(String::new())); } "suggestion_up" => { if !form_state.autocomplete_suggestions.is_empty() { - let current = form_state.selected_suggestion_index.unwrap_or(0); + let current = + form_state.selected_suggestion_index.unwrap_or(0); let prev = if current == 0 { form_state.autocomplete_suggestions.len() - 1 } else { @@ -58,19 +66,31 @@ if app_state.ui.show_form && form_state.autocomplete_active { } "exit" => { form_state.deactivate_autocomplete(); - return Ok(EditEventOutcome::Message("Autocomplete cancelled".to_string())); + return Ok(EditEventOutcome::Message( + "Autocomplete cancelled".to_string(), + )); } "enter_decider" => { - if let Some(selected_idx) = form_state.selected_suggestion_index { - if let Some(selection) = form_state.autocomplete_suggestions.get(selected_idx).cloned() { - let current_input = form_state.get_current_input_mut(); - *current_input = selection; + if let Some(selected_idx) = + form_state.selected_suggestion_index + { + if let Some(selection) = form_state + .autocomplete_suggestions + .get(selected_idx) + .cloned() + { + let current_input = + form_state.get_current_input_mut(); + // Use the ID from the Hit struct for the field value + *current_input = selection.id.to_string(); let new_cursor_pos = current_input.len(); form_state.set_current_cursor_pos(new_cursor_pos); *ideal_cursor_column = new_cursor_pos; form_state.deactivate_autocomplete(); form_state.set_has_unsaved_changes(true); - return Ok(EditEventOutcome::Message("Selection made".to_string())); + return Ok(EditEventOutcome::Message( + "Selection made".to_string(), + )); } } // If no selection, fall through to default behavior @@ -80,7 +100,7 @@ if app_state.ui.show_form && form_state.autocomplete_active { } } } - + if let Some("enter_command_mode") = config.get_action_for_key_in_mode( &config.keybindings.global, key.code, @@ -91,171 +111,410 @@ if app_state.ui.show_form && form_state.autocomplete_active { )); } - if let Some(action) = config.get_action_for_key_in_mode( - &config.keybindings.common, - key.code, - key.modifiers, - ).as_deref() { + if let Some(action) = config + .get_action_for_key_in_mode( + &config.keybindings.common, + key.code, + key.modifiers, + ) + .as_deref() + { if matches!(action, "save" | "revert") { let message_string: String = if app_state.ui.show_login { - auth_e::execute_common_action(action, login_state, grpc_client, current_position, total_count).await? + auth_e::execute_common_action( + action, + login_state, + grpc_client, + current_position, + total_count, + ) + .await? } else if app_state.ui.show_register { - auth_e::execute_common_action(action, register_state, grpc_client, current_position, total_count).await? + auth_e::execute_common_action( + action, + register_state, + grpc_client, + current_position, + total_count, + ) + .await? } else if app_state.ui.show_add_table { - format!("Action '{}' not implemented for Add Table in edit mode.", action) + format!( + "Action '{}' not implemented for Add Table in edit mode.", + action + ) } else if app_state.ui.show_add_logic { - format!("Action '{}' not implemented for Add Logic in edit mode.", action) + format!( + "Action '{}' not implemented for Add Logic in edit mode.", + action + ) } else { - let outcome = form_e::execute_common_action(action, form_state, grpc_client).await?; + let outcome = + form_e::execute_common_action(action, form_state, grpc_client) + .await?; match outcome { EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg, - _ => format!("Unexpected outcome from common action: {:?}", outcome), + _ => format!( + "Unexpected outcome from common action: {:?}", + outcome + ), } }; return Ok(EditEventOutcome::Message(message_string)); } } - if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers).as_deref() { - tracing::info!("[Handler] `handle_edit_event` received action: '{}'", action_str); + if let Some(action_str) = + config.get_edit_action_for_key(key.code, key.modifiers).as_deref() + { + tracing::info!( + "[Handler] `handle_edit_event` received action: '{}'", + action_str + ); // --- MANUAL AUTOCOMPLETE TRIGGER --- if action_str == "trigger_autocomplete" { - tracing::info!("[Handler] Action is 'trigger_autocomplete'. Checking conditions..."); // <-- ADD THIS + tracing::info!("[Handler] Action is 'trigger_autocomplete'. Checking conditions..."); if app_state.ui.show_form { - tracing::info!("[Handler] In form view. Checking field..."); // <-- ADD THIS if let Some(field_def) = form_state.fields.get(form_state.current_field) { if field_def.is_link { - tracing::info!("[Handler] Field '{}' is a link. Activating autocomplete.", field_def.display_name); // <-- ADD THIS - form_state.autocomplete_active = true; - form_state.selected_suggestion_index = Some(0); - form_state.autocomplete_suggestions = vec![ - "Hardcoded Supplier A".to_string(), - "Hardcoded Supplier B".to_string(), - "Hardcoded Company C".to_string(), - ]; - return Ok(EditEventOutcome::Message("Autocomplete triggered".to_string())); + // Use our new field to get the table to search + if let Some(target_table) = &field_def.link_target_table { + tracing::info!( + "[Handler] Field '{}' is a link to table '{}'. Triggering search.", + field_def.display_name, + target_table + ); + + // Set loading state and activate autocomplete UI + form_state.autocomplete_loading = true; + form_state.autocomplete_active = true; + form_state.autocomplete_suggestions.clear(); + form_state.selected_suggestion_index = None; + + let query = form_state.get_current_input().to_string(); + let table_to_search = target_table.clone(); + + // Perform the gRPC call asynchronously + match grpc_client.search_table(table_to_search, query).await { + Ok(response) => { + form_state.autocomplete_suggestions = response.hits; + if !form_state.autocomplete_suggestions.is_empty() { + form_state.selected_suggestion_index = Some(0); + } + form_state.autocomplete_loading = false; // Turn off loading + return Ok(EditEventOutcome::Message(format!( + "Found {} suggestions.", + form_state.autocomplete_suggestions.len() + ))); + } + Err(e) => { + // Handle errors gracefully + form_state.autocomplete_loading = false; + form_state.deactivate_autocomplete(); // Close UI on error + let error_msg = format!("Search failed: {}", e); + tracing::error!("[Handler] {}", error_msg); + return Ok(EditEventOutcome::Message(error_msg)); + } + } + } else { + let msg = "Field is a link, but target table is not defined.".to_string(); + tracing::error!("[Handler] {}", msg); + return Ok(EditEventOutcome::Message(msg)); + } } else { - tracing::error!("[Handler] Field '{}' is NOT a link.", field_def.display_name); // <-- ADD THIS + let msg = format!("Field '{}' is not a linkable field.", field_def.display_name); + tracing::error!("[Handler] {}", msg); + return Ok(EditEventOutcome::Message(msg)); } } - } else { - tracing::error!("[Handler] Not in form view. Cannot trigger autocomplete."); // <-- ADD THIS } - return Ok(EditEventOutcome::Message("Not a linkable field".to_string())); + // Fallback message if not in a form or something went wrong + return Ok(EditEventOutcome::Message("Autocomplete not available here.".to_string())); } // --- END OF NEW LOGIC --- - + if action_str == "enter_decider" { let effective_action = if app_state.ui.show_register && register_state.in_suggestion_mode - && register_state.current_field() == 4 { + && register_state.current_field() == 4 + { "select_suggestion" } else if app_state.ui.show_add_logic - && admin_state.add_logic_state.in_target_column_suggestion_mode - && admin_state.add_logic_state.current_field() == 1 { + && admin_state + .add_logic_state + .in_target_column_suggestion_mode + && admin_state.add_logic_state.current_field() == 1 + { "select_suggestion" } else { "next_field" }; let msg = if app_state.ui.show_login { - auth_e::execute_edit_action(effective_action, key, login_state, ideal_cursor_column).await? + auth_e::execute_edit_action( + effective_action, + key, + login_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action(effective_action, key, &mut admin_state.add_table_state, ideal_cursor_column).await? + add_table_e::execute_edit_action( + effective_action, + key, + &mut admin_state.add_table_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action(effective_action, key, &mut admin_state.add_logic_state, ideal_cursor_column).await? + add_logic_e::execute_edit_action( + effective_action, + key, + &mut admin_state.add_logic_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_register { - auth_e::execute_edit_action(effective_action, key, register_state, ideal_cursor_column).await? + auth_e::execute_edit_action( + effective_action, + key, + register_state, + ideal_cursor_column, + ) + .await? } else { - form_e::execute_edit_action(effective_action, key, form_state, ideal_cursor_column).await? + form_e::execute_edit_action( + effective_action, + key, + form_state, + ideal_cursor_column, + ) + .await? }; return Ok(EditEventOutcome::Message(msg)); } if action_str == "exit" { if app_state.ui.show_register && register_state.in_suggestion_mode { - let msg = auth_e::execute_edit_action("exit_suggestion_mode", key, register_state, ideal_cursor_column).await?; + let msg = auth_e::execute_edit_action( + "exit_suggestion_mode", + key, + register_state, + ideal_cursor_column, + ) + .await?; return Ok(EditEventOutcome::Message(msg)); - } else if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode { - admin_state.add_logic_state.in_target_column_suggestion_mode = false; - admin_state.add_logic_state.show_target_column_suggestions = false; - admin_state.add_logic_state.selected_target_column_suggestion_index = None; - return Ok(EditEventOutcome::Message("Exited column suggestions".to_string())); + } else if app_state.ui.show_add_logic + && admin_state + .add_logic_state + .in_target_column_suggestion_mode + { + admin_state + .add_logic_state + .in_target_column_suggestion_mode = false; + admin_state + .add_logic_state + .show_target_column_suggestions = false; + admin_state + .add_logic_state + .selected_target_column_suggestion_index = None; + return Ok(EditEventOutcome::Message( + "Exited column suggestions".to_string(), + )); } else { return Ok(EditEventOutcome::ExitEditMode); } } - if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { + if app_state.ui.show_add_logic + && admin_state.add_logic_state.current_field() == 1 + { if action_str == "suggestion_down" { - if !admin_state.add_logic_state.in_target_column_suggestion_mode { - if let Some(profile_name) = admin_state.add_logic_state.profile_name.clone().into() { - if let Some(table_name) = admin_state.add_logic_state.selected_table_name.clone() { + if !admin_state + .add_logic_state + .in_target_column_suggestion_mode + { + if let Some(profile_name) = + admin_state.add_logic_state.profile_name.clone().into() + { + if let Some(table_name) = admin_state + .add_logic_state + .selected_table_name + .clone() + { debug!("Fetching table structure for autocomplete: Profile='{}', Table='{}'", profile_name, table_name); - match grpc_client.get_table_structure(profile_name, table_name).await { + match grpc_client + .get_table_structure(profile_name, table_name) + .await + { Ok(ts_response) => { - admin_state.add_logic_state.table_columns_for_suggestions = - ts_response.columns.into_iter().map(|c| c.name).collect(); - admin_state.add_logic_state.update_target_column_suggestions(); - if !admin_state.add_logic_state.target_column_suggestions.is_empty() { - admin_state.add_logic_state.in_target_column_suggestion_mode = true; - return Ok(EditEventOutcome::Message("Column suggestions shown".to_string())); + admin_state + .add_logic_state + .table_columns_for_suggestions = + ts_response + .columns + .into_iter() + .map(|c| c.name) + .collect(); + admin_state + .add_logic_state + .update_target_column_suggestions(); + if !admin_state + .add_logic_state + .target_column_suggestions + .is_empty() + { + admin_state + .add_logic_state + .in_target_column_suggestion_mode = + true; + return Ok(EditEventOutcome::Message( + "Column suggestions shown" + .to_string(), + )); } else { - return Ok(EditEventOutcome::Message("No column suggestions for current input".to_string())); + return Ok(EditEventOutcome::Message( + "No column suggestions for current input" + .to_string(), + )); } } Err(e) => { - debug!("Error fetching table structure: {}", e); - admin_state.add_logic_state.table_columns_for_suggestions.clear(); - admin_state.add_logic_state.update_target_column_suggestions(); - return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", e))); + debug!( + "Error fetching table structure: {}", + e + ); + admin_state + .add_logic_state + .table_columns_for_suggestions + .clear(); + admin_state + .add_logic_state + .update_target_column_suggestions(); + return Ok(EditEventOutcome::Message( + format!("Error fetching columns: {}", e), + )); } } } else { - return Ok(EditEventOutcome::Message("No table selected for column suggestions".to_string())); + return Ok(EditEventOutcome::Message( + "No table selected for column suggestions" + .to_string(), + )); } } else { - return Ok(EditEventOutcome::Message("Profile name missing for column suggestions".to_string())); + return Ok(EditEventOutcome::Message( + "Profile name missing for column suggestions" + .to_string(), + )); } } else { - let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?; + let msg = add_logic_e::execute_edit_action( + action_str, + key, + &mut admin_state.add_logic_state, + ideal_cursor_column, + ) + .await?; return Ok(EditEventOutcome::Message(msg)); } - } else if admin_state.add_logic_state.in_target_column_suggestion_mode && action_str == "suggestion_up" { - let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?; + } else if admin_state + .add_logic_state + .in_target_column_suggestion_mode + && action_str == "suggestion_up" + { + let msg = add_logic_e::execute_edit_action( + action_str, + key, + &mut admin_state.add_logic_state, + ideal_cursor_column, + ) + .await?; return Ok(EditEventOutcome::Message(msg)); } } if app_state.ui.show_register && register_state.current_field() == 4 { - if !register_state.in_suggestion_mode && action_str == "suggestion_down" { + if !register_state.in_suggestion_mode + && action_str == "suggestion_down" + { register_state.update_role_suggestions(); if !register_state.role_suggestions.is_empty() { register_state.in_suggestion_mode = true; - return Ok(EditEventOutcome::Message("Role suggestions shown".to_string())); + return Ok(EditEventOutcome::Message( + "Role suggestions shown".to_string(), + )); } } - if register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up") { - let msg = auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?; + if register_state.in_suggestion_mode + && matches!(action_str, "suggestion_down" | "suggestion_up") + { + let msg = auth_e::execute_edit_action( + action_str, + key, + register_state, + ideal_cursor_column, + ) + .await?; return Ok(EditEventOutcome::Message(msg)); } } let msg = if app_state.ui.show_login { - auth_e::execute_edit_action(action_str, key, login_state, ideal_cursor_column).await? + auth_e::execute_edit_action( + action_str, + key, + login_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action(action_str, key, &mut admin_state.add_table_state, ideal_cursor_column).await? + add_table_e::execute_edit_action( + action_str, + key, + &mut admin_state.add_table_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_add_logic { - if !(admin_state.add_logic_state.in_target_column_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) { - add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await? - } else { String::new() } + if !(admin_state + .add_logic_state + .in_target_column_suggestion_mode + && matches!(action_str, "suggestion_down" | "suggestion_up")) + { + add_logic_e::execute_edit_action( + action_str, + key, + &mut admin_state.add_logic_state, + ideal_cursor_column, + ) + .await? + } else { + String::new() + } } else if app_state.ui.show_register { - if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) { - auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await? - } else { String::new() } + if !(register_state.in_suggestion_mode + && matches!(action_str, "suggestion_down" | "suggestion_up")) + { + auth_e::execute_edit_action( + action_str, + key, + register_state, + ideal_cursor_column, + ) + .await? + } else { + String::new() + } } else { - form_e::execute_edit_action(action_str, key, form_state, ideal_cursor_column).await? + form_e::execute_edit_action( + action_str, + key, + form_state, + ideal_cursor_column, + ) + .await? }; return Ok(EditEventOutcome::Message(msg)); } @@ -267,29 +526,65 @@ if app_state.ui.show_form && form_state.autocomplete_active { register_state.selected_suggestion_index = None; exited_suggestion_mode_for_typing = true; } - if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode { + if app_state.ui.show_add_logic + && admin_state.add_logic_state.in_target_column_suggestion_mode + { admin_state.add_logic_state.in_target_column_suggestion_mode = false; admin_state.add_logic_state.show_target_column_suggestions = false; - admin_state.add_logic_state.selected_target_column_suggestion_index = None; + admin_state + .add_logic_state + .selected_target_column_suggestion_index = None; exited_suggestion_mode_for_typing = true; } let mut char_insert_msg = if app_state.ui.show_login { - auth_e::execute_edit_action("insert_char", key, login_state, ideal_cursor_column).await? + auth_e::execute_edit_action( + "insert_char", + key, + login_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_add_table { - add_table_e::execute_edit_action("insert_char", key, &mut admin_state.add_table_state, ideal_cursor_column).await? + add_table_e::execute_edit_action( + "insert_char", + key, + &mut admin_state.add_table_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_add_logic { - add_logic_e::execute_edit_action("insert_char", key, &mut admin_state.add_logic_state, ideal_cursor_column).await? + add_logic_e::execute_edit_action( + "insert_char", + key, + &mut admin_state.add_logic_state, + ideal_cursor_column, + ) + .await? } else if app_state.ui.show_register { - auth_e::execute_edit_action("insert_char", key, register_state, ideal_cursor_column).await? + auth_e::execute_edit_action( + "insert_char", + key, + register_state, + ideal_cursor_column, + ) + .await? } else { - form_e::execute_edit_action("insert_char", key, form_state, ideal_cursor_column).await? + form_e::execute_edit_action( + "insert_char", + key, + form_state, + ideal_cursor_column, + ) + .await? }; if app_state.ui.show_register && register_state.current_field() == 4 { register_state.update_role_suggestions(); } - if app_state.ui.show_add_logic && admin_state.add_logic_state.current_field() == 1 { + if app_state.ui.show_add_logic + && admin_state.add_logic_state.current_field() == 1 + { admin_state.add_logic_state.update_target_column_suggestions(); } diff --git a/client/src/state/pages/canvas_state.rs b/client/src/state/pages/canvas_state.rs index 5c6e85c..4fe02fb 100644 --- a/client/src/state/pages/canvas_state.rs +++ b/client/src/state/pages/canvas_state.rs @@ -1,4 +1,6 @@ -// src/state/canvas_state.rs +// src/state/pages/canvas_state.rs + +use common::proto::multieko2::search::search_response::Hit; pub trait CanvasState { fn current_field(&self) -> usize; @@ -16,4 +18,7 @@ pub trait CanvasState { // --- Autocomplete Support --- fn get_suggestions(&self) -> Option<&[String]>; fn get_selected_suggestion_index(&self) -> Option; + fn get_rich_suggestions(&self) -> Option<&[Hit]> { + None + } } diff --git a/client/src/state/pages/form.rs b/client/src/state/pages/form.rs index 6cd745b..112119d 100644 --- a/client/src/state/pages/form.rs +++ b/client/src/state/pages/form.rs @@ -3,6 +3,7 @@ use crate::config::colors::themes::Theme; use crate::state::app::highlight::HighlightState; use crate::state::pages::canvas_state::CanvasState; +use common::proto::multieko2::search::search_response::Hit; // Import Hit use ratatui::layout::Rect; use ratatui::Frame; use std::collections::HashMap; @@ -12,7 +13,8 @@ use std::collections::HashMap; pub struct FieldDefinition { pub display_name: String, pub data_key: String, - pub is_link: bool, // --- NEW --- To identify FK fields + pub is_link: bool, + pub link_target_table: Option, } #[derive(Clone)] @@ -28,10 +30,11 @@ pub struct FormState { pub has_unsaved_changes: bool, pub current_cursor_pos: usize, - // --- NEW AUTOCOMPLETE STATE --- + // --- MODIFIED AUTOCOMPLETE STATE --- pub autocomplete_active: bool, - pub autocomplete_suggestions: Vec, + pub autocomplete_suggestions: Vec, // Changed to use the Hit struct pub selected_suggestion_index: Option, + pub autocomplete_loading: bool, // To show a loading indicator } impl FormState { @@ -56,6 +59,7 @@ impl FormState { autocomplete_active: false, autocomplete_suggestions: Vec::new(), selected_suggestion_index: None, + autocomplete_loading: false, // Initialize loading state } } @@ -164,6 +168,7 @@ impl FormState { self.autocomplete_active = false; self.autocomplete_suggestions.clear(); self.selected_suggestion_index = None; + self.autocomplete_loading = false; } } @@ -216,7 +221,14 @@ impl CanvasState for FormState { } // --- MODIFIED: Implement autocomplete trait methods --- + + /// Returns None because this state uses rich suggestions. fn get_suggestions(&self) -> Option<&[String]> { + None + } + + /// Returns rich suggestions. + fn get_rich_suggestions(&self) -> Option<&[Hit]> { if self.autocomplete_active { Some(&self.autocomplete_suggestions) } else { diff --git a/client/src/ui/handlers/ui.rs b/client/src/ui/handlers/ui.rs index acbd02b..6b320f1 100644 --- a/client/src/ui/handlers/ui.rs +++ b/client/src/ui/handlers/ui.rs @@ -98,6 +98,7 @@ pub async fn run_ui() -> Result<()> { display_name: col_name.clone(), data_key: col_name, is_link: false, + link_target_table: None, }) .collect(); @@ -347,19 +348,18 @@ pub async fn run_ui() -> Result<()> { .map(|c| c.name.clone()) .collect(); - // 1. Process regular columns first, filtering out FKs let mut field_definitions: Vec = filter_user_columns(all_columns) .into_iter() - .filter(|col_name| !col_name.ends_with("_id")) // Exclude FKs + .filter(|col_name| !col_name.ends_with("_id")) .map(|col_name| FieldDefinition { display_name: col_name.clone(), data_key: col_name, - is_link: false, // Regular fields are not links + is_link: false, + link_target_table: None, // Regular fields have no target }) .collect(); - // 2. Process linked tables to create the correct labels let linked_tables: Vec = app_state .profile_tree .profiles @@ -375,19 +375,22 @@ pub async fn run_ui() -> Result<()> { .split_once('_') .map_or(linked_table_name.as_str(), |(_, rest)| rest); let data_key = format!("{}_id", base_name); - let display_name = linked_table_name; + let display_name = linked_table_name.clone(); // Clone for use below field_definitions.push(FieldDefinition { display_name, data_key, - is_link: true, // These fields ARE links + is_link: true, + // --- POPULATE THE NEW FIELD --- + link_target_table: Some(linked_table_name), }); } // --- END OF MODIFIED LOGIC --- + form_state = FormState::new( prof_name.clone(), tbl_name.clone(), - field_definitions, + field_definitions, // This now contains the complete definitions ); if let Err(e) = UiService::fetch_and_set_table_count(