derive html input type based on field type

This commit is contained in:
Manuel Gugger 2022-08-04 17:44:40 +02:00
parent 80df5a2719
commit a0fe692046
12 changed files with 165 additions and 63 deletions

View File

@ -2,29 +2,20 @@ use proc_macro;
use quote::quote;
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,
use struct_fields::{
get_actix_admin_fields, get_actix_admin_fields_html_input,
get_actix_admin_fields_is_option_list, get_actix_admin_fields_searchable,
get_actix_admin_fields_select_list, get_actix_admin_fields_type_path_string,
get_field_for_primary_key, get_fields_for_create_model, get_fields_for_edit_model,
get_fields_for_from_model, get_fields_for_tokenstream, get_fields_for_validate_model,
get_primary_key_field_name,
get_actix_admin_fields_select_list,
get_actix_admin_fields_is_option_list,
get_fields_for_validate_model,
get_actix_admin_fields_searchable
};
mod selectlist_fields;
use selectlist_fields::{
get_select_list,
get_select_lists
};
use selectlist_fields::{get_select_list, get_select_lists};
mod model_fields;
mod attributes;
mod model_fields;
#[proc_macro_derive(DeriveActixAdminSelectList, attributes(actix_admin))]
pub fn derive_actix_admin_select_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@ -46,6 +37,7 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
let field_for_primary_key = get_field_for_primary_key(&fields);
let fields_for_validate_model = get_fields_for_validate_model(&fields);
let fields_searchable = get_actix_admin_fields_searchable(&fields);
let fields_type_path = get_actix_admin_fields_type_path_string(&fields);
let select_lists = get_select_lists(&fields);
@ -120,7 +112,6 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
// TODO: separate primary key from other keys
let entity = Entity::find_by_id(id).one(db).await.unwrap().unwrap();
let model = ActixAdminModel::from(entity);
model
}
@ -134,10 +125,8 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
let mut entity: ActiveModel = entity.unwrap().into();
#(#fields_for_edit_model);*;
let entity: Model = entity.update(db).await.unwrap();
}
model
}
@ -187,12 +176,9 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
(num_pages, model_entities)
}
fn validate_model(model: &ActixAdminModel) -> HashMap<String, String> {
let mut errors = HashMap::<String, String>::new();
#(#fields_for_validate_model);*;
//let mut custom_errors = Entity.validate();
//errors.append(&mut custom_errors);
errors
@ -205,7 +191,7 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
#(#field_names),*
).split(",")
.collect::<Vec<_>>();
let html_input_types = stringify!(
#(#field_html_input_type),*
).split(",")
@ -220,14 +206,19 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
#(#is_option_list),*
];
for (field_name, html_input_type, select_list, is_option_list) in izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists) {
vec.push(ActixAdminViewModelField {
field_name: field_name.replace('"', "").replace(' ', "").to_string(),
html_input_type: html_input_type.replace('"', "").replace(' ', "").to_string(),
select_list: select_list.replace('"', "").replace(' ', "").to_string(),
is_option: is_option_list
});
}
let fields_type_paths = [
#(#fields_type_path),*
];
for (field_name, html_input_type, select_list, is_option_list, fields_type_path) in izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths) {
vec.push(ActixAdminViewModelField {
field_name: field_name.replace('"', "").replace(' ', "").to_string(),
html_input_type: html_input_type.replace('"', "").replace(' ', "").to_string(),
select_list: select_list.replace('"', "").replace(' ', "").to_string(),
is_option: is_option_list,
field_type: ActixAdminViewModelFieldType::from(fields_type_path)
});
}
vec
}
}

View File

@ -28,4 +28,21 @@ impl ModelField {
_ => false
}
}
pub fn get_type_path_string(&self) -> String {
let type_path_string: String;
if self.is_option() {
match &self.inner_type.clone().unwrap() {
Type::Path(type_path) => type_path_string = type_path.clone().into_token_stream().to_string(),
_ => panic!("not a type path")
}
} else {
match &self.ty {
Type::Path(type_path) => type_path_string = type_path.clone().into_token_stream().to_string(),
_ => panic!("not a type path")
}
}
type_path_string
}
}

View File

@ -43,9 +43,9 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
(LitStr::from(attr_field)).value()
})
});
let html_input_type = actix_admin_attr.map_or("text".to_string(), |attr| {
let html_input_type = actix_admin_attr.map_or("".to_string(), |attr| {
attr.html_input_type
.map_or("text".to_string(), |attr_field| {
.map_or("".to_string(), |attr_field| {
(LitStr::from(attr_field)).value()
})
});
@ -140,6 +140,20 @@ pub fn get_actix_admin_fields_is_option_list(fields: &Vec<ModelField>) -> Vec<To
.collect::<Vec<_>>()
}
pub fn get_actix_admin_fields_type_path_string(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()
.filter(|model_field| !model_field.primary_key)
.map(|model_field| {
let type_path_string = model_field.get_type_path_string();
quote! {
#type_path_string
}
})
.collect::<Vec<_>>()
}
pub fn get_actix_admin_fields_html_input(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()

View File

@ -1,7 +1,8 @@
use lazy_static::lazy_static;
use sea_orm::DatabaseConnection;
use std::collections::HashMap;
use tera::{Tera};
use tera::{Tera, Result, Value, to_value, try_get_value };
use std::{ hash::BuildHasher};
pub mod view_model;
pub mod model;
@ -11,7 +12,7 @@ pub mod builder;
pub mod prelude {
pub use crate::builder::{ ActixAdminBuilder, ActixAdminBuilderTrait};
pub use crate::model::{ ActixAdminModel, ActixAdminModelTrait};
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField};
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField, ActixAdminViewModelFieldType };
pub use actix_admin_macros::{ DeriveActixAdminModel, DeriveActixAdminSelectList };
pub use crate::{ ActixAdminAppDataTrait, ActixAdmin };
pub use crate::{ hashmap, ActixAdminSelectListTrait };
@ -30,10 +31,43 @@ macro_rules! hashmap {
// globals
lazy_static! {
static ref TERA: Tera =
Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
static ref TERA: Tera = {
let mut tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
tera.register_filter("get_html_input_type", get_html_input_type);
tera.register_filter("get_html_input_class", get_html_input_class);
tera
};
}
pub fn get_html_input_class<S: BuildHasher>(value: &tera::Value, _: &HashMap<String, tera::Value, S>) -> Result<tera::Value> {
let field = try_get_value!("get_html_input_class", "value", ActixAdminViewModelField, value);
let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::Checkbox => "checkbox",
_ => "input"
};
Ok(to_value(html_input_type).unwrap())
}
pub fn get_html_input_type<S: BuildHasher>(value: &tera::Value, _: &HashMap<String, tera::Value, S>) -> Result<tera::Value> {
let field = try_get_value!("get_html_input_type", "value", ActixAdminViewModelField, value);
// TODO: convert to option
if field.html_input_type != "" {
return Ok(to_value(field.html_input_type).unwrap())
}
let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::Text => "text",
ActixAdminViewModelFieldType::DateTime => "datetime-local",
ActixAdminViewModelFieldType::Checkbox => "checkbox",
_ => "text"
};
Ok(to_value(html_input_type).unwrap())
}
// AppDataTrait
pub trait ActixAdminAppDataTrait {
fn get_db(&self) -> &DatabaseConnection;

View File

@ -1,8 +1,9 @@
use async_trait::async_trait;
use sea_orm::DatabaseConnection;
use serde::{Serialize};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use crate::ActixAdminModel;
use std::convert::From;
#[async_trait(?Send)]
pub trait ActixAdminViewModelTrait {
@ -34,10 +35,37 @@ pub struct ActixAdminViewModel {
pub fields: Vec<ActixAdminViewModelField>,
}
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ActixAdminViewModelFieldType {
Number,
Text,
TextArea,
Checkbox,
Date,
Time,
DateTime,
SelectList
}
impl From<&str> for ActixAdminViewModelFieldType {
fn from(input: &str) -> ActixAdminViewModelFieldType {
match input {
"i32" => ActixAdminViewModelFieldType::Number,
"i64" => ActixAdminViewModelFieldType::Number,
"usize" => ActixAdminViewModelFieldType::Number,
"String" => ActixAdminViewModelFieldType::Text,
"bool" => ActixAdminViewModelFieldType::Checkbox,
"DateTimeWithTimeZone" => ActixAdminViewModelFieldType::DateTime,
_ => ActixAdminViewModelFieldType::Text
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ActixAdminViewModelField {
pub field_name: String,
pub html_input_type: String,
pub select_list: String,
pub is_option: bool
pub is_option: bool,
pub field_type: ActixAdminViewModelFieldType
}

View File

@ -13,27 +13,50 @@
<title>Actix Admin</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css" integrity="sha512-1sCRPdkRXhBV2PBLUdRb4tMg1w2YPf37qatUFeS7zlBy7jJI8Lf4VHwWfZZfpXtYSLy85pkm9GaYVYMfw5BC1A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css"
integrity="sha512-1sCRPdkRXhBV2PBLUdRb4tMg1w2YPf37qatUFeS7zlBy7jJI8Lf4VHwWfZZfpXtYSLy85pkm9GaYVYMfw5BC1A=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://unpkg.com/htmx.org@1.7.0"></script>
<script>
function checkAll(bx) {
var cbs = document.getElementsByTagName('input');
for(var i=0; i < cbs.length; i++) {
if(cbs[i].type == 'checkbox') {
cbs[i].checked = bx.checked;
for (var i = 0; i < cbs.length; i++) {
if (cbs[i].type == 'checkbox') {
cbs[i].checked = bx.checked;
}
}
}
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
</head>
<body>
{% include "header.html" %}
<div class="container is-fluid">
{% block content %}
{% endblock content %}
</div>
{% include "header.html" %}
<div class="container is-fluid">
{% block content %}
{% endblock content %}
</div>
</body>
</html>

View File

@ -4,7 +4,7 @@
<form method="post">
{% for model_field in view_model.fields -%}
<div class="field">
<label for="{{ model_field.field_name }}">
<label class="{{ model_field | get_html_input_type }}" for="{{ model_field.field_name }}">
{{ model_field.field_name | split(pat="_") | join(sep=" ") | title }}
</label>
{% if model_field.select_list != "" %}
@ -26,8 +26,8 @@
</div>
{% else %}
<div class="control">
<input class="input {% if model.errors | get(key=model_field.field_name,default="" ) !="" %}is-danger{% endif %}"
type="{{ model_field.html_input_type }}"
<input class="{{ model_field | get_html_input_class }} {% if model.errors | get(key=model_field.field_name,default="" ) !="" %}is-danger{% endif %}"
type="{{ model_field | get_html_input_type }}"
value="{{ model.values | get(key=model_field.field_name, default="") | split(pat=" _") | join(sep=" " ) | title }}"
name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}"

View File

@ -1,7 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.72,19.9a8,8,0,0,1-6.5-9.79A7.77,7.77,0,0,1,10.4,4.16a8,8,0,0,1,9.49,6.52A1.54,1.54,0,0,0,21.38,12h.13a1.37,1.37,0,0,0,1.38-1.54,11,11,0,1,0-12.7,12.39A1.54,1.54,0,0,0,12,21.34h0A1.47,1.47,0,0,0,10.72,19.9Z">
<animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12"
repeatCount="indefinite" />
</path>
</svg>

Before

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

View File

@ -11,10 +11,11 @@ pub struct Model {
pub id: i32,
pub comment: String,
#[sea_orm(column_type = "Text")]
#[actix_admin(html_input_type = "email")]
pub user: String,
#[sea_orm(column_type = "DateTime")]
#[actix_admin(html_input_type = "datetime-local")]
pub insert_date: DateTimeWithTimeZone
pub insert_date: DateTimeWithTimeZone,
pub is_visible: bool
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -44,6 +44,7 @@ pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
.col(ColumnDef::new(comment::Column::Comment).string().not_null())
.col(ColumnDef::new(comment::Column::User).string().not_null())
.col(ColumnDef::new(comment::Column::InsertDate).date_time().not_null())
.col(ColumnDef::new(comment::Column::IsVisible).boolean().not_null())
.to_owned();
create_table(db, &stmt).await

View File

@ -1,9 +1,9 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
use std::str::FromStr;
use std::fmt;
use std::fmt::Display;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdminModel)]
#[sea_orm(table_name = "post")]