code: widget implementation, almost there

This commit is contained in:
Gabor Körber 2024-02-02 21:48:41 +01:00
parent e8ddfb25fa
commit 6e0a71b7de
18 changed files with 482 additions and 127 deletions

59
Cargo.lock generated
View File

@ -306,7 +306,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -317,13 +317,13 @@ checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.73" version = "0.1.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -574,7 +574,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
"syn_derive", "syn_derive",
] ]
@ -693,7 +693,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1572,7 +1572,7 @@ checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -1825,6 +1825,7 @@ name = "miniweb"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"axum 0.7.4", "axum 0.7.4",
"barrel", "barrel",
"derive_builder", "derive_builder",
@ -2032,7 +2033,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2083,7 +2084,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2153,7 +2154,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2284,9 +2285,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.67" version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2325,9 +2326,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.33" version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -2510,7 +2511,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.37", "syn 2.0.48",
"walkdir", "walkdir",
] ]
@ -2620,7 +2621,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -2678,7 +2679,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"sea-bae", "sea-bae",
"syn 2.0.37", "syn 2.0.48",
"unicode-ident", "unicode-ident",
] ]
@ -2742,7 +2743,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
"thiserror", "thiserror",
] ]
@ -2821,7 +2822,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3234,7 +3235,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3268,9 +3269,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.37" version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3286,7 +3287,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3331,7 +3332,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3415,7 +3416,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3509,7 +3510,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]
@ -3698,7 +3699,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -3732,7 +3733,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4032,7 +4033,7 @@ checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.37", "syn 2.0.48",
] ]
[[package]] [[package]]

View File

@ -51,3 +51,4 @@ env_logger = "0.11"
serde_json = "1.0.108" serde_json = "1.0.108"
slug = "0.1.5" slug = "0.1.5"
derive_builder = "0.13.0" derive_builder = "0.13.0"
async-trait = "0.1.77"

View File

@ -4,6 +4,9 @@ default:
@echo "# Miniweb Project" @echo "# Miniweb Project"
@just --list -u @just --list -u
build:
@cargo build
# Run the project (just run), or commands with --bin (just run <command>) # Run the project (just run), or commands with --bin (just run <command>)
run args='miniweb': run args='miniweb':
@cargo run --bin {{args}} @cargo run --bin {{args}}

View File

@ -175,3 +175,64 @@ As I started by quickly using a django admin template with CSS to jump start the
I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use. I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use.
### Builder Pattern etc.
// consider builder: https://docs.rs/derive_builder/latest/derive_builder/
// downside of builders as macro: no intellisense!
// each repository has to implement a repo info.
//#[derive(Builder)]
//#[builder(setter(into))]
#### static initializer
downside of a pub &'static str initializer struct is the rule to avoid any nested types that have any kind of building pattern, because you cannot properly move the data easily, especially if you want to move into enums, like an Option.
otherwise, self mutating building patterns can stay readonly either with the Copy trait and using self:
With "Copy":
fn self_mutating(mut self, value) -> Self {
self.value = value;
self
}
Without Copy:
fn self_mutating_manual(self, value) -> Self {
Struct {
value: value,
..self
}
}
However also works, but seems dirty:
fn self_mutating_instance_mut(&mut self, value) -> Self {
self.value = value;
*self
}
#### Cow
Other remarks: also useful is the usage of Cow, if you want to create internal objects that actuall reuse static data or take dynamic data.
use std::borrow::Cow;
#[derive(Debug, Serialize, Clone)]
pub struct Field {
pub widget: Cow<'static, str>,
pub label: Option<Cow<'static, str>>,
}
let static_field = Field::widget(Cow::Borrowed("/admin/widgets/input_text.jinja"));
let dynamic_label = String::from("Dynamic Label");
let dynamic_field = Field::widget(Cow::Owned(dynamic_label));
### Write article about "did you read the fineprint"
- The bleeding of lifetimes.
- 'static
- Send
- "object safe"
- the async dynamic dispatch.
- the building pattern debacle here

View File

@ -33,3 +33,9 @@ auth:
- auth middleware. - auth middleware.
- message the user. - message the user.
-> messaging system -> messaging system
## Widgets
In the widget section, we need to add more abilities to adjust attributes of html elements directly.
Probably options should be transmitted via JSON data.

View File

@ -2,8 +2,8 @@ pub use config::AdminModelConfig;
pub use dto::AdminApp; pub use dto::AdminApp;
pub use dto::AdminModel; pub use dto::AdminModel;
pub use repository::{ pub use repository::{
AdminRepository, LookupKey, RepoInfo, RepositoryInfo, RepositoryInfoBuilder, RepositoryItem, AdminRepository, LookupKey, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem,
RepositoryList, RepositoryList, Widget,
}; };
mod auth { mod auth {
@ -50,11 +50,126 @@ mod dto {
} }
pub mod repository { pub mod repository {
use super::dto::AdminModel; use super::dto::{AdminApp, AdminModel};
use async_trait::async_trait;
use derive_builder::Builder; use derive_builder::Builder;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use serde_json::Value; use serde_json::Value;
use std::vec::IntoIter; use std::{collections::HashMap, vec::IntoIter};
pub type RepositoryContext = AdminModel;
impl RepositoryContext {
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
RepositoryItem {
detail_url: Some(format!("{}/detail/{}", self.admin_url, key)),
fields: fields,
}
}
}
/// This is a static configuration object.
/// It might be changed in the future to have a dynamic counterpart.
///
/// ## Example:
/// Creating a simple required and readonly text input field with a label:
///
/// ```ignore
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
/// .labelled("Username")
/// .required()
/// .readonly();
/// ```
#[derive(Debug, Serialize, Clone, Copy)]
pub struct Widget {
pub widget: &'static str,
pub label: Option<&'static str>,
#[serde(rename = "type")]
pub field_type: &'static str,
pub required: bool,
pub readonly: bool,
pub options: &'static [(&'static str, &'static str)],
}
impl Widget {
pub fn widget(widget: &'static str) -> Self {
Widget {
widget: widget,
label: None,
field_type: "text",
required: false,
readonly: false,
options: &[],
}
}
pub fn default() -> Self {
Self::widget("/admin/widgets/input_text.jinja")
}
pub fn textarea() -> Self {
Self::widget("/admin/widgets/input_textarea.jinja")
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn readonly(mut self) -> Self {
self.readonly = true;
self
}
pub fn labeled(mut self, label: &'static str) -> Self {
self.label = Some(label);
self
}
pub fn as_password(mut self) -> Self {
self.field_type = "password";
self
}
pub fn as_hidden(mut self) -> Self {
self.field_type = "hidden";
self
}
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
self.options = options;
self
}
}
#[derive(Debug, Serialize)]
struct Field {
widget: String,
label: Option<String>,
#[serde(rename = "type")]
field_type: String,
readonly: bool,
required: bool,
options: Value,
}
impl From<Widget> for Field {
fn from(value: Widget) -> Self {
Field {
widget: value.widget.to_string(),
label: value.label.map(|s| s.to_string()),
field_type: value.field_type.to_string(),
readonly: value.readonly,
required: value.required,
options: value
.options
.iter()
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
.collect::<serde_json::Map<String, Value>>()
.into(),
}
}
}
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum LookupKey { pub enum LookupKey {
@ -156,16 +271,18 @@ pub mod repository {
pub fields: &'static [&'static str], pub fields: &'static [&'static str],
} }
// consider builder: https://docs.rs/derive_builder/latest/derive_builder/ impl RepoInfo {
// downside of builders as macro: no intellisense! pub fn build(self) -> RepositoryInfo {
// each repository has to implement a repo info. self.into()
#[derive(Serialize, Builder)] }
#[builder(setter(into))] }
#[derive(Serialize)]
pub struct RepositoryInfo { pub struct RepositoryInfo {
name: String, name: String,
lookup_key: String, lookup_key: String,
display_list: Vec<String>, display_list: Vec<String>,
fields: Vec<String>, fields: Vec<(String, Field)>,
} }
impl RepositoryInfo { impl RepositoryInfo {
@ -178,12 +295,32 @@ pub mod repository {
} }
} }
/* // self mutating builder pattern? // self mutating builder pattern
pub fn display_list(mut self, display_list: &[&str]) -> RepositoryInfo { pub fn display_list(mut self, display_list: &[&str]) -> Self {
self.display_list = display_list.iter().map(|&e| e.to_string()).collect(); self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
self self
} }
*/
// set field for a given key.
pub fn set_widget(mut self, name: &str, widget: Widget) -> Self {
// Find the index of the existing entry with the same name, if it exists
let pos = self
.fields
.iter()
.position(|(existing_name, _)| existing_name == name);
match pos {
Some(index) => {
// If found, replace the `Field` part of the tuple
self.fields[index].1 = Field::from(widget);
}
None => {
// If not found, add a new entry
self.fields.push((name.to_owned(), Field::from(widget)));
}
}
self
}
} }
impl From<RepoInfo> for RepositoryInfo { impl From<RepoInfo> for RepositoryInfo {
@ -196,22 +333,31 @@ pub mod repository {
.iter() .iter()
.map(|&s| s.to_string()) .map(|&s| s.to_string())
.collect(), .collect(),
fields: repo_info.fields.iter().map(|&s| s.to_string()).collect(), fields: repo_info
.fields
.iter()
.map(|x| (x.to_string(), Field::from(Widget::default())))
.collect(),
} }
} }
} }
#[async_trait]
pub trait AdminRepository: Send + Sync { pub trait AdminRepository: Send + Sync {
fn info(&self, model: &AdminModel) -> RepositoryInfo; async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
fn list(&self, model: &AdminModel) -> RepositoryList; async fn list(&self, context: &RepositoryContext) -> RepositoryList;
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>; async fn get(&self, context: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem>;
fn create(&mut self, model: &AdminModel, data: Value) -> Option<RepositoryItem>; async fn create(
fn update(
&mut self, &mut self,
model: &AdminModel, context: &RepositoryContext,
data: Value,
) -> Option<RepositoryItem>;
async fn update(
&mut self,
context: &RepositoryContext,
id: LookupKey, id: LookupKey,
data: Value, data: Value,
) -> Option<RepositoryItem>; ) -> Option<RepositoryItem>;
fn delete(&mut self, model: &AdminModel, id: LookupKey) -> Option<Value>; async fn delete(&mut self, context: &RepositoryContext, id: LookupKey) -> Option<Value>;
} }
} }

View File

@ -70,7 +70,6 @@ impl Default for AdminContext {
pub fn base_template(headers: &HeaderMap) -> Option<String> { pub fn base_template(headers: &HeaderMap) -> Option<String> {
let hx_request = headers.get("HX-Request").is_some(); let hx_request = headers.get("HX-Request").is_some();
if hx_request { if hx_request {
println!("HX.");
Some("admin/base_hx.jinja".to_string()) Some("admin/base_hx.jinja".to_string())
} else { } else {
None None
@ -123,8 +122,8 @@ pub async fn list_item_collection(
AdminContext { AdminContext {
base: base_template(&headers), base: base_template(&headers),
available_apps: registry.get_apps(), available_apps: registry.get_apps(),
item_info: Some(repo.info(&admin_model)), item_info: Some(repo.info(&admin_model).await),
item_list: repo.list(&admin_model), item_list: repo.list(&admin_model).await,
item_model: Some(admin_model), item_model: Some(admin_model),
..Default::default() ..Default::default()
} }
@ -163,9 +162,9 @@ pub async fn item_details(
AdminContext { AdminContext {
base: base_template(&headers), base: base_template(&headers),
available_apps: registry.get_apps(), available_apps: registry.get_apps(),
item_info: Some(repo.info(&admin_model)), item_info: Some(repo.info(&admin_model).await),
item_list: repo.list(&admin_model), item_list: repo.list(&admin_model).await,
item: repo.get(&admin_model, key), item: repo.get(&admin_model, key).await,
item_model: Some(admin_model), item_model: Some(admin_model),
..Default::default() ..Default::default()
} }
@ -193,8 +192,8 @@ pub async fn new_item(
AdminContext { AdminContext {
base: base_template(&headers), base: base_template(&headers),
available_apps: registry.get_apps(), available_apps: registry.get_apps(),
item_info: Some(repo.info(&admin_model)), item_info: Some(repo.info(&admin_model).await),
item_list: repo.list(&admin_model), item_list: repo.list(&admin_model).await,
item_model: Some(admin_model), item_model: Some(admin_model),
..Default::default() ..Default::default()
} }
@ -222,14 +221,14 @@ pub async fn create_item(
.expect("Admin Model not found?"); .expect("Admin Model not found?");
// create our item. // create our item.
let result = repo.create(&admin_model, form); let result = repo.create(&admin_model, form).await;
// TODO: refactor run over these views, way too much repetition. // TODO: refactor run over these views, way too much repetition.
AdminContext { AdminContext {
base: base_template(&headers), base: base_template(&headers),
available_apps: registry.get_apps(), available_apps: registry.get_apps(),
item_info: Some(repo.info(&admin_model)), item_info: Some(repo.info(&admin_model).await),
item_list: repo.list(&admin_model), item_list: repo.list(&admin_model).await,
item_model: Some(admin_model), item_model: Some(admin_model),
item: result, item: result,
..Default::default() ..Default::default()

View File

@ -0,0 +1,51 @@
use crate::admin::domain::*;
use crate::admin::state::AdminRegistry;
use async_trait::async_trait;
use log::{debug, warn};
use serde_json::{json, Value};
struct Repository {}
impl Repository {}
#[async_trait]
impl AdminRepository for Repository {
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
RepoInfo {
name: "My Empty Repository",
lookup_key: "id",
display_list: &["id"],
fields: &[],
}
.into()
}
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
None
}
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
RepositoryList::Empty
}
async fn create(
&mut self,
model: &RepositoryContext,
mut data: Value,
) -> Option<RepositoryItem> {
None
}
async fn update(
&mut self,
model: &RepositoryContext,
id: LookupKey,
data: Value,
) -> Option<RepositoryItem> {
None
}
async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option<Value> {
None
}
}

View File

@ -1 +1,3 @@
pub mod empty_repository;
pub mod static_repository; pub mod static_repository;
pub mod user_repository;

View File

@ -1,10 +1,11 @@
// implementation of static repository
use crate::admin::domain::*; use crate::admin::domain::*;
use crate::admin::state::AdminRegistry; use crate::admin::state::AdminRegistry;
use async_trait::async_trait;
use log::{debug, warn}; use log::{debug, warn};
use serde_json::{json, Value}; use serde_json::{json, Value};
/// This is a showcase implementation with a static repository
/// It uses a Vec<Value> to store it's data.
struct MyStaticRepository { struct MyStaticRepository {
/// In Memory Storage for Example. /// In Memory Storage for Example.
content: Vec<Value>, content: Vec<Value>,
@ -33,53 +34,47 @@ impl MyStaticRepository {
} }
} }
#[async_trait]
impl AdminRepository for MyStaticRepository { impl AdminRepository for MyStaticRepository {
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem> { async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
if let LookupKey::Integer(id) = id {
let item = self.content.get(id - 1).cloned().unwrap();
Some(RepositoryItem {
detail_url: Some(format!(
"{}/detail/{}",
model.admin_url,
item.get("id").unwrap()
)),
fields: item,
})
} else {
warn!("Got non-integer lookup key: {}", id);
None
}
}
fn list(&self, model: &AdminModel) -> RepositoryList {
RepositoryList::List {
values: self
.content
.clone()
.into_iter()
.map(|item| RepositoryItem {
detail_url: Some(format!(
"{}/detail/{}",
model.admin_url,
item.get("id").unwrap()
)),
fields: item,
})
.collect(),
}
}
fn info(&self, _model: &AdminModel) -> RepositoryInfo {
RepoInfo { RepoInfo {
name: "My Static Repository", name: "My Static Repository",
lookup_key: "id", lookup_key: "id",
display_list: &["id", "name", "level", "age"], display_list: &["id", "name", "level", "age"],
fields: &["name", "level", "age", "powers"], fields: &["name", "level", "age", "powers"],
} }
.into() .build()
.set_widget("age", Widget::default().as_password().labeled("hi there."))
.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
} }
fn create(&mut self, model: &AdminModel, mut data: Value) -> Option<RepositoryItem> { async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
if let LookupKey::Integer(id) = id {
let item = self.content.get(id - 1).cloned().unwrap();
let id = item.get("id").unwrap();
Some(model.build_item(&*id.to_string(), item))
} else {
warn!("Got non-integer lookup key: {}", id);
None
}
}
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
RepositoryList::List {
values: self
.content
.clone()
.into_iter()
.map(|item| model.build_item(&*item.get("id").unwrap().to_string(), item))
.collect(),
}
}
async fn create(
&mut self,
model: &RepositoryContext,
mut data: Value,
) -> Option<RepositoryItem> {
debug!("Asked to create: {}", data); debug!("Asked to create: {}", data);
let new_id = self.next_id; let new_id = self.next_id;
@ -93,13 +88,15 @@ impl AdminRepository for MyStaticRepository {
self.content.push(data.clone()); self.content.push(data.clone());
// Return the newly created item // Return the newly created item
Some(RepositoryItem { Some(model.build_item(&*new_id.to_string(), data))
detail_url: Some(format!("{}/detail/{}", model.admin_url, new_id)),
fields: data,
})
} }
fn update(&mut self, model: &AdminModel, id: LookupKey, data: Value) -> Option<RepositoryItem> { async fn update(
&mut self,
model: &RepositoryContext,
id: LookupKey,
data: Value,
) -> Option<RepositoryItem> {
debug!("I would now update: {}, {}", id, data); debug!("I would now update: {}, {}", id, data);
// First, find the index of the item to update // First, find the index of the item to update
@ -130,7 +127,7 @@ impl AdminRepository for MyStaticRepository {
None None
} }
fn delete(&mut self, _model: &AdminModel, id: LookupKey) -> Option<Value> { async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option<Value> {
debug!("Would delete: {}", id); debug!("Would delete: {}", id);
let item_index = self.content.iter().position(|item| { let item_index = self.content.iter().position(|item| {

View File

@ -0,0 +1,52 @@
use crate::admin::domain::AdminRepository;
use crate::admin::domain::*;
use crate::admin::state::AdminRegistry;
use async_trait::async_trait;
use log::{debug, warn};
use serde_json::{json, Value};
struct UserRepository {}
impl UserRepository {}
#[async_trait]
impl AdminRepository for UserRepository {
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
RepoInfo {
name: "User",
lookup_key: "id",
display_list: &["id"],
fields: &[],
}
.into()
}
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
None
}
async fn list(&self, model: &RepositoryContext) -> RepositoryList {
RepositoryList::Empty
}
async fn create(
&mut self,
model: &RepositoryContext,
mut data: Value,
) -> Option<RepositoryItem> {
None
}
async fn update(
&mut self,
model: &RepositoryContext,
id: LookupKey,
data: Value,
) -> Option<RepositoryItem> {
None
}
async fn delete(&mut self, _: &RepositoryContext, id: LookupKey) -> Option<Value> {
None
}
}

View File

@ -70,6 +70,12 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="{{ url( 'admin:index') }}">{{ translate('Home') }}</a> <a href="{{ url( 'admin:index') }}">{{ translate('Home') }}</a>
{% if title %} &rsaquo; {{ title }}{% endif %} {% if title %} &rsaquo; {{ title }}{% endif %}
<div>
{% for x in y %}
{% for a in b %}
{% endfor %}
{% endfor %}
</div>
</div> </div>
{% endblock %} {% endblock %}
</nav> </nav>

View File

@ -2,4 +2,9 @@
{% block content %} {% block content %}
{% include "admin/dashboard.jinja" %} {% include "admin/dashboard.jinja" %}
<div>
Some text
another text
Third text
</div>
{% endblock %} {% endblock %}

View File

@ -1,26 +1,20 @@
{% extends base|none("admin/base.jinja") %} {% extends base|none("admin/base.jinja") %}
{% macro input(name, value="", type="text") -%}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" placeholder="">
{%- endmacro %}
{% block content %} {% block content %}
{% from "/admin/items/items.jinja" import field_widget %}
<div class="ui container"> <div class="ui container">
<h1>Create {{item_model.name}} in {{item_info.name}}</h1> <h1>Create {{item_model.name}} in {{item_info.name}}</h1>
<form action="{{item_model.add_url}}" method="POST" class="ui form"> <form action="{{item_model.add_url}}" method="POST" class="ui form">
<!--noformat-->
{% set fields = item_info.fields %} {% set fields = item_info.fields %}
{% for field in fields %} {% for field_name, field_defs in fields %}
{% if item %} {% if item %}
{% set field_value = item.fields[field]|none("") %} {% set field_value = item.fields[field_name]|none("") %}
{% else %} {% else %}
{% set field_value = "" %} {% set field_value = "" %}
{% endif %} {% endif %}
{{ field_widget(field_name, field_defs, field_value) }}
<div class="inline field">
<label>{{field|capitalize}}</label>
{{ input(field, field_value) }}
</div>
{% endfor %} {% endfor %}
<button class="ui button" type="submit">Create</button> <button class="ui button" type="submit">Create</button>
</form> </form>

View File

@ -3,9 +3,9 @@
{% block content %} {% block content %}
{% if item %} {% if item %}
{{item.fields}} {{item.fields}}
{% else %} {% else %}
No Item found. No Item found.
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,15 @@
<!--noformat-->
{% macro field_widget(name, definitions, value) -%}
{% set field = {
'type': definitions.type,
'name': name,
'value': value,
'placeholder': definitions.placeholder,
'label': definitions.label,
'readonly': definitions.readonly,
'required': definitions.required,
'options': definitions.options,
}
%}
{% include definitions.widget %}
{%- endmacro %}

View File

@ -0,0 +1,4 @@
<div class="inline field">
<label>{{field.label or field.name|capitalize}}</label>
<input type="{{ field.type|none('text') }}" name="{{ field.name }}" value="{{ field.value }}" placeholder="{{field.placeholder}}">
</div>

View File

@ -0,0 +1,12 @@
<div class="inline field">
<label>{{ field.label or field.name|capitalize }}</label>
<!--noformat-->
<textarea name="{{ field.name }}" {% if field.placeholder %}placeholder="{{ field.placeholder }}"{% endif %}
{% for field_name in ['rows', 'cols', 'maxlength', 'minlength', 'wrap', 'autofocus'] %}
{% if field_name in field.options %}{{field_name}}="{{ field.options[field_name] }}" {% endif %}
{% endfor %}
{% if field.required %}required{% endif %}
{% if field.readonly %}readonly{% endif %}
{% if "disabled" in field.options and field.options.disabled %}disabled{% endif %}
>{{ field.value }}</textarea>
</div>