add dropdown for enums

This commit is contained in:
Manuel Gugger 2022-07-21 19:01:05 +02:00
parent 2b653510b2
commit e0da08ef73
18 changed files with 161 additions and 43 deletions

View File

@ -10,7 +10,7 @@ actix-web = "4.0.1"
actix-rt = "2.7.0" actix-rt = "2.7.0"
actix-session = "0.5.0" actix-session = "0.5.0"
tera = "1.15.0" tera = "1.15.0"
itertools = "0.10.3"
oauth2 = "4.1" oauth2 = "4.1"
base64 = "0.13.0" base64 = "0.13.0"
async-trait = "0.1.53" async-trait = "0.1.53"

View File

@ -19,7 +19,6 @@ bae = "0.1.7"
quote = "1.0" quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] } syn = { version = "1.0", features = ["full", "extra-traits"] }
proc-macro2 = { version = "1.0.36", default-features = false } proc-macro2 = { version = "1.0.36", default-features = false }
rand = "0.8.5" rand = "0.8.5"
url = "2.2.2" url = "2.2.2"
http = "0.2.6" http = "0.2.6"

View File

@ -11,7 +11,8 @@ pub mod derive_attr {
)] )]
pub struct ActixAdmin { pub struct ActixAdmin {
pub primary_key: Option<()>, pub primary_key: Option<()>,
pub html_input_type: Option<syn::LitStr> pub html_input_type: Option<syn::LitStr>,
pub select_list: Option<syn::LitStr>
//pub inner_type: Option<syn::Type>, //pub inner_type: Option<syn::Type>,
// Anything that implements `syn::parse::Parse` is supported. // Anything that implements `syn::parse::Parse` is supported.

View File

@ -2,23 +2,47 @@ use proc_macro;
use quote::quote; use quote::quote;
mod struct_fields; mod struct_fields;
use struct_fields::{ get_fields_for_tokenstream, get_fields_for_edit_model, get_fields_for_from_model, get_actix_admin_fields_html_input, get_fields_for_create_model, get_actix_admin_fields, get_field_for_primary_key, get_primary_key_field_name}; use struct_fields::{
get_fields_for_tokenstream,
get_fields_for_edit_model,
get_fields_for_from_model,
get_actix_admin_fields_html_input,
get_fields_for_create_model,
get_actix_admin_fields,
get_field_for_primary_key,
get_primary_key_field_name,
get_actix_admin_fields_select_list
};
mod selectlist_fields;
use selectlist_fields::{
get_select_list,
get_select_lists
};
mod model_fields; mod model_fields;
mod attributes; mod attributes;
#[proc_macro_derive(DeriveActixAdminSelectList, attributes(actix_admin))]
pub fn derive_actix_admin_select_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
get_select_list(input)
}
#[proc_macro_derive(DeriveActixAdminModel, attributes(actix_admin))] #[proc_macro_derive(DeriveActixAdminModel, attributes(actix_admin))]
pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let fields = get_fields_for_tokenstream(input); let fields = get_fields_for_tokenstream(input);
let field_names = get_actix_admin_fields(&fields); let field_names = get_actix_admin_fields(&fields);
let field_html_input_type = get_actix_admin_fields_html_input(&fields); let field_html_input_type = get_actix_admin_fields_html_input(&fields);
let field_select_list = get_actix_admin_fields_select_list(&fields);
let name_primary_field_str = get_primary_key_field_name(&fields); let name_primary_field_str = get_primary_key_field_name(&fields);
let fields_for_create_model = get_fields_for_create_model(&fields); let fields_for_create_model = get_fields_for_create_model(&fields);
let fields_for_edit_model = get_fields_for_edit_model(&fields); let fields_for_edit_model = get_fields_for_edit_model(&fields);
let fields_for_from_model = get_fields_for_from_model(&fields); let fields_for_from_model = get_fields_for_from_model(&fields);
let field_for_primary_key = get_field_for_primary_key(&fields); let field_for_primary_key = get_field_for_primary_key(&fields);
let select_lists = get_select_lists(&fields);
let expanded = quote! { let expanded = quote! {
use std::convert::From; use std::convert::From;
use std::iter::zip; use std::iter::zip;
@ -30,6 +54,7 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
use sea_orm::{entity::*, query::*}; use sea_orm::{entity::*, query::*};
use std::collections::HashMap; use std::collections::HashMap;
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
use itertools::izip;
use quote::quote; use quote::quote;
impl From<Entity> for ActixAdminViewModel { impl From<Entity> for ActixAdminViewModel {
@ -109,6 +134,12 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
} }
} }
async fn get_select_lists(db: &DatabaseConnection) -> HashMap<String, Vec<(String, String)>> {
hashmap![
#(#select_lists),*
]
}
fn get_entity_name() -> String { fn get_entity_name() -> String {
Entity.table_name().to_string() Entity.table_name().to_string()
} }
@ -136,8 +167,9 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
(num_pages, model_entities) (num_pages, model_entities)
} }
fn get_fields() -> Vec<(String, String)> { fn get_fields() -> Vec<ActixAdminViewModelField> {
let mut vec = Vec::new(); let mut vec = Vec::new();
let field_names = stringify!( let field_names = stringify!(
#(#field_names),* #(#field_names),*
).split(",") ).split(",")
@ -148,23 +180,18 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
).split(",") ).split(",")
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut names_and_input_type = zip(field_names, html_input_types); let field_select_lists = stringify!(
#(#field_select_list),*
names_and_input_type ).split(",")
.for_each( |field_name_and_type_tuple| .collect::<Vec<_>>();
vec.push((
field_name_and_type_tuple.0 for (field_name, html_input_type, select_list) in izip!(&field_names, &html_input_types, &field_select_lists) {
.replace('"', "") vec.push(ActixAdminViewModelField {
.replace(' ', "") field_name: field_name.replace('"', "").replace(' ', "").to_string(),
.to_string(), html_input_type: html_input_type.replace('"', "").replace(' ', "").to_string(),
// TODO: match correct ActixAdminField Value select_list: select_list.replace('"', "").replace(' ', "").to_string()
field_name_and_type_tuple.1 });
.replace('"', "") }
.replace(' ', "")
.to_string()
)
)
);
vec vec
} }
} }

View File

@ -9,7 +9,8 @@ pub struct ModelField {
// struct field is option<> // struct field is option<>
pub inner_type: Option<Type>, pub inner_type: Option<Type>,
pub primary_key: bool, pub primary_key: bool,
pub html_input_type: String pub html_input_type: String,
pub select_list: String
} }
impl ModelField { impl ModelField {

View File

@ -0,0 +1,38 @@
use syn::{
DeriveInput, Ident
};
use quote::quote;
use crate::model_fields::{ ModelField };
use proc_macro2::{Span};
pub fn get_select_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let (_vis, ty, _generics) = (&ast.vis, &ast.ident, &ast.generics);
let expanded = quote! {
impl ActixAdminSelectListTrait for #ty {
fn get_key_value() -> Vec<(String, String)> {
let mut fields = Vec::new();
for field in #ty::iter() {
fields.push((field.to_string(), field.to_string()));
}
fields
}
}
};
proc_macro::TokenStream::from(expanded)
}
pub fn get_select_lists(fields: &Vec<ModelField>) -> Vec<proc_macro2::TokenStream> {
fields
.iter()
.filter(|model_field| model_field.select_list != "")
.map(|model_field| {
let ident_name = model_field.ident.to_string();
let select_list_ident = Ident::new(&(model_field.select_list), Span::call_site());
quote! {
#ident_name => #select_list_ident::get_key_value()
}
})
.collect::<Vec<_>>()
}

View File

@ -9,7 +9,7 @@ use crate::model_fields::{ ModelField };
pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<ModelField> { pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<ModelField> {
let ast: DeriveInput = syn::parse(input).unwrap(); let ast: DeriveInput = syn::parse(input).unwrap();
let (_vis, ty, _generics) = (&ast.vis, &ast.ident, &ast.generics); let (_vis, ty, _generics) = (&ast.vis, &ast.ident, &ast.generics);
let _names_struct_ident = Ident::new(&(ty.to_string() + "FieldStaticStr"), Span::call_site()); //let _names_struct_ident = Ident::new(&(ty.to_string() + "FieldStaticStr"), Span::call_site());
let fields = filter_fields(match ast.data { let fields = filter_fields(match ast.data {
syn::Data::Struct(ref s) => &s.fields, syn::Data::Struct(ref s) => &s.fields,
@ -31,6 +31,9 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
let inner_type = extract_type_from_option(&field.ty); let inner_type = extract_type_from_option(&field.ty);
let field_ty = field.ty.to_owned(); let field_ty = field.ty.to_owned();
let is_primary_key = actix_admin_attr.clone().map_or(false, |attr| attr.primary_key.is_some()); let is_primary_key = actix_admin_attr.clone().map_or(false, |attr| attr.primary_key.is_some());
let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| attr.select_list.map_or("".to_string(),
|attr_field| (LitStr::from(attr_field)).value()
));
let html_input_type = actix_admin_attr.map_or("text".to_string(), |attr| attr.html_input_type.map_or("text".to_string(), let html_input_type = actix_admin_attr.map_or("text".to_string(), |attr| attr.html_input_type.map_or("text".to_string(),
|attr_field| (LitStr::from(attr_field)).value() |attr_field| (LitStr::from(attr_field)).value()
)); ));
@ -41,7 +44,8 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
ty: field_ty, ty: field_ty,
inner_type: inner_type, inner_type: inner_type,
primary_key: is_primary_key, primary_key: is_primary_key,
html_input_type: html_input_type html_input_type: html_input_type,
select_list: select_list
}; };
Some(model_field) Some(model_field)
@ -124,6 +128,21 @@ pub fn get_actix_admin_fields_html_input(fields: &Vec<ModelField>) -> Vec<TokenS
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub fn get_actix_admin_fields_select_list(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()
.filter(|model_field| !model_field.primary_key)
.map(|model_field| {
let select_list = model_field.select_list.to_string();
quote! {
#select_list
}
})
.collect::<Vec<_>>()
}
pub fn get_field_for_primary_key(fields: &Vec<ModelField>) -> TokenStream { pub fn get_field_for_primary_key(fields: &Vec<ModelField>) -> TokenStream {
let primary_key_model_field = fields let primary_key_model_field = fields
.iter() .iter()

View File

@ -2,7 +2,6 @@ use lazy_static::lazy_static;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera}; use tera::{Tera};
use serde::{Serialize};
pub mod view_model; pub mod view_model;
pub mod model; pub mod model;
@ -12,10 +11,10 @@ pub mod builder;
pub mod prelude { pub mod prelude {
pub use crate::builder::{ ActixAdminBuilder, ActixAdminBuilderTrait}; pub use crate::builder::{ ActixAdminBuilder, ActixAdminBuilderTrait};
pub use crate::model::{ ActixAdminModel, ActixAdminModelTrait}; pub use crate::model::{ ActixAdminModel, ActixAdminModelTrait};
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait}; pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField};
pub use actix_admin_macros::{ DeriveActixAdminModel }; pub use actix_admin_macros::{ DeriveActixAdminModel, DeriveActixAdminSelectList };
pub use crate::{ ActixAdminAppDataTrait, ActixAdmin}; pub use crate::{ ActixAdminAppDataTrait, ActixAdmin};
pub use crate::{ hashmap }; pub use crate::{ hashmap, ActixAdminSelectListTrait };
} }
use crate::prelude::*; use crate::prelude::*;
@ -24,7 +23,7 @@ use crate::prelude::*;
macro_rules! hashmap { macro_rules! hashmap {
($( $key: expr => $val: expr ),*) => {{ ($( $key: expr => $val: expr ),*) => {{
let mut map = ::std::collections::HashMap::new(); let mut map = ::std::collections::HashMap::new();
$( map.insert($key.to_string(), $val.to_string()); )* $( map.insert($key.to_string(), $val); )*
map map
}} }}
} }
@ -41,8 +40,12 @@ pub trait ActixAdminAppDataTrait {
fn get_actix_admin(&self) -> &ActixAdmin; fn get_actix_admin(&self) -> &ActixAdmin;
} }
// ActixAdminModel // SelectListTrait
pub trait ActixAdminSelectListTrait {
fn get_key_value() -> Vec<(String, String)>;
}
// ActixAdminModel
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ActixAdmin { pub struct ActixAdmin {
pub entity_names: Vec<String>, pub entity_names: Vec<String>,

View File

@ -2,6 +2,7 @@ use async_trait::async_trait;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use serde::{Serialize}; use serde::{Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use crate::ActixAdminViewModelField;
#[async_trait] #[async_trait]
pub trait ActixAdminModelTrait { pub trait ActixAdminModelTrait {
@ -10,7 +11,7 @@ pub trait ActixAdminModelTrait {
page: usize, page: usize,
posts_per_page: usize, posts_per_page: usize,
) -> (usize, Vec<ActixAdminModel>); ) -> (usize, Vec<ActixAdminModel>);
fn get_fields() -> Vec<(String, String)>; fn get_fields() -> Vec<ActixAdminViewModelField>;
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -37,6 +38,7 @@ impl From<String> for ActixAdminModel {
impl ActixAdminModel { impl ActixAdminModel {
pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Option<T> { pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Option<T> {
println!("{:?}", self.values);
let value = self.values.get(key).unwrap().to_string().parse::<T>(); let value = self.values.get(key).unwrap().to_string().parse::<T>();
match value { match value {
Ok(val) => Some(val), Ok(val) => Some(val),

View File

@ -22,7 +22,7 @@ pub async fn create_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("entity_names", &entity_names); ctx.insert("entity_names", &entity_names);
ctx.insert("view_model", &view_model); ctx.insert("view_model", &view_model);
ctx.insert("model_fields", &view_model.fields); ctx.insert("select_lists", &E::get_select_lists(_db).await);
let body = TERA let body = TERA
.render("create.html", &ctx) .render("create.html", &ctx)

View File

@ -26,6 +26,7 @@ pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
ctx.insert("entity_names", &entity_names); ctx.insert("entity_names", &entity_names);
ctx.insert("view_model", &view_model); ctx.insert("view_model", &view_model);
ctx.insert("model", &model); ctx.insert("model", &model);
ctx.insert("select_lists", &E::get_select_lists(db).await);
let body = TERA let body = TERA
.render("edit.html", &ctx) .render("edit.html", &ctx)

View File

@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use serde::{Serialize}; use serde::{Serialize};
use std::collections::HashMap;
use crate::ActixAdminModel; use crate::ActixAdminModel;
#[async_trait(?Send)] #[async_trait(?Send)]
@ -17,7 +17,8 @@ pub trait ActixAdminViewModelTrait {
async fn delete_entity(db: &DatabaseConnection, id: i32) -> bool; async fn delete_entity(db: &DatabaseConnection, id: i32) -> bool;
async fn get_entity(db: &DatabaseConnection, id: i32) -> ActixAdminModel; async fn get_entity(db: &DatabaseConnection, id: i32) -> ActixAdminModel;
async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> ActixAdminModel; async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> ActixAdminModel;
async fn get_select_lists(db: &DatabaseConnection) -> HashMap<String, Vec<(String, String)>>;
fn get_entity_name() -> String; fn get_entity_name() -> String;
} }
@ -25,5 +26,12 @@ pub trait ActixAdminViewModelTrait {
pub struct ActixAdminViewModel { pub struct ActixAdminViewModel {
pub entity_name: String, pub entity_name: String,
pub primary_key: String, pub primary_key: String,
pub fields: Vec<(String, String)>, pub fields: Vec<ActixAdminViewModelField>,
}
#[derive(Clone, Debug, Serialize)]
pub struct ActixAdminViewModelField {
pub field_name: String,
pub html_input_type: String,
pub select_list: String
} }

View File

@ -4,7 +4,16 @@
<form method="post"> <form method="post">
<div class="grid"> <div class="grid">
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
<input type="{{ model_field[1] }}" name="{{ model_field[0] }}" placeholder="{{ model_field[0] }}" aria-label="{{ model_field[0] }}"><!-- required="" --> {% if model_field.select_list != "" %}
<select name="{{ model_field.field_name }}">
<option value="" selected>Select</option>
{% for select_list_item in select_lists[model_field.field_name] -%}
<option value="{{ select_list_item[0] }}">{{ select_list_item[1] }}</option>
{%- endfor %}
</select>
{% else %}
<input type="{{ model_field.html_input_type }}" name="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}"><!-- required="" -->
{% endif %}
{%- endfor %} {%- endfor %}
<button type="submit">Save</button> <button type="submit">Save</button>
<button onclick="history.back()">Cancel</button> <button onclick="history.back()">Cancel</button>

View File

@ -3,8 +3,17 @@
{% block content %} {% block content %}
<form method="post"> <form method="post">
<div class="grid"> <div class="grid">
{% for key, value in model.values -%} {% for model_field in view_model.fields -%}
<input type="text" value="{{ value }}" name="{{ key }}" placeholder="{{ key }}" aria-label="{{ key }}"><!-- required="" --> {% if model_field.select_list != "" %}
<select name="{{ model_field.field_name }}">
<option value="" selected>Select</option>
{% for select_list_item in select_lists[model_field.field_name] -%}
<option {% if select_list_item[0] == model.values | get(key=model_field.field_name) %} selected {% endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] }}</option>
{%- endfor %}
</select>
{% else %}
<input type="{{ model_field.html_input_type }}" value="{{ model.values | get(key=model_field.field_name) }}" name="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}"><!-- required="" -->
{% endif %}
{%- endfor %} {%- endfor %}
<button type="submit">Save</button> <button type="submit">Save</button>
<button onclick="history.back()">Cancel</button> <button onclick="history.back()">Cancel</button>

View File

@ -7,7 +7,7 @@
<tr> <tr>
<th>{{ view_model.primary_key }}</th> <th>{{ view_model.primary_key }}</th>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
<th>{{ model_field[0] }}</th> <th>{{ model_field.field_name }}</th>
{%- endfor %} {%- endfor %}
<th> <th>
<!-- Edit Action --> <!-- Edit Action -->
@ -20,7 +20,7 @@
<tr> <tr>
<td>{{ entity.primary_key }}</td> <td>{{ entity.primary_key }}</td>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
<td>{{ entity.values | get(key=model_field[0]) }}</td> <td>{{ entity.values | get(key=model_field.field_name) }}</td>
{%- endfor %} {%- endfor %}
<td> <td>
<a href="edit/{{ entity.primary_key }}">&#9998;</a> <a href="edit/{{ entity.primary_key }}">&#9998;</a>

Binary file not shown.

Binary file not shown.

View File

@ -15,8 +15,9 @@ pub struct Model {
pub title: String, pub title: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub text: String, pub text: String,
#[actix_admin(select_list="Tea")]
pub tea_mandatory: Tea, pub tea_mandatory: Tea,
#[actix_admin()] #[actix_admin(select_list="Tea")]
pub tea_optional: Option<Tea>, pub tea_optional: Option<Tea>,
} }
@ -25,7 +26,7 @@ pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminSelectList)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea { pub enum Tea {
#[sea_orm(string_value = "EverydayTea")] #[sea_orm(string_value = "EverydayTea")]