card editing, keeping order, fixes
This commit is contained in:
+185
-41
@@ -1,9 +1,11 @@
|
||||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
#[macro_use] extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{json, Value, Number};
|
||||
|
||||
//use rocket::request::FromSegments;
|
||||
//use rocket::http::uri::Segments;
|
||||
@@ -11,52 +13,32 @@ use rocket_contrib::serve::StaticFiles;
|
||||
use rocket_contrib::templates::Template;
|
||||
|
||||
mod store;
|
||||
|
||||
use crate::store::Store;
|
||||
use rocket::State;
|
||||
use rocket::{State, Data};
|
||||
use parking_lot::RwLock;
|
||||
use rocket::response::Redirect;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::Form;
|
||||
use std::collections::HashMap;
|
||||
use rocket::request::{Form, LenientForm, FromForm, FormItems};
|
||||
use std::env;
|
||||
use crate::store::form::RenderedField;
|
||||
use crate::store::form::{RenderedField, render_empty_fields, RenderedCard, render_card_fields};
|
||||
use crate::store::model::FieldKind;
|
||||
use std::ops::Deref;
|
||||
use indexmap::map::IndexMap;
|
||||
|
||||
fn render_empty_fields<'a>(store : &'a Store) -> Vec<RenderedField<'a>> {
|
||||
let indexes = &store.index;
|
||||
|
||||
store.model.fields.iter().map(|(key, field)| {
|
||||
RenderedField::from_template_field(key, field, None, indexes)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
#[derive(Serialize,Debug)]
|
||||
struct RenderedCard<'a> {
|
||||
pub fields : Vec<RenderedField<'a>>,
|
||||
pub id : usize,
|
||||
}
|
||||
|
||||
fn render_card_fields<'a>(store : &'a Store, values : &'a serde_json::Map<String, Value>) -> Vec<RenderedField<'a>> {
|
||||
let indexes = &store.index;
|
||||
|
||||
store.model.fields.iter().map(|(key, field)| {
|
||||
RenderedField::from_template_field(key, field, values.get(key), indexes)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
#[derive(Serialize,Debug)]
|
||||
struct ListContext<'a> {
|
||||
pub fields : Vec<RenderedField<'a>>,
|
||||
pub cards : Vec<RenderedCard<'a>>,
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct ListContext<'a> {
|
||||
pub fields: Vec<RenderedField<'a>>,
|
||||
pub cards: Vec<RenderedCard<'a>>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn index(store : State<RwLock<Store>>) -> Template {
|
||||
|
||||
fn route_index(store: State<RwLock<Store>>) -> Template {
|
||||
let rg = store.read();
|
||||
|
||||
let context = ListContext {
|
||||
fields: render_empty_fields(&rg),
|
||||
cards: rg.items.iter().filter_map(|(id, card)| {
|
||||
cards: rg.data.cards.iter().filter_map(|(id, card)| {
|
||||
if let Value::Object(map) = card {
|
||||
Some(RenderedCard {
|
||||
fields: render_card_fields(&rg, map),
|
||||
@@ -71,14 +53,137 @@ fn index(store : State<RwLock<Store>>) -> Template {
|
||||
Template::render("index", context)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MapFromForm {
|
||||
pub data: IndexMap<String, String>
|
||||
}
|
||||
|
||||
impl<'a> FromForm<'a> for MapFromForm {
|
||||
type Error = ();
|
||||
|
||||
fn from_form(items: &mut FormItems, _strict: bool) -> Result<Self, Self::Error> {
|
||||
let mut new = MapFromForm::default();
|
||||
items.for_each(|item| {
|
||||
let (k, v) = item.key_value_decoded();
|
||||
new.data.insert(k, v);
|
||||
});
|
||||
Ok(new)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_card_form(store : &Store, mut form : MapFromForm) -> IndexMap::<String, Value> {
|
||||
let mut card = IndexMap::new();
|
||||
|
||||
for (k, field) in &store.model.fields {
|
||||
let mut value: Option<Value> = None;
|
||||
if let Some(input) = form.data.remove(k) {
|
||||
value = Some(match &field.kind {
|
||||
FieldKind::Text | FieldKind::String | FieldKind::FreeEnum { .. } => {
|
||||
Value::String(input)
|
||||
}
|
||||
FieldKind::Bool { .. } => {
|
||||
serde_json::to_value(true).unwrap()
|
||||
}
|
||||
FieldKind::Int { min, max, default } => {
|
||||
let mut val: i64 = if input.is_empty() {
|
||||
*default
|
||||
} else {
|
||||
input.parse().expect("Error parse number")
|
||||
};
|
||||
|
||||
if let Some(min) = min {
|
||||
val = val.max(*min);
|
||||
}
|
||||
|
||||
if let Some(max) = max {
|
||||
val = val.min(*max);
|
||||
}
|
||||
|
||||
serde_json::to_value(val).unwrap()
|
||||
}
|
||||
FieldKind::Float { min, max, default } => {
|
||||
let mut val: f64 = if input.is_empty() {
|
||||
*default
|
||||
} else {
|
||||
input.parse().expect("Error parse number")
|
||||
};
|
||||
|
||||
if let Some(min) = min {
|
||||
val = val.min(*min);
|
||||
}
|
||||
|
||||
if let Some(max) = max {
|
||||
val = val.max(*max);
|
||||
}
|
||||
|
||||
serde_json::to_value(val).unwrap()
|
||||
}
|
||||
FieldKind::Enum { options, default } => {
|
||||
if options.contains(&input) {
|
||||
Value::String(input)
|
||||
} else {
|
||||
let val = default
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| options
|
||||
.first()
|
||||
.expect("fixed enum must have values")
|
||||
.to_owned()
|
||||
);
|
||||
|
||||
Value::String(val)
|
||||
}
|
||||
}
|
||||
FieldKind::Tags { options } => {
|
||||
let tags: Vec<String> = input.split(' ')
|
||||
.map(ToOwned::to_owned)
|
||||
.filter_map(|tag| {
|
||||
if options.contains(&tag) {
|
||||
Some(tag)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect();
|
||||
|
||||
serde_json::to_value(tags).unwrap()
|
||||
}
|
||||
FieldKind::FreeTags { .. } => {
|
||||
let tags: Vec<String> = input.split(' ')
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
|
||||
serde_json::to_value(tags).unwrap()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if let FieldKind::Bool { .. } = field.kind {
|
||||
value = Some(Value::Bool(false));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(v) = value {
|
||||
card.insert(k.to_owned(), v);
|
||||
}
|
||||
}
|
||||
|
||||
card
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AddCardContext<'a> {
|
||||
pub fields : Vec<RenderedField<'a>>,
|
||||
pub fields: Vec<RenderedField<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EditCardContext<'a> {
|
||||
pub fields: Vec<RenderedField<'a>>,
|
||||
pub id : usize,
|
||||
}
|
||||
|
||||
#[get("/add")]
|
||||
fn add(store : State<RwLock<Store>>) -> Template {
|
||||
|
||||
fn route_add(store: State<RwLock<Store>>) -> Template {
|
||||
let rg = store.read();
|
||||
|
||||
let context = AddCardContext {
|
||||
@@ -88,6 +193,42 @@ fn add(store : State<RwLock<Store>>) -> Template {
|
||||
Template::render("add", context)
|
||||
}
|
||||
|
||||
#[post("/add", data = "<form>")]
|
||||
fn route_add_save(form: Form<MapFromForm>, store: State<RwLock<Store>>) -> Redirect {
|
||||
let mut rg = store.write();
|
||||
|
||||
let card = collect_card_form(&rg, form.into_inner());
|
||||
rg.add_card(card);
|
||||
|
||||
Redirect::found(uri!(route_index))
|
||||
}
|
||||
|
||||
#[get("/edit/<id>")]
|
||||
fn route_edit(id : usize, store: State<RwLock<Store>>) -> Option<Template> {
|
||||
let rg = store.read();
|
||||
|
||||
if let Some(Value::Object(map)) = rg.data.cards.get(&id) {
|
||||
let context = EditCardContext {
|
||||
fields: render_card_fields(&rg, map),
|
||||
id,
|
||||
};
|
||||
|
||||
Some(Template::render("edit", context))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/edit/<id>", data = "<form>")]
|
||||
fn route_edit_save(id : usize, form: Form<MapFromForm>, store: State<RwLock<Store>>) -> Option<Redirect> {
|
||||
let mut rg = store.write();
|
||||
|
||||
let card = collect_card_form(&rg, form.into_inner());
|
||||
rg.update_card(id, card);
|
||||
|
||||
Some(Redirect::found(uri!(route_index)))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let data_dir = cwd.join("data");
|
||||
@@ -98,7 +239,10 @@ fn main() {
|
||||
.manage(RwLock::new(store))
|
||||
.mount("/", StaticFiles::from(cwd.join("templates/static/")))
|
||||
.mount("/", routes![
|
||||
index,
|
||||
add,
|
||||
route_index,
|
||||
route_add,
|
||||
route_add_save,
|
||||
route_edit,
|
||||
route_edit_save,
|
||||
]).launch();
|
||||
}
|
||||
|
||||
+23
-1
@@ -1,7 +1,7 @@
|
||||
use crate::store::model::FieldKind;
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
use crate::store::{model, Indexes};
|
||||
use crate::store::{model, Indexes, Store};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
@@ -155,3 +155,25 @@ impl<'a> RenderedField<'a> {
|
||||
rendered
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize,Debug)]
|
||||
pub struct RenderedCard<'a> {
|
||||
pub fields : Vec<RenderedField<'a>>,
|
||||
pub id : usize,
|
||||
}
|
||||
|
||||
pub fn render_empty_fields(store : &Store) -> Vec<RenderedField> {
|
||||
let indexes = &store.index;
|
||||
|
||||
store.model.fields.iter().map(|(key, field)| {
|
||||
RenderedField::from_template_field(key, field, None, indexes)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn render_card_fields<'a>(store : &'a Store, values : &'a serde_json::Map<String, Value>) -> Vec<RenderedField<'a>> {
|
||||
let indexes = &store.index;
|
||||
|
||||
store.model.fields.iter().map(|(key, field)| {
|
||||
RenderedField::from_template_field(key, field, values.get(key), indexes)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
+43
-4
@@ -4,6 +4,9 @@ use std::path::{Path, PathBuf};
|
||||
use rocket::request::FromForm;
|
||||
use crate::store::model::Model;
|
||||
use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use indexmap::map::IndexMap;
|
||||
|
||||
pub mod model;
|
||||
pub mod form;
|
||||
@@ -13,7 +16,7 @@ pub mod form;
|
||||
pub struct Store {
|
||||
path : PathBuf,
|
||||
pub model: Model,
|
||||
pub items : HashMap<usize, serde_json::Value>,
|
||||
pub data: Cards,
|
||||
pub index : Indexes,
|
||||
}
|
||||
|
||||
@@ -30,6 +33,14 @@ struct RepositoryConfig {
|
||||
pub model : Model,
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize,Debug)]
|
||||
pub struct Cards {
|
||||
#[serde(default)]
|
||||
pub cards : IndexMap<usize, Value>,
|
||||
#[serde(default)]
|
||||
pub counter: usize,
|
||||
}
|
||||
|
||||
const REPO_CONFIG_FILE : &'static str = "repository.yaml";
|
||||
const REPO_DATA_FILE : &'static str = "data.json";
|
||||
const REPO_INDEX_FILE : &'static str = "index.json";
|
||||
@@ -47,12 +58,12 @@ impl Store {
|
||||
Store {
|
||||
path: path.as_ref().into(),
|
||||
model: repository_config.model,
|
||||
items: serde_json::from_str(&items).expect("Error parsing data file."),
|
||||
data: serde_json::from_str(&items).expect("Error parsing data file."),
|
||||
index: serde_json::from_str(&indexes).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist(&self) {
|
||||
pub fn persist(&mut self) {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
@@ -60,9 +71,37 @@ impl Store {
|
||||
.open(self.path.join(REPO_DATA_FILE))
|
||||
.expect("Error opening data file for writing.");
|
||||
|
||||
let serialized = serde_json::to_string(&self.items).expect("Error serialize.");
|
||||
let serialized = serde_json::to_string_pretty(&self.data).expect("Error serialize.");
|
||||
file.write(serialized.as_bytes()).expect("Error write data file");
|
||||
}
|
||||
|
||||
pub fn add_card(&mut self, values : IndexMap::<String, Value>) {
|
||||
let packed = serde_json::to_value(values).expect("Error serialize");
|
||||
if let p @ Value::Object(_) = packed {
|
||||
let id = self.data.counter;
|
||||
self.data.counter += 1;
|
||||
self.data.cards.insert(id, p);
|
||||
} else {
|
||||
panic!("Packing did not produce a map.");
|
||||
}
|
||||
self.persist()
|
||||
}
|
||||
|
||||
pub fn update_card(&mut self, id : usize, values : IndexMap::<String, Value>) {
|
||||
let packed = serde_json::to_value(values).expect("Error serialize");
|
||||
|
||||
if !self.data.cards.contains_key(&id) {
|
||||
panic!("No such card.");
|
||||
}
|
||||
|
||||
if let p @ Value::Object(_) = packed {
|
||||
self.data.cards.insert(id, p);
|
||||
} else {
|
||||
panic!("Packing did not produce a map.");
|
||||
}
|
||||
|
||||
self.persist()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_file(path: impl AsRef<Path>) -> String {
|
||||
|
||||
Reference in New Issue
Block a user