// src/state/pages/form.rs use crate::config::colors::themes::Theme; use canvas::{DataProvider, AppMode, EditorState, FormEditor}; use canvas::canvas::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, pub app_mode: AppMode, } 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(), app_mode: AppMode::Edit, } } 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, ) { // Wrap in FormEditor for new API let mut editor = FormEditor::new(self.clone()); // Use new canvas rendering canvas::render_canvas_default(f, area, &editor); // If autocomplete is active, render suggestions if self.autocomplete_active && !self.autocomplete_suggestions.is_empty() { // Note: This will need to be updated when suggestions are integrated // canvas::render_suggestions_dropdown(f, area, input_rect, &canvas::DefaultCanvasTheme, &editor); } } 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; } // Legacy method compatibility pub fn fields(&self) -> Vec<&str> { self.fields .iter() .map(|f| f.display_name.as_str()) .collect() } pub 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.values .get(index) .map(|s| s.as_str()) .unwrap_or("") } pub fn has_display_override(&self, index: usize) -> bool { self.link_display_map.contains_key(&index) } pub fn current_mode(&self) -> AppMode { self.app_mode } // Add missing methods that used to come from CanvasState trait pub fn has_unsaved_changes(&self) -> bool { self.has_unsaved_changes } pub fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_unsaved_changes = changed; } pub fn current_field(&self) -> usize { self.current_field } pub fn set_current_field(&mut self, index: usize) { if index < self.fields.len() { self.current_field = index; } self.deactivate_autocomplete(); } pub fn current_cursor_pos(&self) -> usize { self.current_cursor_pos } pub fn set_current_cursor_pos(&mut self, pos: usize) { self.current_cursor_pos = pos; } } // Step 2: Implement DataProvider for FormState impl DataProvider for FormState { fn field_count(&self) -> usize { self.fields.len() } fn field_name(&self, index: usize) -> &str { &self.fields[index].display_name } fn field_value(&self, index: usize) -> &str { &self.values[index] } fn set_field_value(&mut self, index: usize, value: String) { if let Some(v) = self.values.get_mut(index) { *v = value; self.has_unsaved_changes = true; } } fn supports_suggestions(&self, field_index: usize) -> bool { self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false) } }