post doesnt work, but refactored code displays the autocomplete at least, needs fix

This commit is contained in:
filipriec
2025-06-16 16:42:25 +02:00
parent a9c4527318
commit b30fef4ccd
4 changed files with 149 additions and 167 deletions

View File

@@ -1,30 +1,18 @@
// src/components/common/autocomplete.rs // src/components/common/autocomplete.rs
use common::proto::multieko2::search::search_response::Hit;
use crate::config::colors::themes::Theme; use crate::config::colors::themes::Theme;
use crate::state::pages::form::FormState;
use common::proto::multieko2::search::search_response::Hit;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
widgets::{Block, List, ListItem, ListState}, widgets::{Block, List, ListItem, ListState},
Frame, Frame,
}; };
use std::collections::HashMap;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
/// Converts a serde_json::Value into a displayable String.
/// Handles String, Number, and Bool variants. Returns an empty string for Null and others.
fn json_value_to_string(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
// Return an empty string for Null, Array, or Object so we can filter them out.
_ => String::new(),
}
}
/// Renders an opaque dropdown list for simple string-based suggestions. /// Renders an opaque dropdown list for simple string-based suggestions.
/// This function remains unchanged. /// THIS IS THE RESTORED FUNCTION.
pub fn render_autocomplete_dropdown( pub fn render_autocomplete_dropdown(
f: &mut Frame, f: &mut Frame,
input_rect: Rect, input_rect: Rect,
@@ -84,22 +72,22 @@ pub fn render_autocomplete_dropdown(
.collect(); .collect();
let list = List::new(items); let list = List::new(items);
let mut profile_list_state = ListState::default(); let mut list_state = ListState::default();
profile_list_state.select(selected_index); list_state.select(selected_index);
f.render_stateful_widget(list, dropdown_area, &mut profile_list_state); f.render_stateful_widget(list, dropdown_area, &mut list_state);
} }
// --- MODIFIED FUNCTION FOR RICH SUGGESTIONS ---
/// Renders an opaque dropdown list for rich `Hit`-based suggestions. /// Renders an opaque dropdown list for rich `Hit`-based suggestions.
/// Displays the value of the first meaningful column, followed by the Hit ID. /// RENAMED from render_rich_autocomplete_dropdown
pub fn render_rich_autocomplete_dropdown( pub fn render_hit_autocomplete_dropdown(
f: &mut Frame, f: &mut Frame,
input_rect: Rect, input_rect: Rect,
frame_area: Rect, frame_area: Rect,
theme: &Theme, theme: &Theme,
suggestions: &[Hit], suggestions: &[Hit],
selected_index: Option<usize>, selected_index: Option<usize>,
form_state: &FormState,
) { ) {
if suggestions.is_empty() { if suggestions.is_empty() {
return; return;
@@ -107,50 +95,9 @@ pub fn render_rich_autocomplete_dropdown(
let display_names: Vec<String> = suggestions let display_names: Vec<String> = suggestions
.iter() .iter()
.map(|hit| { .map(|hit| form_state.get_display_name_for_hit(hit))
// Use serde_json::Value to handle mixed types (string, null, etc.)
if let Ok(content_map) =
serde_json::from_str::<HashMap<String, serde_json::Value>>(
&hit.content_json,
)
{
// Define keys to ignore for a cleaner display
const IGNORED_KEYS: &[&str] = &["id", "deleted", "created_at"];
// Get keys, filter out ignored ones, and sort for consistency
let mut keys: Vec<_> = content_map
.keys()
.filter(|k| !IGNORED_KEYS.contains(&k.as_str()))
.cloned()
.collect();
keys.sort();
// Get only the first non-empty value from the sorted keys
let values: Vec<_> = keys
.iter()
.map(|key| {
content_map
.get(key)
.map(json_value_to_string)
.unwrap_or_default()
})
.filter(|s| !s.is_empty()) // Filter out null/empty values
.take(1) // Changed from take(2) to take(1)
.collect();
let display_part = values.first().cloned().unwrap_or_default(); // Get the first value
if display_part.is_empty() {
format!("{}", hit.id)
} else {
format!("{} | {}", display_part, hit.id) // ID at the end
}
} else {
format!("{} (parse error)", hit.id)
}
})
.collect(); .collect();
// --- Calculate Dropdown Size & Position ---
let max_suggestion_width = let max_suggestion_width =
display_names.iter().map(|s| s.width()).max().unwrap_or(0) as u16; display_names.iter().map(|s| s.width()).max().unwrap_or(0) as u16;
let horizontal_padding: u16 = 2; let horizontal_padding: u16 = 2;
@@ -164,7 +111,6 @@ pub fn render_rich_autocomplete_dropdown(
height: dropdown_height, height: dropdown_height,
}; };
// --- Clamping Logic ---
if dropdown_area.bottom() > frame_area.height { if dropdown_area.bottom() > frame_area.height {
dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); dropdown_area.y = input_rect.y.saturating_sub(dropdown_height);
} }
@@ -174,7 +120,6 @@ pub fn render_rich_autocomplete_dropdown(
dropdown_area.x = dropdown_area.x.max(0); dropdown_area.x = dropdown_area.x.max(0);
dropdown_area.y = dropdown_area.y.max(0); dropdown_area.y = dropdown_area.y.max(0);
// --- Rendering Logic ---
let background_block = let background_block =
Block::default().style(Style::default().bg(Color::DarkGray)); Block::default().style(Style::default().bg(Color::DarkGray));
f.render_widget(background_block, dropdown_area); f.render_widget(background_block, dropdown_area);

View File

@@ -78,25 +78,25 @@ pub fn render_form(
// --- NEW: RENDER AUTOCOMPLETE --- // --- NEW: RENDER AUTOCOMPLETE ---
if form_state.autocomplete_active { if form_state.autocomplete_active {
// 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 {
let selected_index = form_state.get_selected_suggestion_index(); 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 let Some(rich_suggestions) = form_state.get_rich_suggestions() {
if !rich_suggestions.is_empty() { if !rich_suggestions.is_empty() {
autocomplete::render_rich_autocomplete_dropdown( // CHANGE THIS to call the renamed function
autocomplete::render_hit_autocomplete_dropdown(
f, f,
active_rect, active_rect,
f.area(), // Use f.area() for clamping, not f.size() f.area(),
theme, theme,
rich_suggestions, rich_suggestions,
selected_index, selected_index,
form_state,
); );
} }
} }
// 2. Fallback to simple suggestions if rich ones aren't available. // The fallback to simple suggestions is now correctly handled
// because the original render_autocomplete_dropdown exists again.
else if let Some(simple_suggestions) = form_state.get_suggestions() { else if let Some(simple_suggestions) = form_state.get_suggestions() {
if !simple_suggestions.is_empty() { if !simple_suggestions.is_empty() {
autocomplete::render_autocomplete_dropdown( autocomplete::render_autocomplete_dropdown(
@@ -112,3 +112,4 @@ pub fn render_form(
} }
} }
} }

View File

@@ -1,17 +1,100 @@
// src/services/ui_service.rs // src/services/ui_service.rs
use crate::services::grpc_client::GrpcClient; use crate::services::grpc_client::GrpcClient;
use crate::state::pages::form::FormState;
use crate::tui::functions::common::form::SaveOutcome;
use crate::state::pages::add_logic::AddLogicState;
use crate::state::app::state::AppState; use crate::state::app::state::AppState;
use crate::state::pages::add_logic::AddLogicState;
use crate::state::pages::form::{FieldDefinition, FormState};
use crate::tui::functions::common::form::SaveOutcome;
use crate::utils::columns::filter_user_columns; use crate::utils::columns::filter_user_columns;
use anyhow::{Context, Result}; use anyhow::{anyhow, Context, Result};
use std::sync::Arc; use std::sync::Arc;
pub struct UiService; pub struct UiService;
impl UiService { impl UiService {
pub async fn load_table_view(
grpc_client: &mut GrpcClient,
app_state: &mut AppState,
profile_name: &str,
table_name: &str,
) -> Result<FormState> {
// 1. & 2. Fetch and Cache Schema - UNCHANGED
let table_structure = grpc_client
.get_table_structure(profile_name.to_string(), table_name.to_string())
.await
.context(format!(
"Failed to get table structure for {}.{}",
profile_name, table_name
))?;
let cache_key = format!("{}.{}", profile_name, table_name);
app_state
.schema_cache
.insert(cache_key, Arc::new(table_structure.clone()));
tracing::info!("Schema for '{}.{}' cached.", profile_name, table_name);
// --- START: FINAL, SIMPLIFIED, CORRECT LOGIC ---
// 3a. Create definitions for REGULAR fields first.
let mut fields: Vec<FieldDefinition> = table_structure
.columns
.iter()
.filter(|col| {
!col.is_primary_key
&& col.name != "deleted"
&& col.name != "created_at"
&& !col.name.ends_with("_id") // Filter out ALL potential links
})
.map(|col| FieldDefinition {
display_name: col.name.clone(),
data_key: col.name.clone(),
is_link: false,
link_target_table: None,
})
.collect();
// 3b. Now, find and APPEND definitions for LINK fields based on the `_id` convention.
let link_fields: Vec<FieldDefinition> = table_structure
.columns
.iter()
.filter(|col| col.name.ends_with("_id")) // Find all foreign key columns
.map(|col| {
// The table we link to is derived from the column name.
// e.g., "test_diacritics_id" -> "test_diacritics"
let target_table_base = col
.name
.strip_suffix("_id")
.unwrap_or(&col.name);
// Find the full table name from the profile tree for display.
// e.g., "test_diacritics" -> "2025_test_diacritics"
let full_target_table_name = app_state
.profile_tree
.profiles
.iter()
.find(|p| p.name == profile_name)
.and_then(|p| p.tables.iter().find(|t| t.name.ends_with(target_table_base)))
.map_or(target_table_base.to_string(), |t| t.name.clone());
FieldDefinition {
display_name: full_target_table_name.clone(),
data_key: col.name.clone(), // The actual FK column name
is_link: true,
link_target_table: Some(full_target_table_name),
}
})
.collect();
fields.extend(link_fields); // Append the link fields to the end
// --- END: FINAL, SIMPLIFIED, CORRECT LOGIC ---
Ok(FormState::new(
profile_name.to_string(),
table_name.to_string(),
fields,
))
}
pub async fn initialize_add_logic_table_data( pub async fn initialize_add_logic_table_data(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
add_logic_state: &mut AddLogicState, add_logic_state: &mut AddLogicState,
@@ -93,6 +176,7 @@ impl UiService {
} }
} }
// REFACTOR THIS FUNCTION
pub async fn initialize_app_state_and_form( pub async fn initialize_app_state_and_form(
grpc_client: &mut GrpcClient, grpc_client: &mut GrpcClient,
app_state: &mut AppState, app_state: &mut AppState,
@@ -122,35 +206,19 @@ impl UiService {
initial_table_name.clone(), initial_table_name.clone(),
); );
let table_structure = grpc_client // NOW, just call our new central function. This avoids code duplication.
.get_table_structure( let form_state = Self::load_table_view(
initial_profile_name.clone(), grpc_client,
initial_table_name.clone(), app_state,
) &initial_profile_name,
.await &initial_table_name,
.context(format!( )
"Failed to get initial table structure for {}.{}", .await?;
initial_profile_name, initial_table_name
))?;
// NEW: Populate the "Rulebook" cache // The field names for the UI are derived from the new form_state
let cache_key = format!( let field_names = form_state.fields.iter().map(|f| f.display_name.clone()).collect();
"{}.{}",
initial_profile_name, initial_table_name
);
app_state
.schema_cache
.insert(cache_key, Arc::new(table_structure.clone()));
let column_names: Vec<String> = table_structure Ok((initial_profile_name, initial_table_name, field_names))
.columns
.iter()
.map(|col| col.name.clone())
.collect();
let filtered_columns = filter_user_columns(column_names);
Ok((initial_profile_name, initial_table_name, filtered_columns))
} }
pub async fn fetch_and_set_table_count( pub async fn fetch_and_set_table_count(

View File

@@ -350,123 +350,91 @@ pub async fn run_ui() -> Result<()> {
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();
// This condition correctly detects a table switch.
if prev_view_profile_name != current_view_profile if prev_view_profile_name != current_view_profile
|| prev_view_table_name != current_view_table || prev_view_table_name != current_view_table
{ {
if let (Some(prof_name), Some(tbl_name)) = if let (Some(prof_name), Some(tbl_name)) =
(current_view_profile.as_ref(), current_view_table.as_ref()) (current_view_profile.as_ref(), current_view_table.as_ref())
{ {
// --- START OF REFACTORED LOGIC ---
app_state.show_loading_dialog( app_state.show_loading_dialog(
"Loading Table", "Loading Table",
&format!("Fetching data for {}.{}...", prof_name, tbl_name), &format!("Fetching data for {}.{}...", prof_name, tbl_name),
); );
needs_redraw = true; needs_redraw = true;
match grpc_client // 1. Call our new, central function. It handles fetching AND caching.
.get_table_structure(prof_name.clone(), tbl_name.clone()) match UiService::load_table_view(
.await &mut grpc_client,
&mut app_state,
prof_name,
tbl_name,
)
.await
{ {
Ok(structure_response) => { Ok(mut new_form_state) => {
// --- START OF MODIFIED LOGIC --- // 2. The function succeeded, we have a new FormState.
let all_columns: Vec<String> = structure_response // Now, fetch its data.
.columns
.iter()
.map(|c| c.name.clone())
.collect();
let mut field_definitions: Vec<FieldDefinition> =
filter_user_columns(all_columns)
.into_iter()
.filter(|col_name| !col_name.ends_with("_id"))
.map(|col_name| FieldDefinition {
display_name: col_name.clone(),
data_key: col_name,
is_link: false,
link_target_table: None, // Regular fields have no target
})
.collect();
let linked_tables: Vec<String> = app_state
.profile_tree
.profiles
.iter()
.find(|p| p.name == *prof_name)
.and_then(|profile| {
profile.tables.iter().find(|t| t.name == *tbl_name)
})
.map_or(vec![], |table| table.depends_on.clone());
for linked_table_name in linked_tables {
let base_name = linked_table_name
.split_once('_')
.map_or(linked_table_name.as_str(), |(_, rest)| rest);
let data_key = format!("{}_id", base_name);
let display_name = linked_table_name.clone(); // Clone for use below
field_definitions.push(FieldDefinition {
display_name,
data_key,
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, // 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(
&mut grpc_client, &mut grpc_client,
&mut form_state, &mut new_form_state,
) )
.await .await
{ {
// Handle count fetching error
app_state.update_dialog_content( app_state.update_dialog_content(
&format!("Error fetching count: {}", e), &format!("Error fetching count: {}", e),
vec!["OK".to_string()], vec!["OK".to_string()],
DialogPurpose::LoginFailed, DialogPurpose::LoginFailed, // Or a more appropriate purpose
); );
} else if form_state.total_count > 0 { } else if new_form_state.total_count > 0 {
// If there are records, load the first/last one
if let Err(e) = UiService::load_table_data_by_position( if let Err(e) = UiService::load_table_data_by_position(
&mut grpc_client, &mut grpc_client,
&mut form_state, &mut new_form_state,
) )
.await .await
{ {
// Handle data loading error
app_state.update_dialog_content( app_state.update_dialog_content(
&format!("Error loading data: {}", e), &format!("Error loading data: {}", e),
vec!["OK".to_string()], vec!["OK".to_string()],
DialogPurpose::LoginFailed, DialogPurpose::LoginFailed, // Or a more appropriate purpose
); );
} else { } else {
// Success! Hide the loading dialog.
app_state.hide_dialog(); app_state.hide_dialog();
} }
} else { } else {
form_state.reset_to_empty(); // No records, so just reset to an empty form.
new_form_state.reset_to_empty();
app_state.hide_dialog(); app_state.hide_dialog();
} }
// 3. CRITICAL: Replace the old form_state with the new one.
form_state = new_form_state;
// 4. Update our tracking variables.
prev_view_profile_name = current_view_profile; prev_view_profile_name = current_view_profile;
prev_view_table_name = current_view_table; prev_view_table_name = current_view_table;
table_just_switched = true; table_just_switched = true;
} }
Err(e) => { Err(e) => {
// This handles errors from load_table_view (e.g., schema fetch failed)
app_state.update_dialog_content( app_state.update_dialog_content(
&format!("Error fetching table structure: {}", e), &format!("Error loading table: {}", e),
vec!["OK".to_string()], vec!["OK".to_string()],
DialogPurpose::LoginFailed, DialogPurpose::LoginFailed, // Or a more appropriate purpose
); );
// Revert the view change in app_state to avoid a loop
app_state.current_view_profile_name = app_state.current_view_profile_name =
prev_view_profile_name.clone(); prev_view_profile_name.clone();
app_state.current_view_table_name = app_state.current_view_table_name =
prev_view_table_name.clone(); prev_view_table_name.clone();
} }
} }
// --- END OF REFACTORED LOGIC ---
} }
needs_redraw = true; needs_redraw = true;
} }