code: widget implementation, almost there

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

View File

@@ -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>;
}
}

View File

@@ -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()

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 user_repository;

View File

@@ -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| {

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
}
}