code: widget implementation, almost there
This commit is contained in:
@@ -2,8 +2,8 @@ pub use config::AdminModelConfig;
|
||||
pub use dto::AdminApp;
|
||||
pub use dto::AdminModel;
|
||||
pub use repository::{
|
||||
AdminRepository, LookupKey, RepoInfo, RepositoryInfo, RepositoryInfoBuilder, RepositoryItem,
|
||||
RepositoryList,
|
||||
AdminRepository, LookupKey, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem,
|
||||
RepositoryList, Widget,
|
||||
};
|
||||
|
||||
mod auth {
|
||||
@@ -50,11 +50,126 @@ mod dto {
|
||||
}
|
||||
|
||||
pub mod repository {
|
||||
use super::dto::AdminModel;
|
||||
use super::dto::{AdminApp, AdminModel};
|
||||
use async_trait::async_trait;
|
||||
use derive_builder::Builder;
|
||||
use serde::{Serialize, Serializer};
|
||||
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)]
|
||||
pub enum LookupKey {
|
||||
@@ -156,16 +271,18 @@ pub mod repository {
|
||||
pub fields: &'static [&'static str],
|
||||
}
|
||||
|
||||
// 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(Serialize, Builder)]
|
||||
#[builder(setter(into))]
|
||||
impl RepoInfo {
|
||||
pub fn build(self) -> RepositoryInfo {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RepositoryInfo {
|
||||
name: String,
|
||||
lookup_key: String,
|
||||
display_list: Vec<String>,
|
||||
fields: Vec<String>,
|
||||
fields: Vec<(String, Field)>,
|
||||
}
|
||||
|
||||
impl RepositoryInfo {
|
||||
@@ -178,12 +295,32 @@ pub mod repository {
|
||||
}
|
||||
}
|
||||
|
||||
/* // self mutating builder pattern?
|
||||
pub fn display_list(mut self, display_list: &[&str]) -> RepositoryInfo {
|
||||
// self mutating builder pattern
|
||||
pub fn display_list(mut self, display_list: &[&str]) -> Self {
|
||||
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
|
||||
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 {
|
||||
@@ -196,22 +333,31 @@ pub mod repository {
|
||||
.iter()
|
||||
.map(|&s| s.to_string())
|
||||
.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 {
|
||||
fn info(&self, model: &AdminModel) -> RepositoryInfo;
|
||||
fn list(&self, model: &AdminModel) -> RepositoryList;
|
||||
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem>;
|
||||
fn create(&mut self, model: &AdminModel, data: Value) -> Option<RepositoryItem>;
|
||||
fn update(
|
||||
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
|
||||
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
|
||||
async fn get(&self, context: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem>;
|
||||
async fn create(
|
||||
&mut self,
|
||||
model: &AdminModel,
|
||||
context: &RepositoryContext,
|
||||
data: Value,
|
||||
) -> Option<RepositoryItem>;
|
||||
async fn update(
|
||||
&mut self,
|
||||
context: &RepositoryContext,
|
||||
id: LookupKey,
|
||||
data: Value,
|
||||
) -> Option<RepositoryItem>;
|
||||
fn delete(&mut self, model: &AdminModel, id: LookupKey) -> Option<Value>;
|
||||
async fn delete(&mut self, context: &RepositoryContext, id: LookupKey) -> Option<Value>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ impl Default for AdminContext {
|
||||
pub fn base_template(headers: &HeaderMap) -> Option<String> {
|
||||
let hx_request = headers.get("HX-Request").is_some();
|
||||
if hx_request {
|
||||
println!("HX.");
|
||||
Some("admin/base_hx.jinja".to_string())
|
||||
} else {
|
||||
None
|
||||
@@ -123,8 +122,8 @@ pub async fn list_item_collection(
|
||||
AdminContext {
|
||||
base: base_template(&headers),
|
||||
available_apps: registry.get_apps(),
|
||||
item_info: Some(repo.info(&admin_model)),
|
||||
item_list: repo.list(&admin_model),
|
||||
item_info: Some(repo.info(&admin_model).await),
|
||||
item_list: repo.list(&admin_model).await,
|
||||
item_model: Some(admin_model),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -163,9 +162,9 @@ pub async fn item_details(
|
||||
AdminContext {
|
||||
base: base_template(&headers),
|
||||
available_apps: registry.get_apps(),
|
||||
item_info: Some(repo.info(&admin_model)),
|
||||
item_list: repo.list(&admin_model),
|
||||
item: repo.get(&admin_model, key),
|
||||
item_info: Some(repo.info(&admin_model).await),
|
||||
item_list: repo.list(&admin_model).await,
|
||||
item: repo.get(&admin_model, key).await,
|
||||
item_model: Some(admin_model),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -193,8 +192,8 @@ pub async fn new_item(
|
||||
AdminContext {
|
||||
base: base_template(&headers),
|
||||
available_apps: registry.get_apps(),
|
||||
item_info: Some(repo.info(&admin_model)),
|
||||
item_list: repo.list(&admin_model),
|
||||
item_info: Some(repo.info(&admin_model).await),
|
||||
item_list: repo.list(&admin_model).await,
|
||||
item_model: Some(admin_model),
|
||||
..Default::default()
|
||||
}
|
||||
@@ -222,14 +221,14 @@ pub async fn create_item(
|
||||
.expect("Admin Model not found?");
|
||||
|
||||
// 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.
|
||||
AdminContext {
|
||||
base: base_template(&headers),
|
||||
available_apps: registry.get_apps(),
|
||||
item_info: Some(repo.info(&admin_model)),
|
||||
item_list: repo.list(&admin_model),
|
||||
item_info: Some(repo.info(&admin_model).await),
|
||||
item_list: repo.list(&admin_model).await,
|
||||
item_model: Some(admin_model),
|
||||
item: result,
|
||||
..Default::default()
|
||||
|
||||
51
src/admin_examples/empty_repository.rs
Normal file
51
src/admin_examples/empty_repository.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
pub mod empty_repository;
|
||||
pub mod static_repository;
|
||||
pub mod user_repository;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// implementation of static repository
|
||||
|
||||
use crate::admin::domain::*;
|
||||
use crate::admin::state::AdminRegistry;
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, warn};
|
||||
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 {
|
||||
/// In Memory Storage for Example.
|
||||
content: Vec<Value>,
|
||||
@@ -33,53 +34,47 @@ impl MyStaticRepository {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AdminRepository for MyStaticRepository {
|
||||
fn get(&self, model: &AdminModel, id: LookupKey) -> Option<RepositoryItem> {
|
||||
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 {
|
||||
async fn info(&self, _: &RepositoryContext) -> RepositoryInfo {
|
||||
RepoInfo {
|
||||
name: "My Static Repository",
|
||||
lookup_key: "id",
|
||||
display_list: &["id", "name", "level", "age"],
|
||||
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);
|
||||
|
||||
let new_id = self.next_id;
|
||||
@@ -93,13 +88,15 @@ impl AdminRepository for MyStaticRepository {
|
||||
self.content.push(data.clone());
|
||||
|
||||
// Return the newly created item
|
||||
Some(RepositoryItem {
|
||||
detail_url: Some(format!("{}/detail/{}", model.admin_url, new_id)),
|
||||
fields: data,
|
||||
})
|
||||
Some(model.build_item(&*new_id.to_string(), 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);
|
||||
|
||||
// First, find the index of the item to update
|
||||
@@ -130,7 +127,7 @@ impl AdminRepository for MyStaticRepository {
|
||||
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);
|
||||
|
||||
let item_index = self.content.iter().position(|item| {
|
||||
|
||||
52
src/admin_examples/user_repository.rs
Normal file
52
src/admin_examples/user_repository.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user