add search with 'contains'

This commit is contained in:
Manuel Gugger 2022-07-31 16:33:02 +02:00
parent 09b9845b3d
commit a8ae8eff55
13 changed files with 76 additions and 31 deletions

View File

@ -12,7 +12,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 select_list: Option<syn::LitStr>,
pub searchable: Option<()>
//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

@ -13,7 +13,8 @@ use struct_fields::{
get_primary_key_field_name, get_primary_key_field_name,
get_actix_admin_fields_select_list, get_actix_admin_fields_select_list,
get_actix_admin_fields_is_option_list, get_actix_admin_fields_is_option_list,
get_fields_for_validate_model get_fields_for_validate_model,
get_actix_admin_fields_searchable
}; };
mod selectlist_fields; mod selectlist_fields;
@ -44,6 +45,7 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
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 fields_for_validate_model = get_fields_for_validate_model(&fields); let fields_for_validate_model = get_fields_for_validate_model(&fields);
let fields_searchable = get_actix_admin_fields_searchable(&fields);
let select_lists = get_select_lists(&fields); let select_lists = get_select_lists(&fields);
@ -96,8 +98,8 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
#[async_trait(?Send)] #[async_trait(?Send)]
impl ActixAdminViewModelTrait for Entity { impl ActixAdminViewModelTrait for Entity {
async fn list(db: &DatabaseConnection, page: usize, entities_per_page: usize) -> (usize, Vec<ActixAdminModel>) { async fn list(db: &DatabaseConnection, page: usize, entities_per_page: usize, search: &String) -> (usize, Vec<ActixAdminModel>) {
let entities = Entity::list_model(db, page, entities_per_page).await; let entities = Entity::list_model(db, page, entities_per_page, search).await;
entities entities
} }
@ -163,9 +165,13 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
#[async_trait] #[async_trait]
impl ActixAdminModelTrait for Entity { impl ActixAdminModelTrait for Entity {
async fn list_model(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> (usize, Vec<ActixAdminModel>) { async fn list_model(db: &DatabaseConnection, page: usize, posts_per_page: usize, search: &String) -> (usize, Vec<ActixAdminModel>) {
use sea_orm::{ query::* }; use sea_orm::{ query::* };
let paginator = Entity::find() let paginator = Entity::find()
.filter(
Condition::any()
#(#fields_searchable)*
)
.order_by_asc(Column::Id) .order_by_asc(Column::Id)
.paginate(db, posts_per_page); .paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap(); let num_pages = paginator.num_pages().await.ok().unwrap();

View File

@ -11,7 +11,8 @@ pub struct ModelField {
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 pub select_list: String,
pub searchable: bool
} }
impl ModelField { impl ModelField {

View File

@ -1,8 +1,8 @@
use crate::attributes::derive_attr; use crate::attributes::derive_attr;
use crate::model_fields::ModelField; use crate::model_fields::ModelField;
use proc_macro2::TokenStream; use proc_macro2::{Span, TokenStream};
use quote::quote; use quote::quote;
use syn::{DeriveInput, Fields, LitStr}; use syn::{DeriveInput, Fields, LitStr, Ident};
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();
@ -16,6 +16,10 @@ pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::V
fields fields
} }
fn capitalize_first_letter(s: &str) -> String {
s[0..1].to_uppercase() + &s[1..]
}
pub fn filter_fields(fields: &Fields) -> Vec<ModelField> { pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
fields fields
.iter() .iter()
@ -31,6 +35,9 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
let is_primary_key = actix_admin_attr let is_primary_key = actix_admin_attr
.clone() .clone()
.map_or(false, |attr| attr.primary_key.is_some()); .map_or(false, |attr| attr.primary_key.is_some());
let is_searchable = actix_admin_attr
.clone()
.map_or(false, |attr| attr.searchable.is_some());
let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| { let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| {
attr.select_list.map_or("".to_string(), |attr_field| { attr.select_list.map_or("".to_string(), |attr_field| {
(LitStr::from(attr_field)).value() (LitStr::from(attr_field)).value()
@ -51,6 +58,7 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
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, select_list: select_list,
searchable: is_searchable
}; };
Some(model_field) Some(model_field)
} else { } else {
@ -146,6 +154,20 @@ pub fn get_actix_admin_fields_html_input(fields: &Vec<ModelField>) -> Vec<TokenS
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub fn get_actix_admin_fields_searchable(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()
.filter(|model_field| model_field.searchable)
.map(|model_field| {
let column_name = format!("{}", capitalize_first_letter(&model_field.ident.to_string()));
let column_ident = Ident::new(&column_name, Span::call_site());
quote! {
.add(Column::#column_ident.contains(&search))
}
})
.collect::<Vec<_>>()
}
pub fn get_actix_admin_fields_select_list(fields: &Vec<ModelField>) -> Vec<TokenStream> { pub fn get_actix_admin_fields_select_list(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields fields
.iter() .iter()

View File

@ -10,6 +10,7 @@ pub trait ActixAdminModelTrait {
db: &DatabaseConnection, db: &DatabaseConnection,
page: usize, page: usize,
posts_per_page: usize, posts_per_page: usize,
search: &String
) -> (usize, Vec<ActixAdminModel>); ) -> (usize, Vec<ActixAdminModel>);
fn get_fields() -> Vec<ActixAdminViewModelField>; fn get_fields() -> Vec<ActixAdminViewModelField>;
fn validate_model(model: &ActixAdminModel) -> HashMap<String, String>; fn validate_model(model: &ActixAdminModel) -> HashMap<String, String>;

View File

@ -35,9 +35,10 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
.entities_per_page .entities_per_page
.unwrap_or(DEFAULT_ENTITIES_PER_PAGE); .unwrap_or(DEFAULT_ENTITIES_PER_PAGE);
let render_partial = params.render_partial.unwrap_or(false); let render_partial = params.render_partial.unwrap_or(false);
let search = params.search.clone().unwrap_or(String::new());
let db = data.get_db(); let db = data.get_db();
let result: (usize, Vec<ActixAdminModel>) = E::list(db, page, entities_per_page).await; let result: (usize, Vec<ActixAdminModel>) = E::list(db, page, entities_per_page, &search).await;
let entities = result.1; let entities = result.1;
let num_pages = result.0; let num_pages = result.0;

View File

@ -10,6 +10,7 @@ pub trait ActixAdminViewModelTrait {
db: &DatabaseConnection, db: &DatabaseConnection,
page: usize, page: usize,
entities_per_page: usize, entities_per_page: usize,
search: &String
) -> (usize, Vec<ActixAdminModel>); ) -> (usize, Vec<ActixAdminModel>);
// TODO: Replace return value with proper Result Type containing Ok or Err // TODO: Replace return value with proper Result Type containing Ok or Err

View File

@ -5,7 +5,7 @@
{% else %} {% else %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -15,11 +15,12 @@
<script src="https://unpkg.com/htmx.org@1.7.0"></script> <script src="https://unpkg.com/htmx.org@1.7.0"></script>
<style type="text/css"> <style type="text/css">
.full-width { width: 100%; }
</style> </style>
</head> </head>
<body> <body>
<main class="container"> <main class="container-fluid">
{% include "header.html" %} {% include "header.html" %}
<div> <div>
{% block content %} {% block content %}

View File

@ -8,8 +8,8 @@
<label for="{{ model_field.field_name }}"> <label for="{{ model_field.field_name }}">
{{ model_field.field_name | split(pat="_") | join(sep=" ") | title }} {{ model_field.field_name | split(pat="_") | join(sep=" ") | title }}
{% if model_field.select_list != "" %} {% if model_field.select_list != "" %}
<select name="{{ model_field.field_name }}" <select name="{{ model_field.field_name }}" {% if model.errors | get(key=model_field.field_name,
{% if model.errors | get(key=model_field.field_name, default="" ) !="" %} placeholder="Invalid" aria-invalid="true" {% endif %}> default="" ) !="" %} placeholder="Invalid" aria-invalid="true" {% endif %}>
{% if model_field.is_option %} {% if model_field.is_option %}
<option value=""></option> <option value=""></option>
{% else %} {% else %}
@ -17,24 +17,28 @@
{% endif %} {% endif %}
{% for select_list_item in select_lists[model_field.field_name] -%} {% 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, default="" ) %} <option {% if select_list_item[0]==model.values | get(key=model_field.field_name, default="" ) %}
selected {% endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] | split(pat="_") | join(sep=" ") | title }}</option> selected {% endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] | split(pat="_") |
join(sep=" ") | title }}</option>
{%- endfor %} {%- endfor %}
</select> </select>
</label> </label>
{% else %} {% else %}
<input type="{{ model_field.html_input_type }}" <input type="{{ model_field.html_input_type }}"
value="{{ model.values | get(key=model_field.field_name, default="") | split(pat="_") | join(sep=" ") | title }}" value="{{ model.values | get(key=model_field.field_name, default="") | split(pat=" _") | join(sep=" " )
name="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}" | title }}" name="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}" {% if model.errors | get(key=model_field.field_name,
{% if model.errors | get(key=model_field.field_name, default="") != "" %} default="" ) !="" %} placeholder="Invalid" aria-invalid="true" {% endif %}>
placeholder="Invalid" aria-invalid="true"
{% endif %}
>
{% endif %} {% endif %}
</label> </label>
{%- endfor %} {%- endfor %}
<button type="submit">Save</button> <div class="grid">
<a href="{{ list_link }}" role="button" class="secondary">Cancel</a> <div>
<button type="submit">Save</button>
</div>
<div>
<a href="{{ list_link }}" role="button" class="secondary full-width">Cancel</a>
</div>
</div>
</div> </div>
</form> </form>
</article> </article>

View File

@ -5,24 +5,22 @@
{% if not render_partial or render_partial == false %} {% if not render_partial or render_partial == false %}
<article> <article>
<div class="grid"> <div class="grid">
<div>
<a href="create" role="button">Create</a>
</div>
<!--
<div> <div>
<details role="list"> <details role="list">
<summary aria-haspopup="listbox" role="button"> <summary aria-haspopup="listbox" role="button">
With selected Actions
</summary> </summary>
<ul role="listbox"> <ul role="listbox">
<li><a>Delete</a></li> <li><a href="create">Create</a></li>
<li><a>Delete Selected</a></li>
</ul> </ul>
</details> </details>
</div>--> </div>
<div> <div>
<input type="search" id="search" name="search" placeholder="Search" <input type="search" id="search" name="search" placeholder="Search"
hx-get="/admin/{{ entity_name }}/list?render_partial=true&entities_per_page={{ entities_per_page }}&page={{ page }}" hx-get="/admin/{{ entity_name }}/list?render_partial=true&entities_per_page={{ entities_per_page }}&page={{ page }}"
hx-trigger="keyup changed delay:500ms, search" hx-target="#{{ entity_name }}table" hx-trigger="keyup changed delay:500ms, search" hx-target="#{{ entity_name }}table"
hx-indicator=".htmx-indicator"
> >
</div> </div>
</div> </div>
@ -32,7 +30,7 @@
<thead> <thead>
<tr> <tr>
<th> <th>
<!-- Select Checkbox --> <div class="htmx-indicator">{% include "spinner.svg" %}</div>
</th> </th>
<th>{{ view_model.primary_key | title }}</th> <th>{{ view_model.primary_key | title }}</th>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}

View File

@ -0,0 +1,7 @@
<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>

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

View File

@ -12,8 +12,10 @@ pub struct Model {
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
#[actix_admin(primary_key)] #[actix_admin(primary_key)]
pub id: i32, pub id: i32,
#[actix_admin(searchable)]
pub title: String, pub title: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
#[actix_admin(searchable)]
pub text: String, pub text: String,
#[actix_admin(select_list="Tea")] #[actix_admin(select_list="Tea")]
pub tea_mandatory: Tea, pub tea_mandatory: Tea,