Files
komp_ac/client/src/state/pages/form.rs

324 lines
9.9 KiB
Rust

// 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<String>,
}
#[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<FieldDefinition>,
pub values: Vec<String>,
pub current_field: usize,
pub has_unsaved_changes: bool,
pub current_cursor_pos: usize,
pub autocomplete_active: bool,
pub autocomplete_suggestions: Vec<Hit>,
pub selected_suggestion_index: Option<usize>,
pub autocomplete_loading: bool,
pub link_display_map: HashMap<usize, String>,
}
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<FieldDefinition>,
) -> 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::<HashMap<String, serde_json::Value>>(
&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<String, String>,
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::<i64>() {
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<Hit>) {
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<String> {
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::<HashMap<String, serde_json::Value>>(&hit.content_json) {
let current_field_def = &self.fields[self.current_field];
if let Some(value) = content_map.get(&current_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)
}
}