build form from yaml

session-crate
Ondřej Hruška 4 years ago
parent a1b8f5a1a1
commit faa41feaf6
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      .gitignore
  2. 71
      Cargo.lock
  3. 3
      Cargo.toml
  4. 32
      data/repository.yaml
  5. 38
      src/main.rs
  6. 127
      src/store/form.rs
  7. 94
      src/store/mod.rs
  8. 96
      src/store/model.rs
  9. 67
      templates/form_macros.html.tera
  10. 55
      templates/index.html.tera
  11. 11
      templates/layout.html.tera
  12. 27
      templates/static/style.css

2
.gitignore vendored

@ -1,4 +1,4 @@
/target
**/*.rs.bk
.idea/
inventory.json

71
Cargo.lock generated

@ -174,6 +174,11 @@ dependencies = [
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dtoa"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "educe"
version = "0.4.2"
@ -304,6 +309,28 @@ dependencies = [
"unicode-normalization 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ifmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ifmt-impl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ifmt-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "indexmap"
version = "1.3.0"
@ -382,6 +409,11 @@ name = "libc"
version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "linked-hash-map"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lock_api"
version = "0.3.2"
@ -633,6 +665,16 @@ dependencies = [
"sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
version = "0.4.30"
@ -733,13 +775,16 @@ dependencies = [
name = "rocket-inv"
version = "0.1.0"
dependencies = [
"ifmt 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"json_dotpath 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rocket 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rocket-download-response 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
"rocket_contrib 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -841,6 +886,17 @@ dependencies = [
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_yaml"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
"yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha-1"
version = "0.8.1"
@ -1136,6 +1192,14 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "yaml-rust"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "yansi"
version = "0.4.0"
@ -1169,6 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum devise_codegen 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7"
"checksum devise_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487"
"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
"checksum educe 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d27c760a73a13abb1ddf417c06bc2b8d14b0607dd19097316e0ca9412a971088"
"checksum error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9"
"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
@ -1185,6 +1250,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
"checksum hyper 0.10.16 (registry+https://github.com/rust-lang/crates.io-index)" = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273"
"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e"
"checksum ifmt 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "abec215007c2ef1ccfb17a6bae6a87736fedb573860d2606ddec08b427666164"
"checksum ifmt-impl 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "744691ef283c5d8d4321f75cfa0e3b460b5adf3338bb7dcfd060f33e40300072"
"checksum indexmap 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712d7b3ea5827fcb9d4fda14bf4da5f136f0db2ae9c8f4bd4e2d1c6fde4e6db2"
"checksum inotify 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40b54539f3910d6f84fbf9a643efd6e3aa6e4f001426c0329576128255994718"
"checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0"
@ -1196,6 +1263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
"checksum lock_api 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e57b3997725d2b60dbec1297f6c2e2957cc383db1cebd6be812163f969c7d586"
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
@ -1224,6 +1292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
"checksum pest_generator 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9fcf299b5712d06ee128a556c94709aaa04512c4dffb8ead07c5c998447fc0"
"checksum pest_meta 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "df43fd99896fd72c485fe47542c7b500e4ac1e8700bf995544d1317a60ded547"
"checksum proc-macro-hack 0.5.11 (registry+https://github.com/rust-lang/crates.io-index)" = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
"checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
@ -1245,6 +1314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
"checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7"
"checksum serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)" = "691b17f19fc1ec9d94ec0b5864859290dff279dbd7b03f017afda54eb36c3c35"
"checksum sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "23962131a91661d643c98940b20fcaffe62d776a823247be80a48fcb8b6fce68"
"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
"checksum slug 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
@ -1285,5 +1355,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d"
"checksum yansi 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d60c3b48c9cdec42fb06b3b84b5b087405e1fa1c644a1af3930e4dfafe93de48"
"checksum yansi 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"

@ -12,9 +12,12 @@ rocket-download-response = "0.4.9"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8.11"
json_dotpath = "0.1.2"
ifmt = "0.2.0"
parking_lot = "0.10.0"
lazy_static = "1.4.0"
[dependencies.rocket]
version = "0.4.2"

@ -0,0 +1,32 @@
model:
fields:
category:
type: "free_enum"
generic_code:
type: "free_enum"
code:
type: "string"
value:
type: "string"
package:
type: "free_enum"
mounting:
type: "enum"
options:
- "SMD"
- "Through-hole"
- "Screw"
quantity:
type: "int"
min: 0
location:
type: "free_enum"
sublocation:
label: "Sub-location"
type: "string"
tags:
type: "free_tags"
note:
type: "text"
checkbox:
type: "bool"

@ -5,7 +5,7 @@
//use rocket::request::FromSegments;
//use rocket::http::uri::Segments;
//use rocket_contrib::serve::StaticFiles;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template;
mod store;
@ -16,24 +16,42 @@ use rocket::response::Redirect;
use rocket::http::Status;
use rocket::request::Form;
use std::collections::HashMap;
use std::env;
use crate::store::form::RenderedField;
#[derive(Serialize)]
struct FormContext<'a> {
pub fields : Vec<RenderedField<'a>>,
}
#[get("/")]
fn index(store : State<RwLock<Store>>) -> Template {
let mut context = HashMap::new();
let rg = store.read();
context.insert("records", &rg.parts);
let indexes = &rg.index;
let context = FormContext {
fields: rg.model.fields.iter().map(|(key, field)| {
RenderedField::from_template_field(key, field, None, indexes)
}).collect()
};
Template::render("index", context)
}
#[post("/add", data="<record>")]
fn add_part(store : State<RwLock<Store>>, record : Form<store::Part>) -> Redirect {
store.write().add(record.into_inner());
Redirect::to(uri!(index))
}
//#[post("/add", data="<record>")]
//fn add_part(store : State<RwLock<Store>>, record : Form<store::Part>) -> Redirect {
// store.write().add(record.into_inner());
// Redirect::to(uri!(index))
//}
fn main() {
let cwd = env::current_dir().unwrap();
let data_dir = cwd.join("data");
let store = Store::new(data_dir);
rocket::ignite()
.attach(Template::fairing())
.manage(RwLock::new(Store::new()))
.mount("/", routes![index, add_part]).launch();
.manage(RwLock::new(store))
.mount("/", StaticFiles::from(cwd.join("templates/static/")))
.mount("/", routes![index]).launch();
}

@ -0,0 +1,127 @@
use crate::store::model::FieldKind;
use serde_json::Value;
use std::borrow::Cow;
use crate::store::{model, Indexes};
use lazy_static::lazy_static;
lazy_static! {
/// This is an example for using doc comment attributes
static ref EMPTY_VEC: Vec<String> = vec![];
}
#[derive(Serialize, Debug, Default)]
pub struct RenderedField<'a> {
pub key: Cow<'a, str>,
pub label: Cow<'a, str>,
pub kind: &'static str,
pub step: &'static str,
pub min: String,
pub max: String,
pub options: Option<&'a Vec<String>>,
pub value: Cow<'a, str>,
pub checked: bool,
}
impl<'a> RenderedField<'a> {
pub fn from_template_field<'i>(
key: &'i String,
field: &'i model::Field,
value: Option<&'i Value>,
index: &'i Indexes
) -> RenderedField<'i> {
let mut rendered = RenderedField::default();
rendered.key = key.as_str().into();
rendered.label = if field.label.is_empty() {
rendered.key.clone()
} else {
field.label.as_str().into()
};
match &field.kind {
FieldKind::String => {
rendered.kind = "string";
if let Some(Value::String(s)) = value {
rendered.value = Cow::Borrowed(&s.as_str());
}
}
FieldKind::Text => {
rendered.kind = "text";
if let Some(Value::String(s)) = value {
rendered.value = Cow::Borrowed(&s.as_str());
}
}
FieldKind::Bool { default } => {
rendered.kind = "bool";
rendered.checked = if let Some(Value::Bool(v)) = value {
*v
} else {
*default
}
}
FieldKind::Int { min, max, default } => {
rendered.kind = "number";
let num = if let Some(Value::Number(n)) = value {
n.as_i64().expect("Error parsing number")
} else {
*default
};
if let Some(n) = min {
rendered.min = n.to_string();
}
if let Some(n) = max {
rendered.max = n.to_string();
}
rendered.value = Cow::Owned(num.to_string());
rendered.step = "1";
}
FieldKind::Float { min, max, default } => {
rendered.kind = "number";
let num = if let Some(Value::Number(n)) = value {
n.as_f64().expect("Error parsing number")
} else {
*default
};
if let Some(n) = min {
rendered.min = n.to_string();
}
if let Some(n) = max {
rendered.max = n.to_string();
}
rendered.value = Cow::Owned(num.to_string());
rendered.step = "any";
}
FieldKind::Enum { options, default } => {
rendered.kind = "select";
rendered.options = Some(options);
}
FieldKind::FreeEnum { enum_group } => {
rendered.kind = "free_select";
let group = enum_group.as_ref().unwrap_or(key);
rendered.options = Some(index.free_enums.get(group).unwrap_or(&EMPTY_VEC))
}
FieldKind::Tags { options } => {
rendered.kind = "select";
rendered.options = Some(options);
}
FieldKind::FreeTags { tag_group } => {
rendered.kind = "free_select";
let group = tag_group.as_ref().unwrap_or(key);
rendered.options = Some(index.free_tags.get(group).unwrap_or(&EMPTY_VEC))
}
}
rendered
}
}

@ -1,52 +1,88 @@
use std::fs::{File, OpenOptions};
use std::io::{Read, Write, Error};
use std::path::Path;
use std::path::{Path, PathBuf};
use rocket::request::FromForm;
use crate::store::model::Model;
use std::collections::HashMap;
#[derive(Serialize,Deserialize)]
pub mod model;
pub mod form;
/// Store instance
#[derive(Debug)]
pub struct Store {
pub parts : Vec<Part>
path : PathBuf,
pub model: Model,
pub items : HashMap<usize, serde_json::Value>,
pub index : Indexes,
}
#[derive(Serialize,Deserialize,FromForm)]
pub struct Part {
name : String,
quantity : usize,
location : String,
/// Indexes loaded from the indexes file
#[derive(Serialize,Deserialize,Debug,Default)]
pub struct Indexes {
pub free_enums : HashMap<String, Vec<String>>,
pub free_tags : HashMap<String, Vec<String>>,
}
fn load_file_or(file : impl AsRef<Path>, def : String) -> String {
let mut file = match File::open(file) {
Ok(file) => file,
Err(_) => return def
};
let mut buf = String::new();
if file.read_to_string(&mut buf).is_err() {
return def;
}
buf
/// Struct loaded from the repositroy config file
#[derive(Serialize,Deserialize,Debug)]
struct RepositoryConfig {
pub model : Model,
}
const REPO_CONFIG_FILE : &'static str = "repository.yaml";
const REPO_DATA_FILE : &'static str = "data.json";
const REPO_INDEX_FILE : &'static str = "index.json";
impl Store {
pub fn new() -> Self {
let mut file = load_file_or("inventory.json", "[]".to_string());
pub fn new(path: impl AsRef<Path>) -> Self {
let file = load_file(path.as_ref().join(REPO_CONFIG_FILE));
let repository_config : RepositoryConfig = serde_yaml::from_str(&file)
.expect("Error parsing repository config file.");
let items = load_file_or(path.as_ref().join(REPO_DATA_FILE), "{}");
let indexes = load_file_or(path.as_ref().join(REPO_INDEX_FILE), "{}");
Store {
parts: serde_json::from_str(&file).unwrap(),
path: path.as_ref().into(),
model: repository_config.model,
items: serde_json::from_str(&items).expect("Error parsing data file."),
index: serde_json::from_str(&indexes).unwrap_or_default(),
}
}
pub fn add(&mut self, part : Part) {
self.parts.push(part);
pub fn persist(&self) {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(self.path.join(REPO_DATA_FILE))
.expect("Error opening data file for writing.");
self.persist()
let serialized = serde_json::to_string(&self.items).expect("Error serialize.");
file.write(serialized.as_bytes()).expect("Error write data file");
}
}
pub fn persist(&self) {
let mut file = OpenOptions::new().write(true).create(true).truncate(true).open("inventory.json").unwrap();
fn load_file(path: impl AsRef<Path>) -> String {
let mut file= File::open(&path).expect(&format!("Error opening file {}", path.as_ref().display()));
let mut buf = String::new();
file.read_to_string(&mut buf).expect(&format!("Error reading file {}", path.as_ref().display()));
buf
}
fn load_file_or(file : impl AsRef<Path>, def : impl Into<String>) -> String {
let mut file = match File::open(file) {
Ok(file) => file,
Err(_) => return def.into()
};
file.write(serde_json::to_string(&self.parts).unwrap().as_bytes()).unwrap();
let mut buf = String::new();
if file.read_to_string(&mut buf).is_err() {
return def.into();
}
buf
}

@ -0,0 +1,96 @@
use std::collections::HashMap;
/// A data card's model.
/// Cards of one model can be sorted, searched and filtered by their fields.
#[derive(Serialize, Deserialize, Debug)]
pub struct Model {
/// Fields defined by this model
pub fields: HashMap<String, Field>,
}
/// One field of a model
#[derive(Serialize, Deserialize, Debug)]
pub struct Field {
/// Field label shown in the user interface
#[serde(default)]
pub label: String,
/// Field data type
#[serde(flatten)]
pub kind: FieldKind,
}
/// Field data type and validations
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum FieldKind {
/// Single-line text entry
String,
/// Long-form text entry, can have multiple rows
Text,
/// Checkbox or a toggle switch
Bool {
/// Default value when the model's card is created
#[serde(default)]
default: bool,
},
/// Integer entry
Int {
/// Lowest allowed value
#[serde(default)]
min: Option<i64>,
/// Highest allowed value
#[serde(default)]
max: Option<i64>,
/// Default value
#[serde(default)]
default: i64,
},
/// Floating point entry
Float {
/// Lowest allowed value
#[serde(default)]
min: Option<f64>,
/// Highest allowed value
#[serde(default)]
max: Option<f64>,
/// Default value
#[serde(default)]
default: f64,
},
/// Enumeration entry with a fixed set of options
Enum {
/// Options to choose from, must not be empty
options: Vec<String>,
/// Default option (if not the first)
#[serde(default)]
default: Option<String>,
},
/// Enum that can be freely expanded by the user
FreeEnum {
/// Group name.
/// If not set, a private group for this particular field is used.
#[serde(default)]
enum_group: Option<String>,
},
/// Tags with a fixed set of options to choose from
Tags {
/// Options to choose from
options: Vec<String>,
},
/// Tags that can be freely added by the user
FreeTags {
/// Group name.
/// If not set, a private group for this particular field is used.
#[serde(default)]
tag_group: Option<String>,
},
}

@ -0,0 +1,67 @@
{% macro text(field) %}
<label for="field-{{field.key}}">
{{field.label}}
</label>
<input id="field-{{field.key}}"
name="{{field.key}}"
type="text"
value="{{field.value}}">
{% endmacro %}
{% macro longtext(field) %}
<label for="field-{{field.key}}">
{{field.label}}
</label>
<textarea id="field-{{field.key}}" name="{{field.key}}">{{field.value}}</textarea>
{% endmacro %}
{% macro free_select(field) %}
<label for="field-{{field.key}}">
{{field.label}}
</label>
<input id="field-{{field.key}}"
name="{{field.key}}"
type="text"
list="suggestions-{{field.key}}"
value="{{field.value}}">
<datalist id="suggestions-{{field.key}}">
{% for option in field.options %}
<option value="{{option}}">
{% endfor %}
</datalist>
{% endmacro %}
{% macro number(field) %}
<label for="field-{{field.key}}">
{{field.label}}
</label>
<input id="field-{{field.key}}"
name="{{field.key}}"
type="number"
value="{{field.value}}"
step="{{field.step}}"
min="{{field.min}}"
max="{{field.max}}">
{% endmacro %}
{% macro checkbox(field) %}
<label for="field-{{field.key}}">
{{field.label}}
</label>
<input id="field-{{field.key}}"
name="{{field.key}}"
type="checkbox"
{% if field.checked %}checked{% endif %}>
{% endmacro %}
{% macro select(field) %}
<label for="field-{{field.key}}">
{{field.label}}
</label>
<select id="field-{{field.key}}" name="{{field.key}}">
{% for option in field.options %}
<option value="{{option}}">{{option}}</option>
{% endfor %}
</select>
{% endmacro %}

@ -1,16 +1,47 @@
<!DOCTYPE html>
{% extends "layout" %}
{% import "form_macros" as form %}
{% block title -%}
Form
{%- endblock title %}
{% block content %}
<form action="/add" method="POST">
Name: <input type="text" name="name"><br>
Qty: <input type="number" step="1" min="0" name="quantity"><br>
Location: <input type="text" name="location"><br>
{% for field in fields %}
<div class="Row">
{% if field.kind == "string" %}
{{ form::text(field=field) }}
{% elif field.kind == "text" %}
{{ form::longtext(field=field) }}
{% elif field.kind == "number" %}
{{ form::number(field=field) }}
{% elif field.kind == "bool" %}
{{ form::checkbox(field=field) }}
{% elif field.kind == "select" %}
{{ form::select(field=field) }}
{% elif field.kind == "free_select" %}
{{ form::free_select(field=field) }}
{% else %}
{{ field.key }}
{% endif %}
</div>
{% endfor %}
<button type="submit">Add</button>
</form>
<ul>
{% for record in records %}
<li>
{{ record.name }} x {{ record.quantity }} in {{ record.location }}
</li>
{% endfor %}
</ul>
{% endblock content %}

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock title %}</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
{% block content %}{% endblock content %}
</body>
</html>

@ -0,0 +1,27 @@
*, *::before, *::after {
box-sizing: border-box;
}
form .Row {
display: flex;
padding: .25rem;
}
form .Row label {
flex-shrink: 0;
width: 10rem;
text-align: right;
display: inline-block;
padding-right: .5rem;
}
form .Row input,
form .Row select {
height: 2.1rem;
}
form .Row textarea {
flex-shrink: 1;
width: 30rem;
height: 6rem;
}
Loading…
Cancel
Save