added notes, improved other features and removed most bugs
This commit is contained in:
+223
-187
@@ -1,8 +1,9 @@
|
||||
use core::fmt;
|
||||
|
||||
use egui::{RichText, ScrollArea};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{PROJECT_FOLDER, RightPanelContent, object::ObjectInstance};
|
||||
use crate::{PROJECT_FOLDER, RightPanelContent, error::Error, object::ObjectInstance};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum FieldType {
|
||||
@@ -21,57 +22,77 @@ pub struct FieldDefinition {
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum EditorMode {
|
||||
#[default]
|
||||
View,
|
||||
EditTemplate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Template {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
|
||||
pub description: Option<String>,
|
||||
pub fields: Vec<FieldDefinition>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub saved: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub editor_mode: EditorMode,
|
||||
pub error: Option<Error>,
|
||||
}
|
||||
|
||||
#[serde(skip)]
|
||||
pub dialog: Option<egui_file::FileDialog>,
|
||||
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,
|
||||
path: None,
|
||||
editor_mode: EditorMode::default(),
|
||||
dialog: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Template {
|
||||
pub fn load(path: PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
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.path = Some(path);
|
||||
template.saved = true;
|
||||
Ok(template)
|
||||
}
|
||||
|
||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let path = PROJECT_FOLDER
|
||||
.join("templates")
|
||||
.join(format!("{}.json", &self.id));
|
||||
|
||||
let content = serde_json::to_string_pretty(self)?;
|
||||
std::fs::write(self.path.as_ref().ok_or("no path")?, content)?;
|
||||
std::fs::write(path, content)?;
|
||||
self.saved = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,110 +105,64 @@ impl Template {
|
||||
new_field_required: &mut bool,
|
||||
new_field_description: &mut String,
|
||||
) {
|
||||
match self.editor_mode {
|
||||
EditorMode::View => {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
if ui.button("Edit Template").clicked() {
|
||||
self.editor_mode = EditorMode::EditTemplate;
|
||||
}
|
||||
if let Some(error) = &mut self.error {
|
||||
error.show(ui);
|
||||
}
|
||||
|
||||
if ui.button("New Instance").clicked() {
|
||||
*new_instance = RightPanelContent::Instance {
|
||||
instance: Box::new(ObjectInstance::new(self)),
|
||||
path: None,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
self.viewer_ui(ui, new_instance);
|
||||
|
||||
if self.saved {
|
||||
ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
||||
} else {
|
||||
ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
||||
}
|
||||
|
||||
// Show current save path or "Not saved yet"
|
||||
let path_display = self
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|p| p.to_str())
|
||||
.unwrap_or("Not saved yet");
|
||||
ui.label(path_display);
|
||||
}
|
||||
EditorMode::EditTemplate => {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
self.editor_ui(
|
||||
ui,
|
||||
new_field_name,
|
||||
new_field_type,
|
||||
new_field_required,
|
||||
new_field_description,
|
||||
);
|
||||
|
||||
// Save/Cancel buttons
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save Template").clicked() {
|
||||
if let Some(_path) = &self.path {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
} else {
|
||||
// Open save dialog
|
||||
let mut dialog = egui_file::FileDialog::save_file(Some(
|
||||
PROJECT_FOLDER.clone(),
|
||||
));
|
||||
dialog.open();
|
||||
self.dialog = Some(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Cancel").clicked() {
|
||||
self.editor_mode = EditorMode::View;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewer_ui(&self, ui: &mut egui::Ui, new_instance: &mut RightPanelContent) {
|
||||
// Show template view
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading(&self.name);
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(description) = &self.description {
|
||||
ui.separator();
|
||||
ui.label(description);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Fields");
|
||||
|
||||
for field in &self.fields {
|
||||
ui.separator();
|
||||
// Save/Cancel buttons
|
||||
ui.horizontal(|ui| {
|
||||
ui.strong(&field.name);
|
||||
ui.label(format!("({:?})", field.field_type));
|
||||
if field.required {
|
||||
ui.label("*");
|
||||
if ui.button("Save Template").clicked() {
|
||||
if let Err(e) = self.save() {
|
||||
eprintln!("Failed to save: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("Cancel").clicked() {
|
||||
// load default state
|
||||
*self = Self::load(&self.id).unwrap();
|
||||
}
|
||||
|
||||
if ui.button("Create New Instance").clicked() {
|
||||
if self.saved {
|
||||
*new_instance = 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(desc) = &field.description {
|
||||
ui.label(desc);
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
if ui.button("Create New Instance").clicked() {
|
||||
*new_instance = RightPanelContent::Instance {
|
||||
instance: Box::new(ObjectInstance::new(self)),
|
||||
path: None,
|
||||
};
|
||||
}
|
||||
self.editor_ui(
|
||||
ui,
|
||||
new_field_name,
|
||||
new_field_type,
|
||||
new_field_required,
|
||||
new_field_description,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,19 +174,57 @@ impl Template {
|
||||
new_field_required: &mut bool,
|
||||
new_field_description: &mut String,
|
||||
) {
|
||||
// Template name and description
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Template Name:");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});
|
||||
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.horizontal(|ui| {
|
||||
ui.label("Description:");
|
||||
ui.text_edit_multiline(self.description.get_or_insert_with(String::new));
|
||||
});
|
||||
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();
|
||||
ui.heading("Fields");
|
||||
|
||||
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;
|
||||
@@ -220,52 +233,73 @@ impl Template {
|
||||
|
||||
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||
.show_header(ui, |ui| {
|
||||
ui.label(field.name.clone());
|
||||
if ui.button("❌").clicked() {
|
||||
to_remove = Some(i);
|
||||
}
|
||||
ui.label(field.name.clone());
|
||||
})
|
||||
.body(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Name:");
|
||||
ui.text_edit_singleline(&mut field.name);
|
||||
});
|
||||
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.horizontal(|ui| {
|
||||
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,
|
||||
] {
|
||||
ui.selectable_value(
|
||||
&mut field.field_type,
|
||||
variant.clone(),
|
||||
format!("{variant:?}"),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
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.horizontal(|ui| {
|
||||
ui.checkbox(&mut field.required, "Required");
|
||||
});
|
||||
ui.label("Required:");
|
||||
if ui.checkbox(&mut field.required, "").clicked() {
|
||||
self.saved = false;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Description:");
|
||||
ui.text_edit_singleline(field.description.get_or_insert_with(String::new));
|
||||
});
|
||||
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
|
||||
@@ -273,13 +307,14 @@ impl Template {
|
||||
ui.heading("Add New Field");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
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.horizontal(|ui| {
|
||||
ui.label("Type:");
|
||||
egui::ComboBox::from_id_salt("new_field_type")
|
||||
.selected_text(format!("{new_field_type:?}"))
|
||||
@@ -298,35 +333,36 @@ impl Template {
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(new_field_required, "Required");
|
||||
});
|
||||
ui.label("Required:");
|
||||
ui.checkbox(new_field_required, "");
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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())
|
||||
},
|
||||
});
|
||||
|
||||
// Reset new field form
|
||||
new_field_name.clear();
|
||||
*new_field_required = false;
|
||||
new_field_description.clear();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user