// src/state/pages/form.rs use crate::config::colors::themes::Theme; use canvas::canvas::{CanvasState, CanvasAction, ActionContext, HighlightState}; use common::proto::komp_ac::search::search_response::Hit; use ratatui::layout::Rect; use ratatui::Frame; use std::collections::HashMap; 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(), _ => String::new(), } } #[derive(Debug, Clone)] pub struct FieldDefinition { pub display_name: String, pub data_key: String, pub is_link: bool, pub link_target_table: Option, } #[derive(Clone)] pub struct FormState { pub id: i64, pub profile_name: String, pub table_name: String, pub total_count: u64, pub current_position: u64, pub fields: Vec, pub values: Vec, pub current_field: usize, pub has_unsaved_changes: bool, pub current_cursor_pos: usize, pub autocomplete_active: bool, pub autocomplete_suggestions: Vec, pub selected_suggestion_index: Option, pub autocomplete_loading: bool, pub link_display_map: HashMap, } impl FormState { // Add this method pub fn deactivate_autocomplete(&mut self) { self.autocomplete_active = false; self.autocomplete_suggestions.clear(); self.selected_suggestion_index = None; self.autocomplete_loading = false; } pub fn new( profile_name: String, table_name: String, fields: Vec, ) -> Self { let values = vec![String::new(); fields.len()]; FormState { id: 0, profile_name, table_name, total_count: 0, current_position: 1, fields, values, current_field: 0, has_unsaved_changes: false, current_cursor_pos: 0, autocomplete_active: false, autocomplete_suggestions: Vec::new(), selected_suggestion_index: None, autocomplete_loading: false, link_display_map: HashMap::new(), } } pub fn get_display_name_for_hit(&self, hit: &Hit) -> String { if let Ok(content_map) = serde_json::from_str::>( &hit.content_json, ) { const IGNORED_KEYS: &[&str] = &["id", "deleted", "created_at"]; let mut keys: Vec<_> = content_map .keys() .filter(|k| !IGNORED_KEYS.contains(&k.as_str())) .cloned() .collect(); keys.sort(); let values: Vec<_> = keys .iter() .map(|key| { content_map .get(key) .map(json_value_to_string) .unwrap_or_default() }) .filter(|s| !s.is_empty()) .take(1) .collect(); let display_part = values.first().cloned().unwrap_or_default(); if display_part.is_empty() { format!("ID: {}", hit.id) } else { format!("{} | ID: {}", display_part, hit.id) } } else { format!("ID: {} (parse error)", hit.id) } } pub fn render( &self, f: &mut Frame, area: Rect, theme: &Theme, is_edit_mode: bool, highlight_state: &HighlightState, // Now using canvas::HighlightState ) { let fields_str_slice: Vec<&str> = self.fields().iter().map(|s| *s).collect(); let values_str_slice: Vec<&String> = self.values.iter().collect(); crate::components::form::form::render_form( f, area, self, &fields_str_slice, &self.current_field, &values_str_slice, &self.table_name, theme, is_edit_mode, highlight_state, self.total_count, self.current_position, ); } pub fn reset_to_empty(&mut self) { self.id = 0; self.values.iter_mut().for_each(|v| v.clear()); self.current_field = 0; self.current_cursor_pos = 0; self.has_unsaved_changes = false; if self.total_count > 0 { self.current_position = self.total_count + 1; } else { self.current_position = 1; } self.deactivate_autocomplete(); self.link_display_map.clear(); } pub fn get_current_input(&self) -> &str { self.values .get(self.current_field) .map(|s| s.as_str()) .unwrap_or("") } pub fn get_current_input_mut(&mut self) -> &mut String { self.link_display_map.remove(&self.current_field); self.values .get_mut(self.current_field) .expect("Invalid current_field index") } pub fn update_from_response( &mut self, response_data: &HashMap, new_position: u64, ) { self.values = self .fields .iter() .map(|field_def| { response_data .get(&field_def.data_key) .cloned() .unwrap_or_default() }) .collect(); let id_str_opt = response_data .iter() .find(|(k, _)| k.eq_ignore_ascii_case("id")) .map(|(_, v)| v); if let Some(id_str) = id_str_opt { if let Ok(parsed_id) = id_str.parse::() { self.id = parsed_id; } else { tracing::error!( "Failed to parse 'id' field '{}' for table {}.{}", id_str, self.profile_name, self.table_name ); self.id = 0; } } else { self.id = 0; } self.current_position = new_position; self.has_unsaved_changes = false; self.current_field = 0; self.current_cursor_pos = 0; self.deactivate_autocomplete(); self.link_display_map.clear(); } // NEW: Keep the rich suggestions methods for compatibility pub fn get_rich_suggestions(&self) -> Option<&[Hit]> { if self.autocomplete_active { Some(&self.autocomplete_suggestions) } else { None } } pub fn activate_rich_suggestions(&mut self, suggestions: Vec) { self.autocomplete_suggestions = suggestions; self.autocomplete_active = !self.autocomplete_suggestions.is_empty(); self.selected_suggestion_index = if self.autocomplete_active { Some(0) } else { None }; self.autocomplete_loading = false; } } impl CanvasState for FormState { fn current_field(&self) -> usize { self.current_field } fn current_cursor_pos(&self) -> usize { self.current_cursor_pos } fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } fn inputs(&self) -> Vec<&String> { self.values.iter().collect() } fn get_current_input(&self) -> &str { FormState::get_current_input(self) } fn get_current_input_mut(&mut self) -> &mut String { FormState::get_current_input_mut(self) } fn fields(&self) -> Vec<&str> { self.fields .iter() .map(|f| f.display_name.as_str()) .collect() } fn set_current_field(&mut self, index: usize) { if index < self.fields.len() { self.current_field = index; } self.deactivate_autocomplete(); } fn set_current_cursor_pos(&mut self, pos: usize) { self.current_cursor_pos = pos; } fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_unsaved_changes = changed; } // --- FEATURE-SPECIFIC ACTION HANDLING --- fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option { match action { CanvasAction::SelectSuggestion => { if let Some(selected_idx) = self.selected_suggestion_index { if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() { // Extract the value from the selected suggestion if let Ok(content_map) = serde_json::from_str::>(&hit.content_json) { let current_field_def = &self.fields[self.current_field]; if let Some(value) = content_map.get(¤t_field_def.data_key) { let new_value = json_value_to_string(value); let display_name = self.get_display_name_for_hit(&hit); *self.get_current_input_mut() = new_value.clone(); self.set_current_cursor_pos(new_value.len()); self.set_has_unsaved_changes(true); self.deactivate_autocomplete(); return Some(format!("Selected: {}", display_name)); } } } } None } _ => None, // Let canvas handle other actions } } fn get_display_value_for_field(&self, index: usize) -> &str { if let Some(display_text) = self.link_display_map.get(&index) { return display_text.as_str(); } self.inputs() .get(index) .map(|s| s.as_str()) .unwrap_or("") } fn has_display_override(&self, index: usize) -> bool { self.link_display_map.contains_key(&index) } }