311 lines
9.0 KiB
Rust
311 lines
9.0 KiB
Rust
// 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<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>,
|
|
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<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(),
|
|
app_mode: AppMode::Edit,
|
|
}
|
|
}
|
|
|
|
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,
|
|
) {
|
|
// 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<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;
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|