use eframe::egui; use serde_json::Value; use std::collections::HashMap; use super::help::{show_help_window, HelpWindowOptions}; use egui_commonmark::CommonMarkCache; use egui_phosphor::regular as icons; /// Field types supported by the generic editor #[derive(Clone)] pub enum FieldType { Text, #[allow(dead_code)] Dropdown(Vec<(String, String)>), // (value, label) MultilineText, Checkbox, Date, // simple single-line date input (YYYY-MM-DD) } /// Definition of an editable field #[derive(Clone)] pub struct EditorField { pub name: String, pub label: String, pub field_type: FieldType, pub required: bool, pub read_only: bool, } /// Replacement for FormBuilder that uses egui_form + garde for validation. /// Maintains compatibility with existing EditorField schema. pub struct FormBuilder { pub title: String, pub fields: Vec, pub data: HashMap, // Store as strings for form editing pub original_data: serde_json::Map, // Store original JSON data pub show: bool, pub item_id: Option, pub is_new: bool, field_help: HashMap, pub form_help_text: Option, pub show_form_help: bool, help_cache: CommonMarkCache, } impl FormBuilder { pub fn new(title: impl Into, fields: Vec) -> Self { Self { title: title.into(), fields, data: HashMap::new(), original_data: serde_json::Map::new(), show: false, item_id: None, is_new: false, field_help: HashMap::new(), form_help_text: None, show_form_help: false, help_cache: CommonMarkCache::default(), } } #[allow(dead_code)] pub fn with_help(mut self, help_text: impl Into) -> Self { self.form_help_text = Some(help_text.into()); self } pub fn open(&mut self, item: &Value) { self.show = true; self.data.clear(); self.original_data.clear(); // Convert JSON to string map if let Some(obj) = item.as_object() { self.original_data = obj.clone(); for (k, v) in obj { let value_str = match v { Value::String(s) => s.clone(), Value::Number(n) => n.to_string(), Value::Bool(b) => b.to_string(), Value::Null => String::new(), _ => serde_json::to_string(v).unwrap_or_default(), }; self.data.insert(k.clone(), value_str); } self.item_id = obj.get("id").and_then(|v| match v { Value::String(s) => Some(s.clone()), Value::Number(n) => n.as_i64().map(|i| i.to_string()), _ => None, }); self.is_new = false; } } pub fn open_new(&mut self, preset: Option<&serde_json::Map>) { self.show = true; self.data.clear(); if let Some(p) = preset { for (k, v) in p { let value_str = match v { Value::String(s) => s.clone(), Value::Number(n) => n.to_string(), Value::Bool(b) => b.to_string(), Value::Null => String::new(), _ => serde_json::to_string(v).unwrap_or_default(), }; self.data.insert(k.clone(), value_str); } } self.item_id = None; self.is_new = true; } pub fn close(&mut self) { self.show = false; self.data.clear(); self.original_data.clear(); self.item_id = None; self.is_new = false; } /// Show the form editor and return Some(data) if saved, None if still open pub fn show_editor( &mut self, ctx: &egui::Context, ) -> Option>> { if !self.show { return None; } let mut result = None; let mut close_requested = false; // Dynamic sizing let root_bounds = ctx.available_rect(); let screen_bounds = ctx.input(|i| { i.viewport().inner_rect.unwrap_or(egui::Rect::from_min_size( egui::Pos2::ZERO, egui::vec2(800.0, 600.0), )) }); let horizontal_margin = 24.0; let vertical_margin = 24.0; let max_w = (root_bounds.width() - horizontal_margin) .min(screen_bounds.width() - horizontal_margin) .max(260.0); let max_h = (root_bounds.height() - vertical_margin) .min(screen_bounds.height() - vertical_margin) .max(260.0); let default_w = (root_bounds.width() * 0.6).clamp(320.0, max_w); let default_h = (root_bounds.height() * 0.7).clamp(300.0, max_h); let content_max_h = (max_h - 160.0).max(180.0); let _window_response = egui::Window::new(&self.title) .collapsible(false) .resizable(true) .default_width(default_w) .default_height(default_h) .min_width(f32::min(280.0, max_w)) .min_height(f32::min(260.0, max_h)) .max_width(max_w) .max_height(max_h) .open(&mut self.show) .show(ctx, |ui| { egui::ScrollArea::vertical() .max_height(content_max_h) .show(ui, |ui| { ui.vertical(|ui| { for field in &self.fields.clone() { let field_value = self .data .entry(field.name.clone()) .or_insert_with(String::new); match &field.field_type { FieldType::Text => { let label_text = if field.required { format!("{} *", field.label) } else { field.label.clone() }; ui.label(&label_text); ui.add( egui::TextEdit::singleline(field_value) .desired_width(f32::INFINITY) .interactive(!field.read_only), ); } FieldType::MultilineText => { let label_text = if field.required { format!("{} *", field.label) } else { field.label.clone() }; ui.label(&label_text); ui.add( egui::TextEdit::multiline(field_value) .desired_width(f32::INFINITY) .interactive(!field.read_only), ); } FieldType::Checkbox => { let mut checked = field_value == "true" || field_value == "1"; ui.add_enabled( !field.read_only, egui::Checkbox::new(&mut checked, &field.label), ); if !field.read_only { *field_value = if checked { "true".to_string() } else { "false".to_string() }; } } FieldType::Date => { let label_text = if field.required { format!("{} *", field.label) } else { field.label.clone() }; ui.label(&label_text); ui.add( egui::TextEdit::singleline(field_value) .hint_text("YYYY-MM-DD") .desired_width(f32::INFINITY) .interactive(!field.read_only), ); } FieldType::Dropdown(options) => { let label_text = if field.required { format!("{} *", field.label) } else { field.label.clone() }; ui.label(&label_text); ui.add_enabled_ui(!field.read_only, |ui| { egui::ComboBox::from_id_salt(&field.name) .width(ui.available_width()) .selected_text( options .iter() .find(|(v, _)| v == field_value) .map(|(_, l)| l.as_str()) .unwrap_or(""), ) .show_ui(ui, |ui| { for (value, label) in options { ui.selectable_value( field_value, value.clone(), label, ); } }); }); } } // Show help text if available if let Some(help) = self.field_help.get(&field.name) { ui.label( egui::RichText::new(help) .small() .color(ui.visuals().weak_text_color()), ); } ui.add_space(8.0); } }); }); ui.separator(); ui.horizontal(|ui| { // Help button if help text is available if self.form_help_text.is_some() { if ui.button(format!("{} Help", icons::QUESTION)).clicked() { self.show_form_help = true; } ui.separator(); } // Submit button if ui.button(format!("{} Save", icons::CHECK)).clicked() { // Validate required fields let mut missing_fields = Vec::new(); for field in &self.fields { if field.required { let value = self.data.get(&field.name).map(|s| s.as_str()).unwrap_or(""); if value.trim().is_empty() { missing_fields.push(field.label.clone()); } } } if !missing_fields.is_empty() { log::warn!("Missing required fields: {}", missing_fields.join(", ")); // Show error in UI - for now just log, could add error message field } else { // Convert string map back to JSON let mut json_map = serde_json::Map::new(); for (k, v) in &self.data { // Try to preserve types let json_value = if v == "true" { Value::Bool(true) } else if v == "false" { Value::Bool(false) } else if let Ok(n) = v.parse::() { Value::Number(n.into()) } else if let Ok(n) = v.parse::() { serde_json::Number::from_f64(n) .map(Value::Number) .unwrap_or_else(|| Value::String(v.clone())) } else if v.is_empty() { Value::Null } else { Value::String(v.clone()) }; json_map.insert(k.clone(), json_value); } // CRITICAL: Include the item_id so updates work if let Some(ref id) = self.item_id { json_map.insert( "__editor_item_id".to_string(), Value::String(id.clone()), ); } result = Some(Some(json_map)); close_requested = true; } } if ui.button(format!("{} Cancel", icons::X)).clicked() { result = Some(None); close_requested = true; } }); }); if close_requested || !self.show { self.close(); } // Show help window if requested if let Some(help_text) = &self.form_help_text { if self.show_form_help { show_help_window( ctx, &mut self.help_cache, format!("{}_help", self.title), &format!("{} - Help", self.title), help_text, &mut self.show_form_help, HelpWindowOptions::default(), ); } } result } }