use core::fmt; use egui::{RichText, ScrollArea}; use serde::{Deserialize, Serialize}; use crate::{PROJECT_FOLDER, RightPanelContent, error::Error, object::ObjectInstance}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum FieldType { Image, SingleLine, MultiLine, Date, Number, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FieldDefinition { pub name: String, pub field_type: FieldType, pub required: bool, pub description: Option, } #[derive(Serialize, Deserialize)] pub struct Template { pub name: String, pub id: String, pub description: Option, pub fields: Vec, #[serde(skip)] pub saved: bool, #[serde(skip)] pub error: Option, } impl fmt::Debug for Template { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Template") .field("name", &self.name) .field("id", &self.id) .field("description", &self.description) .field("fields", &self.fields) .field("saved", &self.saved) .finish() } } impl Clone for Template { fn clone(&self) -> Self { Self { name: self.name.clone(), id: self.id.clone(), description: self.description.clone(), fields: self.fields.clone(), saved: self.saved, error: None, } } } impl Default for Template { fn default() -> Self { Self { name: "New Template".to_string(), id: uuid::Uuid::new_v4().to_string(), description: Some(String::from("Placeholder description")), fields: Vec::new(), saved: false, error: None, } } } impl Template { pub fn load(id: &str) -> Result> { let path = PROJECT_FOLDER.join("templates").join(format!("{id}.json")); let content = std::fs::read_to_string(&path)?; let mut template: Self = serde_json::from_str(&content)?; template.saved = true; Ok(template) } pub fn save(&mut self) -> Result<(), Box> { let path = PROJECT_FOLDER .join("templates") .join(format!("{}.json", &self.id)); let content = serde_json::to_string_pretty(self)?; std::fs::write(path, content)?; self.saved = true; Ok(()) } pub fn ui( &mut self, ui: &mut egui::Ui, new_instance: &mut Option, new_field_name: &mut String, new_field_type: &mut FieldType, new_field_required: &mut bool, new_field_description: &mut String, ) { if let Some(error) = &mut self.error { error.show(ui); } if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) { if let Err(e) = self.save() { eprintln!("Failed to save: {e}"); } } ScrollArea::vertical().show(ui, |ui| { ui.vertical(|ui| { ui.group(|ui| { ui.horizontal(|ui| { if self.saved { ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN)); } else { ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW)); } ui.label(format!("id: {}", self.id)); }); }); // Save/Cancel buttons ui.horizontal(|ui| { if ui.button("Save").clicked() { if let Err(e) = self.save() { eprintln!("Failed to save: {e}"); } } if ui.button("Create Copy").clicked() { let mut copy = self.clone(); copy.id = uuid::Uuid::new_v4().to_string(); copy.name = format!("{} (Copy)", self.name); copy.save().unwrap(); } if ui.button("Delete").clicked() { std::fs::remove_file( PROJECT_FOLDER .join("templates") .join(format!("{}.json", self.id)), ) .unwrap(); *new_instance = Some(RightPanelContent::None); } if ui.button("Cancel").clicked() { // load default state *self = Self::load(&self.id).unwrap(); } if ui.button("Use Template").clicked() { if self.saved { *new_instance = Some(RightPanelContent::Object { object: Box::new(ObjectInstance::new(self)), }); } else { self.error = Some(Error::new( "You must save the template before creating a new instance!" .to_string(), )); } } }); self.editor_ui( ui, new_field_name, new_field_type, new_field_required, new_field_description, ); }); }); } pub fn editor_ui( &mut self, ui: &mut egui::Ui, new_field_name: &mut String, new_field_type: &mut FieldType, new_field_required: &mut bool, new_field_description: &mut String, ) { egui::Grid::new("template_grid") .num_columns(2) .striped(true) .show(ui, |ui| { ui.label("Template Name:"); if ui .add(egui::TextEdit::singleline(&mut self.name).frame(false)) .changed() { self.saved = false; } ui.end_row(); ui.label("Description:"); if ui .add( egui::TextEdit::multiline(self.description.get_or_insert_with(String::new)) .desired_rows(1) .frame(false), ) .changed() { self.saved = false; } ui.end_row(); }); ui.separator(); egui::CollapsingHeader::new("Name").show(ui, |ui: &mut egui::Ui| { egui::Grid::new("field_grid") .num_columns(2) .striped(true) .show(ui, |ui| { ui.label("Name:"); ui.label("Name"); ui.end_row(); ui.label("Type:"); ui.label("SingleLine"); ui.end_row(); ui.label("Required"); ui.label("✓"); ui.end_row(); ui.label("Description:"); ui.label("Object name"); ui.end_row(); }); }); // List of fields let mut to_remove = None; for (i, field) in self.fields.iter_mut().enumerate() { let id = ui.make_persistent_id(format!("field_{i}")); egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true) .show_header(ui, |ui| { if ui.button("❌").clicked() { to_remove = Some(i); } ui.label(field.name.clone()); }) .body(|ui| { ui.separator(); egui::Grid::new("field_grid") .num_columns(2) .striped(true) .show(ui, |ui| { ui.label("Name:"); if ui.text_edit_singleline(&mut field.name).changed() { self.saved = false; } ui.end_row(); ui.label("Type:"); egui::ComboBox::from_id_salt(format!("field_type_{i}")) .selected_text(format!("{:?}", field.field_type)) .show_ui(ui, |ui| { for variant in [ FieldType::SingleLine, FieldType::MultiLine, FieldType::Number, FieldType::Date, FieldType::Image, ] { if ui .selectable_value( &mut field.field_type, variant.clone(), format!("{variant:?}"), ) .changed() { self.saved = false; } } }); ui.end_row(); ui.label("Required:"); if ui.checkbox(&mut field.required, "").clicked() { self.saved = false; } ui.end_row(); ui.label("Description:"); if ui .text_edit_singleline( field.description.get_or_insert_with(String::new), ) .changed() { self.saved = false; } ui.end_row(); ui.separator(); }); }); } // Remove field if needed if let Some(index) = to_remove { self.fields.remove(index); self.saved = false; } // Add new field ui.separator(); ui.heading("Add New Field"); ui.horizontal(|ui| { egui::Grid::new("field_grid") .num_columns(2) .striped(true) .show(ui, |ui| { ui.label("Name:"); ui.text_edit_singleline(new_field_name); ui.end_row(); ui.label("Type:"); egui::ComboBox::from_id_salt("new_field_type") .selected_text(format!("{new_field_type:?}")) .show_ui(ui, |ui| { for variant in [ FieldType::SingleLine, FieldType::MultiLine, FieldType::Number, FieldType::Date, FieldType::Image, ] { ui.selectable_value( new_field_type, variant.clone(), format!("{variant:?}"), ); } }); ui.end_row(); ui.label("Required:"); ui.checkbox(new_field_required, ""); ui.end_row(); ui.label("Description:"); ui.text_edit_singleline(new_field_description); ui.end_row(); if ui.button("Add Field").clicked() && !new_field_name.is_empty() { self.fields.push(FieldDefinition { name: new_field_name.clone(), field_type: new_field_type.clone(), required: *new_field_required, description: if new_field_description.is_empty() { None } else { Some(new_field_description.clone()) }, }); self.saved = false; // Reset new field form new_field_name.clear(); *new_field_required = false; new_field_description.clear(); } }); }); } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct FieldValue { pub value: String, #[serde(skip)] pub modified: bool, }