add dropdown for custom filters

This commit is contained in:
Manuel Gugger 2023-07-04 19:49:26 +02:00
parent cf9b8f0c26
commit d95c84c2c6
12 changed files with 141 additions and 32 deletions

View File

@ -2,7 +2,7 @@
name = "actix-admin" name = "actix-admin"
description = "An admin interface for actix-web" description = "An admin interface for actix-web"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
version = "0.4.0" version = "0.5.0"
repository = "https://github.com/mgugger/actix-admin" repository = "https://github.com/mgugger/actix-admin"
edition = "2021" edition = "2021"
exclude = [ exclude = [
@ -32,7 +32,7 @@ itertools = "^0.10.5"
serde = "^1.0.164" serde = "^1.0.164"
serde_derive = "^1.0.164" serde_derive = "^1.0.164"
sea-orm = { version = "^0.11.3", features = [], default-features = false } sea-orm = { version = "^0.11.3", features = [], default-features = false }
actix-admin-macros = { version = "0.4.0", path = "actix_admin_macros" } actix-admin-macros = { version = "0.5.0", path = "actix_admin_macros" }
derive_more = "0.99.17" derive_more = "0.99.17"
regex = "1.8.4" regex = "1.8.4"
urlencoding = "2.1.2" urlencoding = "2.1.2"

View File

@ -3,7 +3,7 @@ name = "actix-admin-macros"
description = "macros to be used with actix-admin crate" description = "macros to be used with actix-admin crate"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/mgugger/actix-admin" repository = "https://github.com/mgugger/actix-admin"
version = "0.4.0" version = "0.5.0"
edition = "2021" edition = "2021"
exclude = [ exclude = [
"tests/*" "tests/*"

View File

@ -98,13 +98,22 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
Ok(model) Ok(model)
} }
async fn get_viewmodel_filter() -> HashMap<String, ActixAdminViewModelFilter> { async fn get_viewmodel_filter(db: &DatabaseConnection) -> HashMap<String, ActixAdminViewModelFilter> {
Entity::get_filter().iter().map(|f| let mut hashmap: HashMap<String, ActixAdminViewModelFilter> = HashMap::new();
(f.name.to_string(), ActixAdminViewModelFilter {
name: f.name.to_string(), for filter in Entity::get_filter() {
value: None hashmap.insert(
}) filter.name.to_string(),
).collect() ActixAdminViewModelFilter {
name: filter.name.to_string(),
value: None,
values: Entity::get_filter_values(&filter, db).await,
filter_type: Some(filter.filter_type)
}
);
};
hashmap
} }
async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError> { async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError> {

View File

@ -24,7 +24,7 @@ Check the [examples](https://github.com/mgugger/actix-admin/tree/main/examples)
[package] [package]
name = "actix-admin-example" name = "actix-admin-example"
description = "An admin interface for actix-web" description = "An admin interface for actix-web"
version = "0.4.0" version = "0.5.0"
edition = "2021" edition = "2021"
[[bin]] [[bin]]
@ -40,6 +40,6 @@ chrono = "0.4.23"
tera = "^1.17.1" tera = "^1.17.1"
serde = "^1.0.152" serde = "^1.0.152"
serde_derive = "^1.0.152" serde_derive = "^1.0.152"
actix-admin = { version = "0.4.0", path = "../../" } actix-admin = { version = "0.5.0", path = "../../" }
regex = "1.7.1" regex = "1.7.1"
``` ```

View File

@ -0,0 +1,8 @@
---
title: "Custom Filters"
date: 2023-01-17T11:44:56+01:00
draft: false
weight: 6
---
# Custom Filters

View File

@ -12,7 +12,7 @@ weight: 1
Cargo.toml: Cargo.toml:
```cargo ```cargo
[dependencies] [dependencies]
actix-admin = "0.4.0" actix-admin = "0.5.0"
``` ```
## Build the Actix-Admin Configuration ## Build the Actix-Admin Configuration
@ -50,7 +50,7 @@ let app = App::new()
.app_data(web::Data::new(conn.clone())) .app_data(web::Data::new(conn.clone()))
.app_data(web::Data::new(actix_admin_builder.get_actix_admin())) .app_data(web::Data::new(actix_admin_builder.get_actix_admin()))
.service( .service(
actix_admin_builder.get_scope::<AppState>() actix_admin_builder.get_scope()
) )
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
``` ```

View File

@ -1,7 +1,7 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use actix_admin::{prelude::*}; use actix_admin::{prelude::*};
use super::Post; use super::{Post, post};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel)]
#[sea_orm(table_name = "comment")] #[sea_orm(table_name = "comment")]
@ -51,22 +51,61 @@ impl ActixAdminModelValidationTrait<ActiveModel> for Entity {
} }
} }
#[async_trait]
impl ActixAdminModelFilterTrait<Entity> for Entity { impl ActixAdminModelFilterTrait<Entity> for Entity {
fn get_filter() -> Vec<ActixAdminModelFilter<Entity>> { fn get_filter() -> Vec<ActixAdminModelFilter<Entity>> {
vec![ vec![
ActixAdminModelFilter::<Entity> {
name: "Id".to_string(),
filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> {
q.apply_if(v, | query, val: String| query.filter(Column::Id.eq(val)))
}
},
ActixAdminModelFilter::<Entity> { ActixAdminModelFilter::<Entity> {
name: "User".to_string(), name: "User".to_string(),
filter_type: ActixAdminModelFilterType::Text,
filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> { filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> {
q.apply_if(v, | query, val: String| query.filter(Column::User.eq(val))) q.apply_if(v, | query, val: String| query.filter(Column::User.eq(val)))
} },
values: None
},
ActixAdminModelFilter::<Entity> {
name: "Insert Date After".to_string(),
filter_type: ActixAdminModelFilterType::DateTime,
filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> {
q.apply_if(v, | query, val: String| query.filter(Column::InsertDate.gte(val)))
},
values: None
},
ActixAdminModelFilter::<Entity> {
name: "Insert Date After".to_string(),
filter_type: ActixAdminModelFilterType::DateTime,
filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> {
q.apply_if(v, | query, val: String| query.filter(Column::InsertDate.gte(val)))
},
values: None
},
ActixAdminModelFilter::<Entity> {
name: "Is Visible".to_string(),
filter_type: ActixAdminModelFilterType::Checkbox,
filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> {
q.apply_if(v, | query, val: String| query.filter(Column::IsVisible.eq(val)))
},
values: None
},
ActixAdminModelFilter::<Entity> {
name: "Post".to_string(),
filter_type: ActixAdminModelFilterType::SelectList,
filter: |q: sea_orm::Select<Entity>, v| -> sea_orm::Select<Entity> {
q.apply_if(v, | query, val: String| query.filter(Column::PostId.eq(val)))
},
values: None
} }
] ]
} }
async fn get_filter_values(filter: &ActixAdminModelFilter<Entity>, db: &DatabaseConnection) -> Option<Vec<(String, String)>> {
match filter.name.as_str() {
"Post" => Some({
Post::find().order_by_asc(post::Column::Id).all(db).await.unwrap()
.iter().map(|p| (p.id.to_string(), p.title.to_string())).collect()
}),
_ => None
}
}
} }

View File

@ -24,7 +24,7 @@ pub mod view_model;
pub mod prelude { pub mod prelude {
pub use crate::builder::{ActixAdminBuilder, ActixAdminBuilderTrait}; pub use crate::builder::{ActixAdminBuilder, ActixAdminBuilderTrait};
pub use crate::model::{ActixAdminModel, ActixAdminModelTrait, ActixAdminModelValidationTrait, ActixAdminModelFilter, ActixAdminModelFilterTrait}; pub use crate::model::{ActixAdminModel, ActixAdminModelTrait, ActixAdminModelValidationTrait, ActixAdminModelFilter, ActixAdminModelFilterTrait, ActixAdminModelFilterType};
pub use crate::routes::{create_or_edit_post, get_admin_ctx, SortOrder}; pub use crate::routes::{create_or_edit_post, get_admin_ctx, SortOrder};
pub use crate::view_model::{ pub use crate::view_model::{
ActixAdminViewModel, ActixAdminViewModelField, ActixAdminViewModelFieldType, ActixAdminViewModel, ActixAdminViewModelField, ActixAdminViewModelFieldType,

View File

@ -36,20 +36,37 @@ pub trait ActixAdminModelValidationTrait<T> {
pub struct ActixAdminModelFilter<E: EntityTrait> { pub struct ActixAdminModelFilter<E: EntityTrait> {
pub name: String, pub name: String,
pub filter: fn(sea_orm::Select<E>, Option<String>) -> sea_orm::Select<E> pub filter_type: ActixAdminModelFilterType,
pub filter: fn(sea_orm::Select<E>, Option<String>) -> sea_orm::Select<E>,
pub values: Option<Vec<(String, String)>>
} }
#[derive(Clone, Debug, Serialize)]
pub enum ActixAdminModelFilterType {
Text,
SelectList,
Date,
DateTime,
Checkbox
}
#[async_trait]
pub trait ActixAdminModelFilterTrait<E: EntityTrait> { pub trait ActixAdminModelFilterTrait<E: EntityTrait> {
fn get_filter() -> Vec<ActixAdminModelFilter<E>> { fn get_filter() -> Vec<ActixAdminModelFilter<E>> {
Vec::new() Vec::new()
} }
async fn get_filter_values(_filter: &ActixAdminModelFilter<E>, _db: &DatabaseConnection)-> Option<Vec<(String, String)>> {
None
}
} }
impl<T: EntityTrait> From<ActixAdminModelFilter<T>> for ActixAdminViewModelFilter { impl<T: EntityTrait> From<ActixAdminModelFilter<T>> for ActixAdminViewModelFilter {
fn from(filter: ActixAdminModelFilter<T>) -> Self { fn from(filter: ActixAdminModelFilter<T>) -> Self {
ActixAdminViewModelFilter { ActixAdminViewModelFilter {
name: filter.name, name: filter.name,
value: None value: None,
values: None,
filter_type: Some(filter.filter_type)
} }
} }
} }

View File

@ -90,6 +90,8 @@ pub async fn list<E: ActixAdminViewModelTrait>(
let af = ActixAdminViewModelFilter { let af = ActixAdminViewModelFilter {
name: kv.next().unwrap().strip_prefix("filter_").unwrap_or_default().to_string(), name: kv.next().unwrap().strip_prefix("filter_").unwrap_or_default().to_string(),
value: kv.next().map(|s| s.to_string()).filter(|f| !f.is_empty()), value: kv.next().map(|s| s.to_string()).filter(|f| !f.is_empty()),
values: None,
filter_type: None
}; };
af af
}).collect(); }).collect();
@ -143,7 +145,7 @@ pub async fn list<E: ActixAdminViewModelTrait>(
ctx.insert("notifications", &notifications); ctx.insert("notifications", &notifications);
ctx.insert("entities_per_page", &entities_per_page); ctx.insert("entities_per_page", &entities_per_page);
ctx.insert("render_partial", &render_partial); ctx.insert("render_partial", &render_partial);
ctx.insert("viewmodel_filter", &E::get_viewmodel_filter().await); ctx.insert("viewmodel_filter", &E::get_viewmodel_filter(&db).await);
ctx.insert( ctx.insert(
"view_model", "view_model",
&ActixAdminViewModelSerializable::from(view_model.clone()), &ActixAdminViewModelSerializable::from(view_model.clone()),

View File

@ -7,11 +7,42 @@
<ul class="menu-list"> <ul class="menu-list">
{% for key, value in viewmodel_filter %} {% for key, value in viewmodel_filter %}
<li> <li>
<div class="field"> <div class="field mt-3">
<label class="label">{{key}}</label> <label class="label">{{key}}</label>
{% if value.filter_type == "Text" %}
<div class="control"> <div class="control">
<input class="input" type="text" placeholder="" name="filter_{{key}}"> <input class="input" value="{{ value.value }}" type="text" placeholder="" name="filter_{{key}}">
</div> </div>
{% elif value.filter_type == "DateTime" %}
<div class="control">
<input class="input" value="{{ value.value }}" type="datetime-local" placeholder="" name="filter_{{key}}">
</div>
{% elif value.filter_type == "Checkbox" %}
<div class="select is-fullwidth">
<select name="filter_{{key}}" id="filter_{{key}}">
<option value=""></option>
<option value="1">&#10003;</option>
<option value="0">&#10060;</option>
</select>
</div>
{% elif value.filter_type == "Date" %}
<div class="control">
<input class="input" type="date" placeholder="" name="filter_{{key}}">
</div>
{% elif value.filter_type == "SelectList" %}
<div class="select is-fullwidth">
<select name="filter_{{key}}" id="filter_{{key}}">
<option value=""></option>
{% for selectval in value.values %}
<option value="{{ selectval[0] }}">{{ selectval[1] }}</option>
{% endfor %}
</select>
</div>
{% else %}
<div class="control">
<input class="input" value="{{ value.value }}" type="text" placeholder="" name="filter_{{key}}">
</div>
{% endif %}
</div> </div>
</li> </li>
{% endfor %} {% endfor %}
@ -59,7 +90,8 @@
</div> </div>
</div> </div>
<form id="search_form" action="/admin/{{ entity_name }}/list" hx-boost="true" hx-indicator="#loading" <form id="search_form" action="/admin/{{ entity_name }}/list" hx-boost="true" hx-indicator="#loading"
hx-target="#{{ entity_name }}table" hx-trigger="reload_table from:#entities_per_page" hx-include="[id='filter_form']"> hx-target="#{{ entity_name }}table" hx-trigger="reload_table from:#entities_per_page"
hx-include="[id='filter_form']">
<input type="hidden" id="sort_by" name="sort_by" value="{{ sort_by }}"> <input type="hidden" id="sort_by" name="sort_by" value="{{ sort_by }}">
<input type="hidden" id="sort_order" name="sort_order" value="{{ sort_order }}"> <input type="hidden" id="sort_order" name="sort_order" value="{{ sort_order }}">
<input type="hidden" name="page" value="{{ page }}"> <input type="hidden" name="page" value="{{ page }}">

View File

@ -3,7 +3,7 @@ use regex::Regex;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ActixAdminModel, SortOrder}; use crate::{ActixAdminModel, SortOrder, model::ActixAdminModelFilterType};
use actix_session::{Session}; use actix_session::{Session};
use std::convert::From; use std::convert::From;
use crate::ActixAdminError; use crate::ActixAdminError;
@ -26,7 +26,7 @@ pub trait ActixAdminViewModelTrait {
async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError>; async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError>;
async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>; async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError>; async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError>;
async fn get_viewmodel_filter() -> HashMap<String, ActixAdminViewModelFilter>; async fn get_viewmodel_filter(db: &DatabaseConnection) -> HashMap<String, ActixAdminViewModelFilter>;
fn validate_entity(model: &mut ActixAdminModel); fn validate_entity(model: &mut ActixAdminModel);
fn get_entity_name() -> String; fn get_entity_name() -> String;
@ -58,7 +58,9 @@ pub struct ActixAdminViewModelSerializable {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct ActixAdminViewModelFilter { pub struct ActixAdminViewModelFilter {
pub name: String, pub name: String,
pub value: Option<String> pub value: Option<String>,
pub values: Option<Vec<(String, String)>>,
pub filter_type: Option<ActixAdminModelFilterType>
} }
// TODO: better alternative to serialize only specific fields for ActixAdminViewModel // TODO: better alternative to serialize only specific fields for ActixAdminViewModel