suggestions in the dropdown menu now works amazingly well

This commit is contained in:
filipriec
2025-06-15 23:11:27 +02:00
parent 0e69df8282
commit 512e7fb9e7
7 changed files with 541 additions and 118 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target /target
.env .env
/tantivy_indexes /tantivy_indexes
server/tantivy_indexes

View File

@@ -1,5 +1,8 @@
// src/components/common/autocomplete.rs // 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 crate::config::colors::themes::Theme;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
@@ -9,6 +12,11 @@ use ratatui::{
}; };
use unicode_width::UnicodeWidthStr; 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. /// Renders an opaque dropdown list for autocomplete suggestions.
pub fn render_autocomplete_dropdown( pub fn render_autocomplete_dropdown(
f: &mut Frame, f: &mut Frame,
@@ -88,3 +96,84 @@ pub fn render_autocomplete_dropdown(
f.render_stateful_widget(list, dropdown_area, &mut profile_list_state); 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<usize>,
) {
if suggestions.is_empty() {
return;
}
// --- Get display names from Hits, with a fallback for parsing errors ---
let display_names: Vec<String> = suggestions
.iter()
.map(|hit| {
serde_json::from_str::<SuggestionContent>(&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<ListItem> = 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);
}

View File

@@ -78,16 +78,34 @@ pub fn render_form(
// --- NEW: RENDER AUTOCOMPLETE --- // --- NEW: RENDER AUTOCOMPLETE ---
if form_state.autocomplete_active { if form_state.autocomplete_active {
if let Some(suggestions) = form_state.get_suggestions() { // Use the Rect of the active field that render_canvas found for us.
if let Some(active_rect) = active_field_rect { if let Some(active_rect) = active_field_rect {
if !suggestions.is_empty() { 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( autocomplete::render_autocomplete_dropdown(
f, f,
active_rect, active_rect,
f.area(), f.area(),
theme, theme,
suggestions, simple_suggestions,
form_state.get_selected_suggestion_index(), selected_index,
); );
} }
} }

View File

@@ -1,16 +1,19 @@
// src/modes/canvas/edit.rs // src/modes/canvas/edit.rs
use crate::config::binds::config::Config; 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::services::grpc_client::GrpcClient;
use crate::state::app::state::AppState;
use crate::state::pages::admin::AdminState;
use crate::state::pages::{ use crate::state::pages::{
auth::{LoginState, RegisterState}, auth::{LoginState, RegisterState},
canvas_state::CanvasState, 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 anyhow::Result;
use common::proto::multieko2::search::search_response::Hit;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use tracing::debug; use tracing::debug;
@@ -33,20 +36,25 @@ pub async fn handle_edit_event(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &AppState, app_state: &AppState,
) -> Result<EditEventOutcome> { ) -> Result<EditEventOutcome> {
if app_state.ui.show_form && form_state.autocomplete_active { 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 let Some(action) =
config.get_edit_action_for_key(key.code, key.modifiers)
{
match action { match action {
"suggestion_down" => { "suggestion_down" => {
if !form_state.autocomplete_suggestions.is_empty() { if !form_state.autocomplete_suggestions.is_empty() {
let current = form_state.selected_suggestion_index.unwrap_or(0); let current =
let next = (current + 1) % form_state.autocomplete_suggestions.len(); form_state.selected_suggestion_index.unwrap_or(0);
let next = (current + 1)
% form_state.autocomplete_suggestions.len();
form_state.selected_suggestion_index = Some(next); form_state.selected_suggestion_index = Some(next);
} }
return Ok(EditEventOutcome::Message(String::new())); return Ok(EditEventOutcome::Message(String::new()));
} }
"suggestion_up" => { "suggestion_up" => {
if !form_state.autocomplete_suggestions.is_empty() { 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 { let prev = if current == 0 {
form_state.autocomplete_suggestions.len() - 1 form_state.autocomplete_suggestions.len() - 1
} else { } else {
@@ -58,19 +66,31 @@ if app_state.ui.show_form && form_state.autocomplete_active {
} }
"exit" => { "exit" => {
form_state.deactivate_autocomplete(); form_state.deactivate_autocomplete();
return Ok(EditEventOutcome::Message("Autocomplete cancelled".to_string())); return Ok(EditEventOutcome::Message(
"Autocomplete cancelled".to_string(),
));
} }
"enter_decider" => { "enter_decider" => {
if let Some(selected_idx) = form_state.selected_suggestion_index { if let Some(selected_idx) =
if let Some(selection) = form_state.autocomplete_suggestions.get(selected_idx).cloned() { form_state.selected_suggestion_index
let current_input = form_state.get_current_input_mut(); {
*current_input = selection; 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(); let new_cursor_pos = current_input.len();
form_state.set_current_cursor_pos(new_cursor_pos); form_state.set_current_cursor_pos(new_cursor_pos);
*ideal_cursor_column = new_cursor_pos; *ideal_cursor_column = new_cursor_pos;
form_state.deactivate_autocomplete(); form_state.deactivate_autocomplete();
form_state.set_has_unsaved_changes(true); 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 // 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( if let Some("enter_command_mode") = config.get_action_for_key_in_mode(
&config.keybindings.global, &config.keybindings.global,
key.code, 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( if let Some(action) = config
&config.keybindings.common, .get_action_for_key_in_mode(
key.code, &config.keybindings.common,
key.modifiers, key.code,
).as_deref() { key.modifiers,
)
.as_deref()
{
if matches!(action, "save" | "revert") { if matches!(action, "save" | "revert") {
let message_string: String = if app_state.ui.show_login { 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 { } 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 { } 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 { } 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 { } 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 { match outcome {
EventOutcome::Ok(msg) | EventOutcome::DataSaved(_, msg) => msg, 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)); return Ok(EditEventOutcome::Message(message_string));
} }
} }
if let Some(action_str) = config.get_edit_action_for_key(key.code, key.modifiers).as_deref() { if let Some(action_str) =
tracing::info!("[Handler] `handle_edit_event` received action: '{}'", 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 --- // --- MANUAL AUTOCOMPLETE TRIGGER ---
if action_str == "trigger_autocomplete" { 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 { 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 let Some(field_def) = form_state.fields.get(form_state.current_field) {
if field_def.is_link { if field_def.is_link {
tracing::info!("[Handler] Field '{}' is a link. Activating autocomplete.", field_def.display_name); // <-- ADD THIS // Use our new field to get the table to search
form_state.autocomplete_active = true; if let Some(target_table) = &field_def.link_target_table {
form_state.selected_suggestion_index = Some(0); tracing::info!(
form_state.autocomplete_suggestions = vec![ "[Handler] Field '{}' is a link to table '{}'. Triggering search.",
"Hardcoded Supplier A".to_string(), field_def.display_name,
"Hardcoded Supplier B".to_string(), target_table
"Hardcoded Company C".to_string(), );
];
return Ok(EditEventOutcome::Message("Autocomplete triggered".to_string())); // 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 { } 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 --- // --- END OF NEW LOGIC ---
if action_str == "enter_decider" { if action_str == "enter_decider" {
let effective_action = if app_state.ui.show_register let effective_action = if app_state.ui.show_register
&& register_state.in_suggestion_mode && register_state.in_suggestion_mode
&& register_state.current_field() == 4 { && register_state.current_field() == 4
{
"select_suggestion" "select_suggestion"
} else if app_state.ui.show_add_logic } else if app_state.ui.show_add_logic
&& admin_state.add_logic_state.in_target_column_suggestion_mode && admin_state
&& admin_state.add_logic_state.current_field() == 1 { .add_logic_state
.in_target_column_suggestion_mode
&& admin_state.add_logic_state.current_field() == 1
{
"select_suggestion" "select_suggestion"
} else { } else {
"next_field" "next_field"
}; };
let msg = if app_state.ui.show_login { 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 { } 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 { } 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 { } 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 { } 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)); return Ok(EditEventOutcome::Message(msg));
} }
if action_str == "exit" { if action_str == "exit" {
if app_state.ui.show_register && register_state.in_suggestion_mode { 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)); return Ok(EditEventOutcome::Message(msg));
} else if app_state.ui.show_add_logic && admin_state.add_logic_state.in_target_column_suggestion_mode { } else if app_state.ui.show_add_logic
admin_state.add_logic_state.in_target_column_suggestion_mode = false; && admin_state
admin_state.add_logic_state.show_target_column_suggestions = false; .add_logic_state
admin_state.add_logic_state.selected_target_column_suggestion_index = None; .in_target_column_suggestion_mode
return Ok(EditEventOutcome::Message("Exited column suggestions".to_string())); {
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 { } else {
return Ok(EditEventOutcome::ExitEditMode); 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 action_str == "suggestion_down" {
if !admin_state.add_logic_state.in_target_column_suggestion_mode { if !admin_state
if let Some(profile_name) = admin_state.add_logic_state.profile_name.clone().into() { .add_logic_state
if let Some(table_name) = admin_state.add_logic_state.selected_table_name.clone() { .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); 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) => { Ok(ts_response) => {
admin_state.add_logic_state.table_columns_for_suggestions = admin_state
ts_response.columns.into_iter().map(|c| c.name).collect(); .add_logic_state
admin_state.add_logic_state.update_target_column_suggestions(); .table_columns_for_suggestions =
if !admin_state.add_logic_state.target_column_suggestions.is_empty() { ts_response
admin_state.add_logic_state.in_target_column_suggestion_mode = true; .columns
return Ok(EditEventOutcome::Message("Column suggestions shown".to_string())); .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 { } 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) => { Err(e) => {
debug!("Error fetching table structure: {}", e); debug!(
admin_state.add_logic_state.table_columns_for_suggestions.clear(); "Error fetching table structure: {}",
admin_state.add_logic_state.update_target_column_suggestions(); e
return Ok(EditEventOutcome::Message(format!("Error fetching columns: {}", 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 { } 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 { } 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 { } 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)); return Ok(EditEventOutcome::Message(msg));
} }
} else if admin_state.add_logic_state.in_target_column_suggestion_mode && action_str == "suggestion_up" { } else if admin_state
let msg = add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await?; .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)); return Ok(EditEventOutcome::Message(msg));
} }
} }
if app_state.ui.show_register && register_state.current_field() == 4 { 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(); register_state.update_role_suggestions();
if !register_state.role_suggestions.is_empty() { if !register_state.role_suggestions.is_empty() {
register_state.in_suggestion_mode = true; 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") { if register_state.in_suggestion_mode
let msg = auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await?; && 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)); return Ok(EditEventOutcome::Message(msg));
} }
} }
let msg = if app_state.ui.show_login { 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 { } 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 { } 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")) { if !(admin_state
add_logic_e::execute_edit_action(action_str, key, &mut admin_state.add_logic_state, ideal_cursor_column).await? .add_logic_state
} else { String::new() } .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 { } else if app_state.ui.show_register {
if !(register_state.in_suggestion_mode && matches!(action_str, "suggestion_down" | "suggestion_up")) { if !(register_state.in_suggestion_mode
auth_e::execute_edit_action(action_str, key, register_state, ideal_cursor_column).await? && matches!(action_str, "suggestion_down" | "suggestion_up"))
} else { String::new() } {
auth_e::execute_edit_action(
action_str,
key,
register_state,
ideal_cursor_column,
)
.await?
} else {
String::new()
}
} else { } 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)); 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; register_state.selected_suggestion_index = None;
exited_suggestion_mode_for_typing = true; 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.in_target_column_suggestion_mode = false;
admin_state.add_logic_state.show_target_column_suggestions = 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; exited_suggestion_mode_for_typing = true;
} }
let mut char_insert_msg = if app_state.ui.show_login { 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 { } 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 { } 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 { } 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 { } 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 { if app_state.ui.show_register && register_state.current_field() == 4 {
register_state.update_role_suggestions(); 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(); admin_state.add_logic_state.update_target_column_suggestions();
} }

View File

@@ -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 { pub trait CanvasState {
fn current_field(&self) -> usize; fn current_field(&self) -> usize;
@@ -16,4 +18,7 @@ pub trait CanvasState {
// --- Autocomplete Support --- // --- Autocomplete Support ---
fn get_suggestions(&self) -> Option<&[String]>; fn get_suggestions(&self) -> Option<&[String]>;
fn get_selected_suggestion_index(&self) -> Option<usize>; fn get_selected_suggestion_index(&self) -> Option<usize>;
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
None
}
} }

View File

@@ -3,6 +3,7 @@
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::state::app::highlight::HighlightState; use crate::state::app::highlight::HighlightState;
use crate::state::pages::canvas_state::CanvasState; use crate::state::pages::canvas_state::CanvasState;
use common::proto::multieko2::search::search_response::Hit; // Import Hit
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui::Frame; use ratatui::Frame;
use std::collections::HashMap; use std::collections::HashMap;
@@ -12,7 +13,8 @@ use std::collections::HashMap;
pub struct FieldDefinition { pub struct FieldDefinition {
pub display_name: String, pub display_name: String,
pub data_key: String, pub data_key: String,
pub is_link: bool, // --- NEW --- To identify FK fields pub is_link: bool,
pub link_target_table: Option<String>,
} }
#[derive(Clone)] #[derive(Clone)]
@@ -28,10 +30,11 @@ pub struct FormState {
pub has_unsaved_changes: bool, pub has_unsaved_changes: bool,
pub current_cursor_pos: usize, pub current_cursor_pos: usize,
// --- NEW AUTOCOMPLETE STATE --- // --- MODIFIED AUTOCOMPLETE STATE ---
pub autocomplete_active: bool, pub autocomplete_active: bool,
pub autocomplete_suggestions: Vec<String>, pub autocomplete_suggestions: Vec<Hit>, // Changed to use the Hit struct
pub selected_suggestion_index: Option<usize>, pub selected_suggestion_index: Option<usize>,
pub autocomplete_loading: bool, // To show a loading indicator
} }
impl FormState { impl FormState {
@@ -56,6 +59,7 @@ impl FormState {
autocomplete_active: false, autocomplete_active: false,
autocomplete_suggestions: Vec::new(), autocomplete_suggestions: Vec::new(),
selected_suggestion_index: None, selected_suggestion_index: None,
autocomplete_loading: false, // Initialize loading state
} }
} }
@@ -164,6 +168,7 @@ impl FormState {
self.autocomplete_active = false; self.autocomplete_active = false;
self.autocomplete_suggestions.clear(); self.autocomplete_suggestions.clear();
self.selected_suggestion_index = None; self.selected_suggestion_index = None;
self.autocomplete_loading = false;
} }
} }
@@ -216,7 +221,14 @@ impl CanvasState for FormState {
} }
// --- MODIFIED: Implement autocomplete trait methods --- // --- MODIFIED: Implement autocomplete trait methods ---
/// Returns None because this state uses rich suggestions.
fn get_suggestions(&self) -> Option<&[String]> { fn get_suggestions(&self) -> Option<&[String]> {
None
}
/// Returns rich suggestions.
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
if self.autocomplete_active { if self.autocomplete_active {
Some(&self.autocomplete_suggestions) Some(&self.autocomplete_suggestions)
} else { } else {

View File

@@ -98,6 +98,7 @@ pub async fn run_ui() -> Result<()> {
display_name: col_name.clone(), display_name: col_name.clone(),
data_key: col_name, data_key: col_name,
is_link: false, is_link: false,
link_target_table: None,
}) })
.collect(); .collect();
@@ -347,19 +348,18 @@ pub async fn run_ui() -> Result<()> {
.map(|c| c.name.clone()) .map(|c| c.name.clone())
.collect(); .collect();
// 1. Process regular columns first, filtering out FKs
let mut field_definitions: Vec<FieldDefinition> = let mut field_definitions: Vec<FieldDefinition> =
filter_user_columns(all_columns) filter_user_columns(all_columns)
.into_iter() .into_iter()
.filter(|col_name| !col_name.ends_with("_id")) // Exclude FKs .filter(|col_name| !col_name.ends_with("_id"))
.map(|col_name| FieldDefinition { .map(|col_name| FieldDefinition {
display_name: col_name.clone(), display_name: col_name.clone(),
data_key: col_name, 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(); .collect();
// 2. Process linked tables to create the correct labels
let linked_tables: Vec<String> = app_state let linked_tables: Vec<String> = app_state
.profile_tree .profile_tree
.profiles .profiles
@@ -375,19 +375,22 @@ pub async fn run_ui() -> Result<()> {
.split_once('_') .split_once('_')
.map_or(linked_table_name.as_str(), |(_, rest)| rest); .map_or(linked_table_name.as_str(), |(_, rest)| rest);
let data_key = format!("{}_id", base_name); 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 { field_definitions.push(FieldDefinition {
display_name, display_name,
data_key, 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 --- // --- END OF MODIFIED LOGIC ---
form_state = FormState::new( form_state = FormState::new(
prof_name.clone(), prof_name.clone(),
tbl_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( if let Err(e) = UiService::fetch_and_set_table_count(