460 lines
15 KiB
Rust
460 lines
15 KiB
Rust
use chrono::NaiveDate;
|
|
use core::fmt;
|
|
use egui::ScrollArea;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{
|
|
PROJECT_FOLDER, RightPanelContent,
|
|
editors::object_editor::ObjectInstance,
|
|
util::{self, Error},
|
|
};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub enum FieldType {
|
|
Image,
|
|
SingleLine,
|
|
MultiLine,
|
|
Date,
|
|
Number,
|
|
Link { template_id: Option<String> },
|
|
Links,
|
|
}
|
|
|
|
impl Default for FieldType {
|
|
fn default() -> Self {
|
|
Self::SingleLine
|
|
}
|
|
}
|
|
|
|
impl FieldType {
|
|
fn types() -> Vec<FieldType> {
|
|
vec![
|
|
FieldType::Image,
|
|
FieldType::SingleLine,
|
|
FieldType::MultiLine,
|
|
FieldType::Date,
|
|
FieldType::Number,
|
|
FieldType::Link { template_id: None },
|
|
FieldType::Links,
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum FieldValue {
|
|
Image(String),
|
|
SingleLine(String),
|
|
MultiLine(String),
|
|
Date(NaiveDate),
|
|
Number(f64),
|
|
Link(String),
|
|
Links(Vec<String>),
|
|
}
|
|
|
|
impl FieldValue {
|
|
pub fn from_type(_type: &FieldType) -> Self {
|
|
match _type {
|
|
FieldType::Image => Self::Image(String::new()),
|
|
FieldType::SingleLine => Self::SingleLine(String::new()),
|
|
FieldType::MultiLine => Self::MultiLine(String::new()),
|
|
FieldType::Date => Self::Date(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()),
|
|
FieldType::Number => Self::Number(0.0),
|
|
FieldType::Link { template_id: None } => Self::Link(String::new()),
|
|
FieldType::Link {
|
|
template_id: Some(template_id),
|
|
} => Self::Link(template_id.clone()),
|
|
FieldType::Links => Self::Links(Vec::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for FieldValue {
|
|
fn default() -> Self {
|
|
Self::SingleLine(String::new())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FieldDefinition {
|
|
pub name: String,
|
|
pub field_type: FieldType,
|
|
pub required: bool,
|
|
|
|
#[serde(default)]
|
|
pub on_preview: bool,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct Template {
|
|
pub name: String,
|
|
pub id: String,
|
|
|
|
pub description: Option<String>,
|
|
pub fields: Vec<FieldDefinition>,
|
|
|
|
#[serde(skip)]
|
|
pub saved: bool,
|
|
|
|
#[serde(skip)]
|
|
pub error: Option<Error>,
|
|
|
|
#[serde(skip)]
|
|
pub new_field_name: String,
|
|
|
|
#[serde(skip)]
|
|
pub new_field_type: FieldType,
|
|
|
|
#[serde(skip)]
|
|
pub new_field_required: bool,
|
|
|
|
#[serde(skip)]
|
|
pub new_field_description: String,
|
|
|
|
#[serde(skip)]
|
|
pub new_field_on_preview: bool,
|
|
}
|
|
|
|
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)
|
|
.field("new_field_name", &self.new_field_name)
|
|
.field("new_field_type", &self.new_field_type)
|
|
.field("new_field_required", &self.new_field_required)
|
|
.field("new_field_description", &self.new_field_description)
|
|
.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,
|
|
|
|
new_field_name: "".to_string(),
|
|
new_field_type: FieldType::default(),
|
|
new_field_required: false,
|
|
new_field_description: "".to_string(),
|
|
new_field_on_preview: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
|
|
new_field_name: "".to_string(),
|
|
new_field_type: FieldType::default(),
|
|
new_field_required: false,
|
|
new_field_description: "".to_string(),
|
|
new_field_on_preview: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Template {
|
|
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.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(path, content)?;
|
|
self.saved = true;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn ui(&mut self, ui: &mut egui::Ui, new_instance: &mut Option<RightPanelContent>) {
|
|
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));
|
|
// });
|
|
// });
|
|
util::saved_status(ui, self.saved, &self.id, &self.name);
|
|
|
|
// 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(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);
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
|
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.strong(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::types() {
|
|
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("On Preview:");
|
|
if ui.checkbox(&mut field.on_preview, "").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(&mut self.new_field_name);
|
|
ui.end_row();
|
|
|
|
ui.label("Type:");
|
|
egui::ComboBox::from_id_salt("new_field_type")
|
|
.selected_text(format!("{:?}", self.new_field_type))
|
|
.show_ui(ui, |ui| {
|
|
for variant in FieldType::types() {
|
|
ui.selectable_value(
|
|
&mut self.new_field_type,
|
|
variant.clone(),
|
|
format!("{variant:?}"),
|
|
);
|
|
}
|
|
});
|
|
ui.end_row();
|
|
|
|
ui.label("Required:");
|
|
ui.checkbox(&mut self.new_field_required, "");
|
|
ui.end_row();
|
|
|
|
ui.label("On Preview:");
|
|
ui.checkbox(&mut self.new_field_on_preview, "");
|
|
ui.end_row();
|
|
|
|
ui.label("Description:");
|
|
ui.text_edit_singleline(&mut self.new_field_description);
|
|
ui.end_row();
|
|
|
|
if ui.button("Add Field").clicked() && !self.new_field_name.is_empty() {
|
|
self.fields.push(FieldDefinition {
|
|
name: self.new_field_name.clone(),
|
|
field_type: self.new_field_type.clone(),
|
|
on_preview: self.new_field_on_preview,
|
|
required: self.new_field_required,
|
|
description: if self.new_field_description.is_empty() {
|
|
None
|
|
} else {
|
|
Some(self.new_field_description.clone())
|
|
},
|
|
});
|
|
|
|
self.saved = false;
|
|
|
|
// Reset new field form
|
|
self.new_field_name.clear();
|
|
self.new_field_required = false;
|
|
self.new_field_description.clear();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|